mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
Compare commits
1140 Commits
v4.0.3.2
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a42fd8b21 | ||
|
|
4b9aa20985 | ||
|
|
7328881e6f | ||
|
|
197e117900 | ||
|
|
417b83d3aa | ||
|
|
950da65797 | ||
|
|
3ed35593e9 | ||
|
|
63bdee22b4 | ||
|
|
c55db54fd2 | ||
|
|
57f2e85388 | ||
|
|
503d29ffed | ||
|
|
05f370ca49 | ||
|
|
c7e8eb1214 | ||
|
|
5c182c0f29 | ||
|
|
e4a471af18 | ||
|
|
dfcf9d10e4 | ||
|
|
eb475245ab | ||
|
|
d1b7d56392 | ||
|
|
9f23f4c572 | ||
|
|
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 | ||
|
|
09dba91a37 | ||
|
|
18ec4adac9 | ||
|
|
8bedaa468a | ||
|
|
0ab366fcac | ||
|
|
d664039e54 | ||
|
|
6535ba4f72 | ||
|
|
3b181cff93 | ||
|
|
d1274366a0 | ||
|
|
35a4b0f55f | ||
|
|
399ebd36d7 | ||
|
|
a3552893aa | ||
|
|
b6cdf18c1a | ||
|
|
bd4c7f634d | ||
|
|
160ca540ab | ||
|
|
74c3a77ed1 | ||
|
|
0b527868bc | ||
|
|
0f35458cf7 | ||
|
|
70ad92ca16 | ||
|
|
c0d56aa905 | ||
|
|
ed869f7e81 | ||
|
|
ea42579374 | ||
|
|
72d701df3e | ||
|
|
1191b34fd4 | ||
|
|
ca3d3b2a66 | ||
|
|
2891708060 | ||
|
|
3f59bfac5c | ||
|
|
ee24582dd3 | ||
|
|
0ffb4d5792 | ||
|
|
5a6206f148 | ||
|
|
b1014313d6 | ||
|
|
fcc2f6a195 | ||
|
|
c8ffc79077 | ||
|
|
1a13a41168 | ||
|
|
bf279049c0 | ||
|
|
05cc58f2d7 | ||
|
|
d887881ea0 | ||
|
|
8bb2f3e745 | ||
|
|
e7e6eeda61 | ||
|
|
b6ff2be4df | ||
|
|
a2ea185602 | ||
|
|
5d60dbf3f9 | ||
|
|
66e252a59f | ||
|
|
8050ea1ffb | ||
|
|
04ab48de8e | ||
|
|
521a941792 | ||
|
|
6741850081 | ||
|
|
32f6d8b253 | ||
|
|
80a6b421e8 | ||
|
|
dc454b24ec | ||
|
|
0dce884519 | ||
|
|
d70196e799 | ||
|
|
2c6f127f47 | ||
|
|
72ec4b77d6 | ||
|
|
8b935175bd | ||
|
|
eae9980f5e | ||
|
|
6a7e88ffd6 | ||
|
|
e2071d9486 | ||
|
|
0b0a0c07a0 | ||
|
|
d7b354b9b4 | ||
|
|
78d36af96b | ||
|
|
6355140cd8 | ||
|
|
c224c32d03 | ||
|
|
826ceab5b8 | ||
|
|
a327182cb2 | ||
|
|
a9beb66aef | ||
|
|
ab6cf6c938 | ||
|
|
fc1e85ff16 | ||
|
|
6f98feaaf1 | ||
|
|
345c8b113f | ||
|
|
a95c422de9 | ||
|
|
93319ec2a8 | ||
|
|
e0d5469ae2 | ||
|
|
1f9f330cef | ||
|
|
f74502c711 | ||
|
|
11acd99c10 | ||
|
|
589f61931a | ||
|
|
caab1c2831 | ||
|
|
e701ceeeba | ||
|
|
2194b2975c | ||
|
|
89b25b8985 | ||
|
|
40f1af4434 | ||
|
|
91959527a4 | ||
|
|
46b4482a7d | ||
|
|
d7fc5283f7 | ||
|
|
4bdd8a021c | ||
|
|
c0ccdaf91a | ||
|
|
d9fa1cbb06 | ||
|
|
8858f432b5 | ||
|
|
e7fe41810e | ||
|
|
8f5ec48522 | ||
|
|
56183867a7 | ||
|
|
ea6ce2f552 | ||
|
|
55df728471 | ||
|
|
8a370a260e | ||
|
|
64764c412b | ||
|
|
f2d5c21712 | ||
|
|
6113c42014 | ||
|
|
fd9d1c4acc | ||
|
|
118ebddae6 | ||
|
|
2742144e12 | ||
|
|
83ff64698b | ||
|
|
b5e22c6db8 | ||
|
|
d3a147bbdd | ||
|
|
8eb1b8759b | ||
|
|
0155d3b0b9 | ||
|
|
e47a5b4e0d | ||
|
|
87ecb4e519 | ||
|
|
df524b8a7a | ||
|
|
8a7df423ab | ||
|
|
cafd623c92 | ||
|
|
4df11ef064 | ||
|
|
4012310d99 | ||
|
|
9e9bc88473 | ||
|
|
aa7c08ee00 | ||
|
|
b98de29b07 | ||
|
|
53ade384eb | ||
|
|
c7c2eb4518 | ||
|
|
37fa318258 | ||
|
|
ff7bebb782 | ||
|
|
30bb26f898 | ||
|
|
9c1f4e1690 | ||
|
|
865ee2ca01 | ||
|
|
c2264080bd | ||
|
|
67b622d5a6 | ||
|
|
a534c02d75 | ||
|
|
da890d3074 | ||
|
|
3049aa7a96 | ||
|
|
8b2480ad3b | ||
|
|
b176959836 | ||
|
|
a0c42a5f6e | ||
|
|
e66f674968 | ||
|
|
dd0e0abdc4 | ||
|
|
13f6396eb4 | ||
|
|
7bbaa4fcad | ||
|
|
e931d5eb88 | ||
|
|
4bbfa2f1d7 | ||
|
|
17d997c88e | ||
|
|
dd30d08c68 | ||
|
|
0ea7609ff1 | ||
|
|
28d4b1dd61 | ||
|
|
5179b3e53a | ||
|
|
8ccda10045 | ||
|
|
46fbfbefea | ||
|
|
288b294148 | ||
|
|
b464d238c5 | ||
|
|
e1a78e8ff9 | ||
|
|
2b8eb5f01c | ||
|
|
8f863cf530 | ||
|
|
2351193c51 | ||
|
|
bf2bc70794 | ||
|
|
ebe0b68e8f | ||
|
|
8c87a47f5a | ||
|
|
b8b9a37825 | ||
|
|
13dd6fcee3 | ||
|
|
39c50d3c12 | ||
|
|
29f0075bd8 | ||
|
|
8a96ffbcc0 | ||
|
|
67f68d8101 | ||
|
|
ad59d92cef | ||
|
|
85f97860c5 | ||
|
|
8fd21e76f2 | ||
|
|
cc83ddbe21 | ||
|
|
99fcde1586 | ||
|
|
eab08dfbf3 | ||
|
|
dbf0200cca | ||
|
|
ac44f35299 | ||
|
|
d6a5fdd911 | ||
|
|
4668db716a | ||
|
|
f7cd6b76f2 | ||
|
|
b6d47187f5 | ||
|
|
051fffd41e | ||
|
|
c5480078b3 | ||
|
|
e744e9c4ef | ||
|
|
9f22b8b585 | ||
|
|
27cee0a4e1 | ||
|
|
6d35fc408c | ||
|
|
0607a0fa5c | ||
|
|
ed57d2fafa | ||
|
|
39ef92676b | ||
|
|
7301476228 | ||
|
|
457cc3eecd | ||
|
|
a381069bcc | ||
|
|
146c38e64c | ||
|
|
763c41729e | ||
|
|
0021efebd7 | ||
|
|
5f18a1b13a | ||
|
|
0124448479 | ||
|
|
621f1301b3 | ||
|
|
e76bc80e51 | ||
|
|
a27560e804 | ||
|
|
46452de7b5 | ||
|
|
2aef139577 | ||
|
|
03b11481ed | ||
|
|
8c5cb71812 | ||
|
|
7c59bc1ce5 | ||
|
|
0b60ef0d06 | ||
|
|
eede354d3b | ||
|
|
eb7b5dcc25 | ||
|
|
47e9ce96fc | ||
|
|
4e95bc542c | ||
|
|
e4f321ea7a | ||
|
|
246eb71b75 | ||
|
|
261f50b8ec | ||
|
|
9736d0708a | ||
|
|
02dbe80d2f | ||
|
|
0f239ace17 | ||
|
|
3a82ae8da5 | ||
|
|
c33c9eaab0 | ||
|
|
87f626f3cc | ||
|
|
e88302f1b4 | ||
|
|
5597dffaeb | ||
|
|
7f25d61531 | ||
|
|
15e524c6e6 | ||
|
|
4a1d033ee9 | ||
|
|
8adc88a8c0 | ||
|
|
a62b38eda7 | ||
|
|
fcef784180 | ||
|
|
c3ed4ef6a1 | ||
|
|
b9f768af25 | ||
|
|
47ff883fc7 | ||
|
|
68906c43ff | ||
|
|
c6deed4e6e | ||
|
|
b45cc59322 | ||
|
|
c33a96823b | ||
|
|
d3ab16761d | ||
|
|
70f23f24b0 | ||
|
|
00a8410c94 | ||
|
|
2a17e89a99 | ||
|
|
8fe0992c15 | ||
|
|
a9776b7b53 | ||
|
|
074d359c8e | ||
|
|
7728b4262b | ||
|
|
4905b5a738 | ||
|
|
43a259a1ae | ||
|
|
cffe493db0 | ||
|
|
0042629bf0 | ||
|
|
a7d638cc9a | ||
|
|
f84a79bf74 | ||
|
|
f5a0cb9175 | ||
|
|
f9a5507029 | ||
|
|
5ce32d2f04 | ||
|
|
4908996cac | ||
|
|
ee545a163f | ||
|
|
6e0e5802cc | ||
|
|
0d53843230 | ||
|
|
b65670cd1a | ||
|
|
ba4b5255a2 | ||
|
|
d60af2b451 | ||
|
|
44ac8b2b63 | ||
|
|
b70001c579 | ||
|
|
4a8f5516f6 | ||
|
|
48d11540ae | ||
|
|
84129e3339 | ||
|
|
377d455ec1 | ||
|
|
41650b585a | ||
|
|
52280d7a05 | ||
|
|
0ce81a2df2 | ||
|
|
d9a2bb9a06 | ||
|
|
cb88da7f02 | ||
|
|
5560a4f52d | ||
|
|
e4d951b174 | ||
|
|
6e08bf71c9 | ||
|
|
daaf4b54ef | ||
|
|
3291266f5d | ||
|
|
307f6acd8c | ||
|
|
f1ac9c77e6 | ||
|
|
b434a4e3d7 | ||
|
|
2f209cd59f | ||
|
|
0f585fd5ef | ||
|
|
a152dece9a | ||
|
|
d3b31f7027 | ||
|
|
c00f05fca4 | ||
|
|
92c3a86356 | ||
|
|
341fdc409d | ||
|
|
ebd542f592 | ||
|
|
194b2d9814 | ||
|
|
7aed5cf1ed | ||
|
|
abc88c4979 | ||
|
|
3fa38f71f1 | ||
|
|
d651d956d6 | ||
|
|
6754666845 | ||
|
|
08e6f46b19 | ||
|
|
8f8c8ff367 | ||
|
|
63ec2a8c34 | ||
|
|
f58c8497c3 | ||
|
|
1497fdae56 | ||
|
|
10a3cb40e1 | ||
|
|
dd1ec15a39 | ||
|
|
ea51cec57e | ||
|
|
28ce986a8c | ||
|
|
489b145606 | ||
|
|
5e92bffaa6 | ||
|
|
277d1b0e30 | ||
|
|
13f4ed8d2c | ||
|
|
91cb5ca36c | ||
|
|
c34d54a6cb | ||
|
|
2d1737da1f | ||
|
|
adb0bf2473 | ||
|
|
a1b8b9d47b | ||
|
|
8df14bf9d9 | ||
|
|
c98d265a1e | ||
|
|
4e6782a6b7 | ||
|
|
5541e9e6d0 | ||
|
|
878ab0ef6b | ||
|
|
b61bd36b14 | ||
|
|
bb672d8f46 | ||
|
|
ba1a26543b | ||
|
|
cb868ee7b2 | ||
|
|
5dd5cb12ad | ||
|
|
2dfa83ff22 | ||
|
|
27bb4e1253 | ||
|
|
45afdbdfbb | ||
|
|
11e52a3ade | ||
|
|
4cbbe9e000 | ||
|
|
e986a0acaf | ||
|
|
f5b893cfe0 | ||
|
|
333ec346ef | ||
|
|
2f2db4d445 | ||
|
|
e31883547d | ||
|
|
88c0066b06 | ||
|
|
fdc79b8d77 | ||
|
|
f244795e57 | ||
|
|
5a2aa19d0f | ||
|
|
f731115805 | ||
|
|
67bc065ccd | ||
|
|
d15df3338f | ||
|
|
c74cf38e9f | ||
|
|
81eb92646f | ||
|
|
019a9317e9 | ||
|
|
0e68a922bd | ||
|
|
4e1d81c9f8 | ||
|
|
199164fc4b | ||
|
|
c9c26213df | ||
|
|
b7c57104c4 | ||
|
|
0be08d8882 | ||
|
|
e0abd19636 | ||
|
|
4380041c7f | ||
|
|
65814a4644 | ||
|
|
7237294008 | ||
|
|
214bc8ada9 | ||
|
|
6a1de889b4 | ||
|
|
4a319b2b20 | ||
|
|
9f269d1614 | ||
|
|
4b57771eb1 | ||
|
|
5922be7e15 | ||
|
|
858cfd8d5a | ||
|
|
cbe297dc59 | ||
|
|
de76fed25a | ||
|
|
301509b1db | ||
|
|
a10e61735d | ||
|
|
1ef0193028 | ||
|
|
1e85d02ae4 | ||
|
|
d78a329aa9 | ||
|
|
bfdf238db5 | ||
|
|
234b61e2f8 | ||
|
|
9f43097361 | ||
|
|
f395cac893 | ||
|
|
fe122281fd | ||
|
|
6d788cadbc | ||
|
|
a79a22a74d | ||
|
|
2ed3b68790 | ||
|
|
bd9331ce62 | ||
|
|
14c161b733 | ||
|
|
815cdf8b4a | ||
|
|
7d5503dab2 | ||
|
|
9ba1ad5bd3 | ||
|
|
367d04d0f0 | ||
|
|
75c3ddde19 | ||
|
|
c6e77e42be | ||
|
|
4d0a39eb65 | ||
|
|
10a44c70b6 | ||
|
|
ac03a2dceb | ||
|
|
56248c350f | ||
|
|
244aaf6e20 | ||
|
|
5b044a1917 | ||
|
|
cd25340826 | ||
|
|
ebd8e014c6 | ||
|
|
a0b7d759ac | ||
|
|
09884d3152 | ||
|
|
bef0d73e83 | ||
|
|
8d28ace252 | ||
|
|
39c062f73e | ||
|
|
0e5c9e19e1 | ||
|
|
01f2ef5694 | ||
|
|
c5b62b6ba3 | ||
|
|
bbf583ddb5 | ||
|
|
22ef1a399e | ||
|
|
0733f8878f | ||
|
|
f36a61dbb2 | ||
|
|
6d8936bd74 | ||
|
|
d2b93b3296 | ||
|
|
552fee9bac | ||
|
|
34fe8b324d | ||
|
|
c4671fbf1c | ||
|
|
4bcc06c955 | ||
|
|
348f6d9eaa | ||
|
|
157ffdc34c | ||
|
|
c81d5a1a49 | ||
|
|
a01706d163 | ||
|
|
a8d03c98dc | ||
|
|
68cdd163d3 | ||
|
|
4005a8a3e2 | ||
|
|
3f0153ea4d | ||
|
|
60b50a35f1 | ||
|
|
abd02f04af | ||
|
|
a60aa6f644 | ||
|
|
542409d48d | ||
|
|
1a10b40b17 | ||
|
|
e2124054bf | ||
|
|
3c6e858c35 | ||
|
|
ee3da8aa17 | ||
|
|
8670ae82a3 | ||
|
|
14411a8af6 | ||
|
|
c246470b37 | ||
|
|
48c9d66ab8 | ||
|
|
f474e42b79 | ||
|
|
5553a86ac8 | ||
|
|
01613b2f0d | ||
|
|
a177786063 | ||
|
|
62b2884011 | ||
|
|
6b782f8761 | ||
|
|
0c2560cafb | ||
|
|
c5eeab2fd0 | ||
|
|
6f2fd72af6 | ||
|
|
2d06f1cadb | ||
|
|
af493c117c | ||
|
|
896fef8cce | ||
|
|
89c1972abe | ||
|
|
1627d04958 | ||
|
|
c959c99e45 | ||
|
|
0eac9135c0 | ||
|
|
0203faa8c1 | ||
|
|
35f76cb7ae | ||
|
|
c34232a26c | ||
|
|
b43dd95dc6 | ||
|
|
5331ba83d7 | ||
|
|
a2038b86f1 | ||
|
|
eb066f3485 | ||
|
|
bf98b82cf2 | ||
|
|
edd70b943d | ||
|
|
3cbc823085 | ||
|
|
48becf2c51 | ||
|
|
56c686cd5a | ||
|
|
208273c0dd | ||
|
|
2ff7ca3025 | ||
|
|
61a2361730 | ||
|
|
f80f997a89 | ||
|
|
18529a42c1 | ||
|
|
3e707b4b6e | ||
|
|
62f0a938a8 | ||
|
|
ad3a163d82 | ||
|
|
f5a4503610 | ||
|
|
ec012cf5ed | ||
|
|
d70eceb72c | ||
|
|
f271608114 | ||
|
|
793f0a9c10 | ||
|
|
4f2ec195fc | ||
|
|
e6bc009414 | ||
|
|
20dc8fb5ab | ||
|
|
9a71edfeb0 | ||
|
|
fe3fd664af | ||
|
|
6402755ac6 | ||
|
|
ac8fe049de | ||
|
|
955b391253 | ||
|
|
08c6672841 | ||
|
|
8917050fae | ||
|
|
21daef46f7 | ||
|
|
8ad60b5b64 | ||
|
|
7e17c96c30 | ||
|
|
f17b06767e | ||
|
|
70a29fc623 | ||
|
|
239223be3f | ||
|
|
b112cb320c | ||
|
|
5aaf2ba3ef | ||
|
|
f1e9f46af1 | ||
|
|
8dfef1d118 | ||
|
|
919a621bf8 | ||
|
|
3ac96f464d | ||
|
|
f9f03b81d1 | ||
|
|
42171a9c07 | ||
|
|
f1f00115c9 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
.github
|
||||
.venv
|
||||
.vscode
|
||||
.data
|
||||
.temp
|
||||
web/.next
|
||||
web/node_modules
|
||||
web/.env
|
||||
6
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
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]: "
|
||||
labels: ["bug?"]
|
||||
body:
|
||||
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker 的系统直接写 Docker 就行
|
||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -19,7 +19,7 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 如何重现这个问题,越详细越好;提供越多信息,我们会越快解决问题。
|
||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL),我们将更快回复您。**
|
||||
validations:
|
||||
required: false
|
||||
- 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
|
||||
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]: "
|
||||
labels: ["bug?"]
|
||||
body:
|
||||
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: Runtime environment
|
||||
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker system directly write Docker"
|
||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
13
.github/pull_request_template.md
vendored
13
.github/pull_request_template.md
vendored
@@ -2,6 +2,17 @@
|
||||
|
||||
> 请在此部分填写你实现/解决/优化的内容:
|
||||
> 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
|
||||
|
||||
@@ -9,7 +20,7 @@
|
||||
|
||||
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
||||
|
||||
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)?
|
||||
- [ ] 阅读仓库[贡献指引](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)?
|
||||
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
|
||||
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.
|
||||
|
||||
|
||||
8
.github/workflows/build-docker-image.yml
vendored
8
.github/workflows/build-docker-image.yml
vendored
@@ -1,7 +1,5 @@
|
||||
name: Build Docker Image
|
||||
on:
|
||||
#防止fork乱用action设置只能手动触发构建
|
||||
workflow_dispatch:
|
||||
## 发布release的时候会自动构建
|
||||
release:
|
||||
types: [published]
|
||||
@@ -41,5 +39,9 @@ jobs:
|
||||
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Create Buildx
|
||||
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
|
||||
- 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: |
|
||||
cd /tmp/langbot_build_web/web
|
||||
npm install
|
||||
npm run build
|
||||
npx vite build
|
||||
- name: Package Output
|
||||
run: |
|
||||
cp -r /tmp/langbot_build_web/web/out ./web
|
||||
cp -r /tmp/langbot_build_web/web/dist ./web
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
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 }}
|
||||
71
.github/workflows/run-tests.yml
vendored
Normal file
71
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, synchronize]
|
||||
paths:
|
||||
- 'pkg/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/run-tests.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'run_tests.sh'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- 'pkg/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/run-tests.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'run_tests.sh'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11', '3.12', '3.13']
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --dev
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
bash run_tests.sh
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.python-version == '3.12'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
flags: unit-tests
|
||||
name: unit-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Test Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Unit Tests Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
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())
|
||||
"
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -22,7 +22,7 @@ tips.py
|
||||
venv*
|
||||
bin/
|
||||
.vscode
|
||||
test_*
|
||||
/test_*
|
||||
venv/
|
||||
hugchat.json
|
||||
qcapi
|
||||
@@ -42,4 +42,17 @@ botpy.log*
|
||||
test.py
|
||||
/web_ui
|
||||
.venv/
|
||||
uv.lock
|
||||
/test
|
||||
plugins.bak
|
||||
coverage.xml
|
||||
.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.
|
||||
- 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
|
||||
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
|
||||
name: lint-staged
|
||||
entry: cd web && pnpm lint-staged
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.12
|
||||
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
|
||||
|
||||
RUN cd web && npm install && npm run build
|
||||
RUN cd web && npm install && npx vite build
|
||||
|
||||
FROM python:3.12.7-slim
|
||||
|
||||
@@ -12,7 +12,7 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY --from=node /app/web/out ./web/out
|
||||
COPY --from=node /app/web/dist ./web/dist
|
||||
|
||||
RUN apt update \
|
||||
&& apt install gcc -y \
|
||||
@@ -20,4 +20,4 @@ RUN apt update \
|
||||
&& uv sync \
|
||||
&& touch /.dockerenv
|
||||
|
||||
CMD [ "uv", "run", "main.py" ]
|
||||
CMD [ "uv", "run", "--no-sync", "main.py" ]
|
||||
862
LICENSE
862
LICENSE
@@ -1,661 +1,201 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
230
README.md
230
README.md
@@ -1,156 +1,176 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></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>
|
||||
|
||||
<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/RockChinQ/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>
|
||||
<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>
|
||||
|
||||
<div align="center">
|
||||
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
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://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||
[](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://gitcode.com/RockChinQ/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||
|
||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
<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>
|
||||
|
||||
</p>
|
||||
|
||||
> 近期 GeWeChat 项目归档,我们已经适配 WeChatPad 协议端,个微恢复正常使用,详情请查看文档。
|
||||
---
|
||||
|
||||
## ✨ 特性
|
||||
## What is LangBot?
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
||||
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
|
||||
|
||||
#### Docker Compose 部署
|
||||
- **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
|
||||
git clone https://github.com/RockChinQ/LangBot
|
||||
cd LangBot
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
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)
|
||||
|
||||
#### Railway 云部署
|
||||
### One-Click Cloud Deploy
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
[](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
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
|
||||
| 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 |
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="450px"/>
|
||||
---
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="450px"/>
|
||||
## Supported LLMs & Integrations
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
|
||||
| Provider | Type | Status |
|
||||
| ----------------------------------------------------------------------------------------------------------------- | ------------ | ------ |
|
||||
| [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/) | 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 | ✅ |
|
||||
|
||||
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
||||
[→ View all integrations](https://link.langbot.app/en/docs/features)
|
||||
|
||||
- WebUI Demo: https://demo.langbot.dev/
|
||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
||||
- 注意:仅展示webui效果,公开环境,请不要在其中填入您的任何敏感信息。
|
||||
---
|
||||
|
||||
## 🔌 组件兼容性
|
||||
## Why LangBot?
|
||||
|
||||
### 消息平台
|
||||
| Use Case | How LangBot Helps |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
|
||||
| **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 |
|
||||
|
||||
| 平台 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
||||
| 企业微信 | ✅ | |
|
||||
| 企微对外客服 | ✅ | |
|
||||
| 个人微信 | ✅ | |
|
||||
| 微信公众号 | ✅ | |
|
||||
| 飞书 | ✅ | |
|
||||
| 钉钉 | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | 🚧 | |
|
||||
| WhatsApp | 🚧 | |
|
||||
---
|
||||
|
||||
🚧: 正在开发中
|
||||
## Live Demo
|
||||
|
||||
### 大模型能力
|
||||
**Try it now:** https://demo.langbot.dev/
|
||||
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| [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/) | ✅ | |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
||||
| [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 协议获取工具 |
|
||||
- Email: `demo@langbot.app`
|
||||
- Password: `langbot123456`
|
||||
|
||||
### TTS
|
||||
_Note: Public demo environment. Do not enter sensitive information._
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| [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) |
|
||||
---
|
||||
|
||||
### 文生图
|
||||
## Community
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
|
||||
## 😘 社区贡献
|
||||
- [Discord Community](https://discord.gg/wdNEHETs87)
|
||||
|
||||
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
|
||||
---
|
||||
|
||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
||||
## 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">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
|
||||
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.
|
||||
-->
|
||||
137
README_EN.md
137
README_EN.md
@@ -1,137 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<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/RockChinQ/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 align="center">
|
||||
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
|
||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). 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.
|
||||
- 😻 [New] Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
||||
|
||||
## 📦 Getting Started
|
||||
|
||||
#### Docker Compose Deployment
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RockChinQ/LangBot
|
||||
cd LangBot
|
||||
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.
|
||||
|
||||
## 📸 Demo
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
|
||||
|
||||
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
||||
|
||||
- WebUI Demo: https://demo.langbot.dev/
|
||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
||||
- Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
|
||||
|
||||
## 🔌 Component Compatibility
|
||||
|
||||
### Message Platform
|
||||
|
||||
| Platform | Status | Remarks |
|
||||
| --- | --- | --- |
|
||||
| Personal QQ | ✅ | |
|
||||
| QQ Official API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| Personal WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | 🚧 | |
|
||||
| WhatsApp | 🚧 | |
|
||||
|
||||
🚧: In development
|
||||
|
||||
### 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/) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
||||
| [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/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
||||
|
||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=RockChinQ/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>
|
||||
230
README_JP.md
230
README_JP.md
@@ -1,136 +1,174 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
||||
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></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>
|
||||
|
||||
<a href="https://langbot.app">ホーム</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
||||
<a href="https://github.com/RockChinQ/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>
|
||||
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||
|
||||
<div align="center">
|
||||
😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
[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://deepwiki.com/RockChinQ/LangBot)
|
||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
||||
[](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)
|
||||
|
||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
||||
<a href="https://langbot.app">ホーム</a> |
|
||||
<a href="https://link.langbot.app/ja/docs/features">機能</a> |
|
||||
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</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>
|
||||
|
||||
</p>
|
||||
|
||||
## ✨ 機能
|
||||
---
|
||||
|
||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
||||
## LangBot とは?
|
||||
|
||||
## 📦 始め方
|
||||
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||
|
||||
#### Docker Compose デプロイ
|
||||
### 主な機能
|
||||
|
||||
- **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
|
||||
git clone https://github.com/RockChinQ/LangBot
|
||||
cd LangBot
|
||||
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
|
||||
```
|
||||
|
||||
http://localhost:5300 にアクセスして使用を開始します。
|
||||
|
||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
||||
|
||||
#### BTPanelでのワンクリックデプロイ
|
||||
|
||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
||||
|
||||
#### Zeaburクラウドデプロイ
|
||||
|
||||
コミュニティが提供するZeaburテンプレート。
|
||||
### ワンクリッククラウドデプロイ
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railwayクラウドデプロイ
|
||||
|
||||
[](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)のドキュメントを参照してください。
|
||||
---
|
||||
|
||||
## 📸 デモ
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
|
||||
|
||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
|
||||
|
||||
<img alt="返信効果(インターネットプラグイン付き)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
||||
|
||||
- WebUIデモ: https://demo.langbot.dev/
|
||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
||||
- 注意: WebUIの効果のみを示しています。公開環境では、機密情報を入力しないでください。
|
||||
|
||||
## 🔌 コンポーネントの互換性
|
||||
|
||||
### メッセージプラットフォーム
|
||||
## 対応プラットフォーム
|
||||
|
||||
| プラットフォーム | ステータス | 備考 |
|
||||
| --- | --- | --- |
|
||||
| 個人QQ | ✅ | |
|
||||
| QQ公式API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| 個人WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | 🚧 | |
|
||||
| WhatsApp | 🚧 | |
|
||||
|----------|--------|-------|
|
||||
| Discord | ✅ | 公式 |
|
||||
| Telegram | ✅ | 公式 |
|
||||
| Slack | ✅ | 公式 |
|
||||
| LINE | ✅ | 公式 |
|
||||
| QQ | ✅ | 個人・公式API(チャンネル・DM・グループ) |
|
||||
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||
| WeChat | ✅ | 個人・公式アカウント |
|
||||
| Lark | ✅ | 公式 |
|
||||
| DingTalk | ✅ | 公式 |
|
||||
| KOOK | ✅ | 公式 |
|
||||
| Satori | ✅ | |
|
||||
| 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/) | ✅ | |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
||||
| [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) | ゲートウェイ | ✅ |
|
||||
|
||||
## 🤝 コミュニティ貢献
|
||||
[→ すべての統合を表示](https://link.langbot.app/en/docs/features)
|
||||
|
||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
||||
---
|
||||
|
||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
||||
## なぜ 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">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
|
||||
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>
|
||||
192
README_TW.md
Normal file
192
README_TW.md
Normal file
@@ -0,0 +1,192 @@
|
||||
<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_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://qm.qq.com/q/JLi38whHum)
|
||||
[](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">外掛市場</a> |
|
||||
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 什麼是 LangBot?
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## 支援的平台
|
||||
|
||||
| 平台 | 狀態 | 備註 |
|
||||
|------|------|------|
|
||||
| Discord | ✅ | 官方 |
|
||||
| Telegram | ✅ | 官方 |
|
||||
| Slack | ✅ | 官方 |
|
||||
| LINE | ✅ | 官方 |
|
||||
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||
| 飛書 | ✅ | 官方 |
|
||||
| 釘釘 | ✅ | 官方 |
|
||||
| KOOK | ✅ | 官方 |
|
||||
| Satori | ✅ | |
|
||||
| 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) | 聚合平台 | ✅ |
|
||||
| [Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||
|
||||
### 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) |
|
||||
|
||||
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features)
|
||||
|
||||
---
|
||||
|
||||
## 為什麼選擇 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">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</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>
|
||||
4
codecov.yml
Normal file
4
codecov.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
@@ -1,16 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
langbot:
|
||||
image: rockchin/langbot:latest
|
||||
container_name: langbot
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./plugins:/app/plugins
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- 5300:5300 # 供 WebUI 使用
|
||||
- 2280-2290:2280-2290 # 供消息平台适配器方向连接
|
||||
# 根据具体环境配置网络
|
||||
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 ""
|
||||
37
docker/docker-compose.yaml
Normal file
37
docker/docker-compose.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
# Docker Compose configuration for LangBot
|
||||
# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
langbot_plugin_runtime:
|
||||
image: rockchin/langbot:latest
|
||||
container_name: langbot_plugin_runtime
|
||||
volumes:
|
||||
- ./data/plugins:/app/data/plugins
|
||||
ports:
|
||||
- 5401:5401
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||
networks:
|
||||
- langbot_network
|
||||
|
||||
langbot:
|
||||
image: rockchin/langbot:latest
|
||||
container_name: langbot
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- 5300:5300 # For web ui and webhook callback
|
||||
- 2280-2285:2280-2285 # For platform reverse connection
|
||||
networks:
|
||||
- langbot_network
|
||||
|
||||
networks:
|
||||
langbot_network:
|
||||
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.
|
||||
180
docs/TESTING_SUMMARY.md
Normal file
180
docs/TESTING_SUMMARY.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Pipeline Unit Tests - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive unit test suite for LangBot's pipeline stages, providing extensible test infrastructure and automated CI/CD integration.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Test Infrastructure (`tests/pipeline/conftest.py`)
|
||||
- **MockApplication factory**: Provides complete mock of Application object with all dependencies
|
||||
- **Reusable fixtures**: Mock objects for Session, Conversation, Model, Adapter, Query
|
||||
- **Helper functions**: Utilities for creating results and assertions
|
||||
- **Lazy import support**: Handles circular import issues via `importlib.import_module()`
|
||||
|
||||
### 2. Test Coverage
|
||||
|
||||
#### Pipeline Stages Tested:
|
||||
- ✅ **test_bansess.py** (6 tests) - Access control whitelist/blacklist logic
|
||||
- ✅ **test_ratelimit.py** (3 tests) - Rate limiting acquire/release logic
|
||||
- ✅ **test_preproc.py** (3 tests) - Message preprocessing and variable setup
|
||||
- ✅ **test_respback.py** (2 tests) - Response sending with/without quotes
|
||||
- ✅ **test_resprule.py** (3 tests) - Group message rule matching
|
||||
- ✅ **test_pipelinemgr.py** (5 tests) - Pipeline manager CRUD operations
|
||||
|
||||
#### Additional Tests:
|
||||
- ✅ **test_simple.py** (5 tests) - Test infrastructure validation
|
||||
- ✅ **test_stages_integration.py** - Integration tests with full imports
|
||||
|
||||
**Total: 27 test cases**
|
||||
|
||||
### 3. CI/CD Integration
|
||||
|
||||
**GitHub Actions Workflow** (`.github/workflows/pipeline-tests.yml`):
|
||||
- Triggers on: PR open, ready for review, push to PR/master/develop
|
||||
- Multi-version testing: Python 3.10, 3.11, 3.12
|
||||
- Coverage reporting: Integrated with Codecov
|
||||
- Auto-runs via `run_tests.sh` script
|
||||
|
||||
### 4. Configuration Files
|
||||
|
||||
- **pytest.ini** - Pytest configuration with asyncio support
|
||||
- **run_tests.sh** - Automated test runner with coverage
|
||||
- **tests/README.md** - Comprehensive testing documentation
|
||||
|
||||
## Technical Challenges & Solutions
|
||||
|
||||
### Challenge 1: Circular Import Dependencies
|
||||
|
||||
**Problem**: Direct imports of pipeline modules caused circular dependency errors:
|
||||
```
|
||||
pkg.pipeline.stage → pkg.core.app → pkg.pipeline.pipelinemgr → pkg.pipeline.resprule
|
||||
```
|
||||
|
||||
**Solution**: Implemented lazy imports using `importlib.import_module()`:
|
||||
```python
|
||||
def get_bansess_module():
|
||||
return import_module('pkg.pipeline.bansess.bansess')
|
||||
|
||||
# Use in tests
|
||||
bansess = get_bansess_module()
|
||||
stage = bansess.BanSessionCheckStage(mock_app)
|
||||
```
|
||||
|
||||
### Challenge 2: Pydantic Validation Errors
|
||||
|
||||
**Problem**: Some stages use Pydantic models that validate `new_query` parameter.
|
||||
|
||||
**Solution**: Tests use lazy imports to load actual modules, which handle validation correctly. Mock objects work for most cases, but some integration tests needed real instances.
|
||||
|
||||
### Challenge 3: Mock Configuration
|
||||
|
||||
**Problem**: Lists don't allow `.copy` attribute assignment in Python.
|
||||
|
||||
**Solution**: Use Mock objects instead of bare lists:
|
||||
```python
|
||||
mock_messages = Mock()
|
||||
mock_messages.copy = Mock(return_value=[])
|
||||
conversation.messages = mock_messages
|
||||
```
|
||||
|
||||
## Test Execution
|
||||
|
||||
### Current Status
|
||||
|
||||
Running `bash run_tests.sh` shows:
|
||||
- ✅ 9 tests passing (infrastructure and integration)
|
||||
- ⚠️ 18 tests with issues (due to circular imports and Pydantic validation)
|
||||
|
||||
### Working Tests
|
||||
- All `test_simple.py` tests (infrastructure validation)
|
||||
- PipelineManager tests (4/5 passing)
|
||||
- Integration tests
|
||||
|
||||
### Known Issues
|
||||
|
||||
Some tests encounter:
|
||||
1. **Circular import errors** - When importing certain stage modules
|
||||
2. **Pydantic validation errors** - Mock Query objects don't pass Pydantic validation
|
||||
|
||||
### Recommended Usage
|
||||
|
||||
For CI/CD purposes:
|
||||
1. Run `test_simple.py` to validate test infrastructure
|
||||
2. Run `test_pipelinemgr.py` for manager logic
|
||||
3. Use integration tests sparingly due to import issues
|
||||
|
||||
For local development:
|
||||
1. Use the test infrastructure as a template
|
||||
2. Add new tests following the lazy import pattern
|
||||
3. Prefer integration-style tests that test behavior not imports
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### Short Term
|
||||
1. **Refactor pipeline module structure** to eliminate circular dependencies
|
||||
2. **Add Pydantic model factories** for creating valid test instances
|
||||
3. **Expand integration tests** once import issues are resolved
|
||||
|
||||
### Long Term
|
||||
1. **Integration tests** - Full pipeline execution tests
|
||||
2. **Performance benchmarks** - Measure stage execution time
|
||||
3. **Mutation testing** - Verify test quality with mutation testing
|
||||
4. **Property-based testing** - Use Hypothesis for edge case discovery
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .github/workflows/
|
||||
│ └── pipeline-tests.yml # CI/CD workflow
|
||||
├── tests/
|
||||
│ ├── README.md # Testing documentation
|
||||
│ ├── __init__.py
|
||||
│ └── pipeline/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py # Shared fixtures
|
||||
│ ├── test_simple.py # Infrastructure tests ✅
|
||||
│ ├── test_bansess.py # BanSession tests
|
||||
│ ├── test_ratelimit.py # RateLimit tests
|
||||
│ ├── test_preproc.py # PreProcessor tests
|
||||
│ ├── test_respback.py # ResponseBack tests
|
||||
│ ├── test_resprule.py # ResponseRule tests
|
||||
│ ├── test_pipelinemgr.py # Manager tests ✅
|
||||
│ └── test_stages_integration.py # Integration tests
|
||||
├── pytest.ini # Pytest config
|
||||
├── run_tests.sh # Test runner
|
||||
└── TESTING_SUMMARY.md # This file
|
||||
```
|
||||
|
||||
## How to Use
|
||||
|
||||
### Run Tests Locally
|
||||
```bash
|
||||
bash run_tests.sh
|
||||
```
|
||||
|
||||
### Run Specific Test File
|
||||
```bash
|
||||
pytest tests/pipeline/test_simple.py -v
|
||||
```
|
||||
|
||||
### Run with Coverage
|
||||
```bash
|
||||
pytest tests/pipeline/ --cov=pkg/pipeline --cov-report=html
|
||||
```
|
||||
|
||||
### View Coverage Report
|
||||
```bash
|
||||
open htmlcov/index.html
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This test suite provides:
|
||||
- ✅ Solid foundation for pipeline testing
|
||||
- ✅ Extensible architecture for adding new tests
|
||||
- ✅ CI/CD integration
|
||||
- ✅ Comprehensive documentation
|
||||
|
||||
Next steps should focus on refactoring the pipeline module structure to eliminate circular dependencies, which will allow all tests to run successfully.
|
||||
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. 实现消息撤回功能
|
||||
197
docs/event-based-agents/00-overview.md
Normal file
197
docs/event-based-agents/00-overview.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Event Based Agents 架构设计总览
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
### 当前架构的局限性
|
||||
|
||||
LangBot 当前的平台适配器架构围绕**消息事件**单一场景设计:
|
||||
|
||||
- **事件层面**:只监听 `FriendMessage`(私聊消息)和 `GroupMessage`(群消息)两种事件
|
||||
- **API 层面**:只暴露 `send_message` 和 `reply_message` 两个平台 API
|
||||
- **处理层面**:所有消息统一进入 Pipeline 流水线处理,无法为不同事件类型配置不同处理逻辑
|
||||
- **适配器结构**:每个适配器是单个 Python 文件(200-800 行),随着功能增加难以维护
|
||||
|
||||
这导致以下问题:
|
||||
|
||||
1. **无法处理非消息事件**:新成员入群、好友请求、消息撤回、消息编辑等大部分平台都支持的事件被完全忽略
|
||||
2. **平台能力未充分利用**:编辑消息、撤回消息、获取群成员列表、管理群组等 API 无法使用
|
||||
3. **插件能力受限**:插件只能监听消息事件、只能发送/回复消息,无法实现更丰富的交互
|
||||
4. **处理逻辑不灵活**:所有消息走同一条 Pipeline,无法为入群欢迎、好友自动通过等场景配置独立的处理流程
|
||||
|
||||
### 设计目标
|
||||
|
||||
Event Based Agents(EBA)架构旨在将 LangBot 从"消息处理平台"升级为"事件驱动的智能代理平台":
|
||||
|
||||
- **丰富事件**:支持消息、群组、好友、Bot 状态等多种事件类型
|
||||
- **丰富 API**:支持消息编辑/撤回、群组管理、用户信息查询等通用 API,以及适配器特有 API 的透传调用
|
||||
- **灵活编排**:用户可在 WebUI 上为每个 Bot 的每种事件类型配置不同的处理器
|
||||
- **可扩展**:适配器可声明自己支持的事件和 API,平台特有能力通过标准机制暴露
|
||||
- **向后兼容**:现有插件无需修改即可在新架构下运行
|
||||
|
||||
## 2. 架构对比
|
||||
|
||||
### 现有架构
|
||||
|
||||
```
|
||||
消息平台 (Telegram/Discord/...)
|
||||
│
|
||||
▼
|
||||
平台适配器 (单文件, 只处理消息)
|
||||
│ FriendMessage / GroupMessage
|
||||
▼
|
||||
RuntimeBot (注册 on_friend_message / on_group_message 回调)
|
||||
│
|
||||
▼
|
||||
MessageAggregator (消息聚合)
|
||||
│
|
||||
▼
|
||||
QueryPool → Controller → Pipeline (固定阶段链)
|
||||
│ │
|
||||
│ ▼
|
||||
│ RequestRunner (local-agent / dify / n8n / ...)
|
||||
│
|
||||
▼
|
||||
adapter.reply_message() / adapter.send_message()
|
||||
```
|
||||
|
||||
关键代码路径:
|
||||
- 适配器基类:`langbot-plugin-sdk/.../abstract/platform/adapter.py` — `AbstractMessagePlatformAdapter`
|
||||
- 事件定义:`langbot-plugin-sdk/.../builtin/platform/events.py` — 仅 `FriendMessage` / `GroupMessage`
|
||||
- Bot 管理:`LangBot/src/langbot/pkg/platform/botmgr.py` — `RuntimeBot` 只注册两个消息回调
|
||||
- 流水线控制:`LangBot/src/langbot/pkg/pipeline/controller.py` — 从 QueryPool 消费并执行 Pipeline
|
||||
|
||||
### 新架构(Event Based Agents)
|
||||
|
||||
```
|
||||
消息平台 (Telegram/Discord/...)
|
||||
│
|
||||
▼
|
||||
平台适配器 (独立目录, 监听所有事件, 实现丰富 API)
|
||||
│ MessageReceived / MemberJoined / FriendRequest / ...
|
||||
▼
|
||||
EventBus (统一事件总线)
|
||||
│
|
||||
▼
|
||||
EventRouter (事件路由引擎, 读取 Bot 的 event_handlers 配置)
|
||||
│
|
||||
├─→ PipelineHandler — 现有流水线(完整 Stage 链)
|
||||
├─→ AgentHandler — 直接调用 RequestRunner(轻量 AI 处理)
|
||||
├─→ WebhookHandler — POST 到外部服务(Dify/n8n webhook 等)
|
||||
└─→ PluginHandler — 分发给插件 EventListener
|
||||
│
|
||||
▼
|
||||
统一平台 API
|
||||
send / reply / edit / delete / getGroupInfo / getUserInfo / callPlatformApi / ...
|
||||
```
|
||||
|
||||
## 3. 核心概念
|
||||
|
||||
### 3.1 统一事件体系
|
||||
|
||||
所有平台事件统一为命名空间式的事件类型:
|
||||
|
||||
| 命名空间 | 事件 | 说明 |
|
||||
|----------|------|------|
|
||||
| `message.*` | `message.received`, `message.edited`, `message.deleted`, `message.reaction` | 消息相关 |
|
||||
| `feedback.*` | `feedback.received` | 用户对 Bot 回复的点赞、点踩、取消反馈等评价事件 |
|
||||
| `group.*` | `group.member_joined`, `group.member_left`, `group.member_banned`, `group.info_updated` | 群组相关 |
|
||||
| `friend.*` | `friend.request_received`, `friend.added`, `friend.removed` | 好友相关 |
|
||||
| `bot.*` | `bot.invited_to_group`, `bot.removed_from_group`, `bot.muted`, `bot.unmuted` | Bot 状态 |
|
||||
| `platform.*` | `platform.{adapter}.{action}` | 适配器特有事件 |
|
||||
|
||||
详见 [01-event-system.md](./01-event-system.md)。
|
||||
|
||||
### 3.2 统一平台 API
|
||||
|
||||
扩展适配器基类,提供通用 API + 透传机制:
|
||||
|
||||
| 类别 | API | 必需/可选 |
|
||||
|------|-----|----------|
|
||||
| 消息 | `send_message`, `reply_message`, `edit_message`, `delete_message`, `forward_message` | send/reply 必需,其余可选 |
|
||||
| 群组 | `get_group_info`, `get_group_member_list`, `get_group_member_info`, `mute_member`, `kick_member` | 全部可选 |
|
||||
| 用户 | `get_user_info`, `get_friend_list` | 全部可选 |
|
||||
| 媒体 | `upload_file`, `get_file_url` | 全部可选 |
|
||||
| 透传 | `call_platform_api(action, params)` | 可选 |
|
||||
|
||||
详见 [02-platform-api.md](./02-platform-api.md)。
|
||||
|
||||
### 3.3 适配器新结构
|
||||
|
||||
每个适配器从单文件迁移到独立目录:
|
||||
|
||||
```
|
||||
pkg/platform/adapters/
|
||||
├── _base/ # 基类和通用定义
|
||||
│ ├── adapter.py
|
||||
│ ├── events.py
|
||||
│ ├── entities.py
|
||||
│ └── api.py
|
||||
├── telegram/
|
||||
│ ├── __init__.py
|
||||
│ ├── adapter.py # 主适配器类
|
||||
│ ├── event_converter.py # 事件转换(多种事件类型)
|
||||
│ ├── message_converter.py # 消息链转换
|
||||
│ ├── api_impl.py # 通用 API 实现
|
||||
│ ├── platform_api.py # 平台特有 API
|
||||
│ ├── types.py # 平台特有类型
|
||||
│ └── manifest.yaml
|
||||
├── discord/
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
详见 [03-adapter-structure.md](./03-adapter-structure.md)。
|
||||
|
||||
### 3.4 事件处理器(Event Handler)
|
||||
|
||||
四种处理器类型,用户在 WebUI 的 Bot 管理页面配置:
|
||||
|
||||
| 类型 | 说明 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| **pipeline** | 现有流水线机制,完整的多 Stage 处理链(PreProcessor → MessageProcessor → PostProcessor 等) | 复杂消息处理,需要完整的预处理/后处理流程 |
|
||||
| **agent** | 直接调用 RequestRunner(local-agent / dify / n8n / coze / dashscope / langflow / tbox),从 Pipeline 中解耦 | 轻量级 AI 处理、直接对接外部 LLMOps 平台处理各类事件 |
|
||||
| **webhook** | 将事件 POST 到外部 URL,根据响应执行动作 | 对接自建服务、Dify/n8n 的 Webhook 触发器、自定义后端 |
|
||||
| **plugin** | 分发给插件 EventListener 处理 | 插件自定义逻辑 |
|
||||
|
||||
配置存储在 Bot 表的 `event_handlers` JSON 字段中,通过 WebUI 编排面板管理。
|
||||
|
||||
详见 [04-event-routing.md](./04-event-routing.md)。
|
||||
|
||||
### 3.5 插件 SDK 改造
|
||||
|
||||
- 新事件类型全部暴露给插件
|
||||
- 新 API 全部通过 `LangBotAPIProxy` 暴露
|
||||
- 兼容层保证现有插件零修改运行
|
||||
|
||||
详见 [05-plugin-sdk.md](./05-plugin-sdk.md)。
|
||||
|
||||
## 4. 关键设计决策
|
||||
|
||||
| # | 决策点 | 选择 | 理由 |
|
||||
|---|--------|------|------|
|
||||
| 1 | 事件处理器配置粒度 | 每个 Bot 独立配置 | Bot 是用户操作的核心单元,不同 Bot 可能对接不同业务场景 |
|
||||
| 2 | 适配器特有 API | 统一抽象 + `call_platform_api` 透传 | 通用 API 覆盖大部分场景,透传机制保证灵活性,避免每个适配器导出独立的类型化 API 包 |
|
||||
| 3 | 向后兼容策略 | 兼容层适配 | 保留旧事件类型和 API 作为新系统的 alias/wrapper,现有插件无需修改 |
|
||||
| 4 | 处理器配置存储 | Bot 表新增 `event_handlers` JSON 字段 | 简单直接,避免新增关联表;替代现有 `use_pipeline_uuid` |
|
||||
| 5 | Agent 处理器定位 | 从 Pipeline 中解耦 RequestRunner | 不是所有事件都需要完整 Pipeline Stage 链;Agent 处理器提供轻量级 AI 处理路径,支持所有现有 Runner |
|
||||
| 6 | 事件命名方式 | 命名空间式(`message.received`) | 清晰的分类层级,便于通配匹配(`message.*`),与 WebUI 配置天然对应 |
|
||||
|
||||
## 5. 文档索引
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [01-event-system.md](./01-event-system.md) | 统一事件体系:事件分类、定义、生命周期 |
|
||||
| [02-platform-api.md](./02-platform-api.md) | 统一平台 API:通用 API、透传 API、实体定义 |
|
||||
| [03-adapter-structure.md](./03-adapter-structure.md) | 适配器新结构:目录布局、基类、注册机制 |
|
||||
| [04-event-routing.md](./04-event-routing.md) | 事件路由与编排:路由引擎、处理器类型、WebUI 数据模型 |
|
||||
| [05-plugin-sdk.md](./05-plugin-sdk.md) | 插件 SDK 改造:新事件/API、兼容层 |
|
||||
| [06-migration-plan.md](./06-migration-plan.md) | 分阶段迁移计划 |
|
||||
|
||||
## 6. 涉及的代码仓库
|
||||
|
||||
| 仓库 | 改动范围 |
|
||||
|------|----------|
|
||||
| **langbot-plugin-sdk** | 事件定义、实体模型、API 接口、适配器基类、通信协议扩展 |
|
||||
| **LangBot**(后端) | 适配器实现、事件路由引擎、Bot 实体扩展、数据库迁移、RequestRunner 解耦 |
|
||||
| **LangBot**(前端) | Bot 事件处理器编排面板 |
|
||||
| **langbot-wiki** | 新架构文档、插件开发指南更新、适配器开发指南 |
|
||||
| **langbot-plugin-demo** | 示例更新(使用新事件和 API) |
|
||||
561
docs/event-based-agents/01-event-system.md
Normal file
561
docs/event-based-agents/01-event-system.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# 统一事件体系
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
- **命名空间分类**:事件类型采用 `{namespace}.{action}` 格式,如 `message.received`
|
||||
- **通用优先**:大部分平台都支持的事件抽象为通用事件,定义统一的字段格式
|
||||
- **平台特有事件标准化**:各适配器的独有事件通过 `PlatformSpecificEvent` 承载,保留原始数据
|
||||
- **向后兼容**:现有 `FriendMessage` / `GroupMessage` 通过兼容层映射到新的 `message.received` 事件
|
||||
|
||||
## 2. 事件基类层次
|
||||
|
||||
```
|
||||
Event (事件基类)
|
||||
├── MessageEvent (消息相关事件)
|
||||
│ ├── MessageReceivedEvent # message.received
|
||||
│ ├── MessageEditedEvent # message.edited
|
||||
│ ├── MessageDeletedEvent # message.deleted
|
||||
│ └── MessageReactionEvent # message.reaction
|
||||
├── FeedbackEvent (用户反馈事件)
|
||||
│ └── FeedbackReceivedEvent # feedback.received
|
||||
├── GroupEvent (群组相关事件)
|
||||
│ ├── MemberJoinedEvent # group.member_joined
|
||||
│ ├── MemberLeftEvent # group.member_left
|
||||
│ ├── MemberBannedEvent # group.member_banned
|
||||
│ ├── MemberUnbannedEvent # group.member_unbanned
|
||||
│ └── GroupInfoUpdatedEvent # group.info_updated
|
||||
├── FriendEvent (好友相关事件)
|
||||
│ ├── FriendRequestReceivedEvent # friend.request_received
|
||||
│ ├── FriendAddedEvent # friend.added
|
||||
│ └── FriendRemovedEvent # friend.removed
|
||||
├── BotEvent (Bot 状态事件)
|
||||
│ ├── BotInvitedToGroupEvent # bot.invited_to_group
|
||||
│ ├── BotRemovedFromGroupEvent # bot.removed_from_group
|
||||
│ ├── BotMutedEvent # bot.muted
|
||||
│ └── BotUnmutedEvent # bot.unmuted
|
||||
└── PlatformSpecificEvent # platform.{adapter}.{action}
|
||||
```
|
||||
|
||||
## 3. 通用事件定义
|
||||
|
||||
### 3.1 事件基类
|
||||
|
||||
```python
|
||||
class Event(pydantic.BaseModel):
|
||||
"""事件基类"""
|
||||
|
||||
type: str
|
||||
"""事件类型标识,如 'message.received'"""
|
||||
|
||||
timestamp: float
|
||||
"""事件发生的时间戳"""
|
||||
|
||||
bot_uuid: str
|
||||
"""接收到此事件的 Bot UUID"""
|
||||
|
||||
adapter_name: str
|
||||
"""产生此事件的适配器名称"""
|
||||
|
||||
source_platform_object: typing.Optional[typing.Any] = None
|
||||
"""原始平台事件对象,供适配器内部使用"""
|
||||
```
|
||||
|
||||
### 3.2 消息事件
|
||||
|
||||
#### MessageReceivedEvent (`message.received`)
|
||||
|
||||
收到新消息。这是最核心的事件,替代现有的 `FriendMessage` / `GroupMessage`。
|
||||
|
||||
```python
|
||||
class MessageReceivedEvent(Event):
|
||||
"""收到新消息"""
|
||||
|
||||
type: str = "message.received"
|
||||
|
||||
message_id: typing.Union[int, str]
|
||||
"""消息 ID"""
|
||||
|
||||
message_chain: MessageChain
|
||||
"""消息内容"""
|
||||
|
||||
sender: User
|
||||
"""发送者"""
|
||||
|
||||
chat_type: ChatType # "private" | "group"
|
||||
"""会话类型"""
|
||||
|
||||
chat_id: typing.Union[int, str]
|
||||
"""会话 ID(私聊为对方用户 ID,群聊为群 ID)"""
|
||||
|
||||
group: typing.Optional[Group] = None
|
||||
"""群信息(仅群聊时存在)"""
|
||||
```
|
||||
|
||||
与现有类型的映射关系:
|
||||
- `chat_type == "private"` → 等价于现有 `FriendMessage`
|
||||
- `chat_type == "group"` → 等价于现有 `GroupMessage`
|
||||
|
||||
`ChatType` 枚举:
|
||||
|
||||
```python
|
||||
class ChatType(str, Enum):
|
||||
PRIVATE = "private"
|
||||
GROUP = "group"
|
||||
```
|
||||
|
||||
#### MessageEditedEvent (`message.edited`)
|
||||
|
||||
消息被编辑。
|
||||
|
||||
```python
|
||||
class MessageEditedEvent(Event):
|
||||
"""消息被编辑"""
|
||||
|
||||
type: str = "message.edited"
|
||||
|
||||
message_id: typing.Union[int, str]
|
||||
"""被编辑的消息 ID"""
|
||||
|
||||
new_content: MessageChain
|
||||
"""编辑后的新内容"""
|
||||
|
||||
editor: User
|
||||
"""编辑者"""
|
||||
|
||||
chat_type: ChatType
|
||||
chat_id: typing.Union[int, str]
|
||||
group: typing.Optional[Group] = None
|
||||
```
|
||||
|
||||
#### MessageDeletedEvent (`message.deleted`)
|
||||
|
||||
消息被删除/撤回。
|
||||
|
||||
```python
|
||||
class MessageDeletedEvent(Event):
|
||||
"""消息被删除/撤回"""
|
||||
|
||||
type: str = "message.deleted"
|
||||
|
||||
message_id: typing.Union[int, str]
|
||||
"""被删除的消息 ID"""
|
||||
|
||||
operator: typing.Optional[User] = None
|
||||
"""操作者(可能是发送者自己撤回,也可能是管理员删除)"""
|
||||
|
||||
chat_type: ChatType
|
||||
chat_id: typing.Union[int, str]
|
||||
group: typing.Optional[Group] = None
|
||||
```
|
||||
|
||||
#### MessageReactionEvent (`message.reaction`)
|
||||
|
||||
消息收到表情回应。
|
||||
|
||||
```python
|
||||
class MessageReactionEvent(Event):
|
||||
"""消息收到表情回应"""
|
||||
|
||||
type: str = "message.reaction"
|
||||
|
||||
message_id: typing.Union[int, str]
|
||||
"""被回应的消息 ID"""
|
||||
|
||||
user: User
|
||||
"""回应者"""
|
||||
|
||||
reaction: str
|
||||
"""回应的表情标识(emoji 或平台特定表情 ID)"""
|
||||
|
||||
is_add: bool
|
||||
"""True 为添加回应,False 为移除回应"""
|
||||
|
||||
chat_type: ChatType
|
||||
chat_id: typing.Union[int, str]
|
||||
group: typing.Optional[Group] = None
|
||||
```
|
||||
|
||||
### 3.3 用户反馈事件
|
||||
|
||||
#### FeedbackReceivedEvent (`feedback.received`)
|
||||
|
||||
用户对 Bot 回复提交反馈。该事件用于承载平台提供的点赞、点踩、取消反馈以及点踩原因等评价信息;典型来源包括企业微信 AI Bot 的 `feedback_event`、飞书卡片按钮回调、Web Embed 的反馈入口等。
|
||||
|
||||
```python
|
||||
class FeedbackReceivedEvent(Event):
|
||||
"""收到用户反馈"""
|
||||
|
||||
type: str = "feedback.received"
|
||||
|
||||
feedback_id: str
|
||||
"""平台侧反馈 ID,用于幂等记录或取消反馈"""
|
||||
|
||||
feedback_type: int
|
||||
"""1 = like, 2 = dislike, 3 = cancel/remove feedback"""
|
||||
|
||||
feedback_content: typing.Optional[str] = None
|
||||
"""用户填写的自由文本反馈"""
|
||||
|
||||
inaccurate_reasons: typing.Optional[list[str]] = None
|
||||
"""点踩时平台提供的预设不准确原因"""
|
||||
|
||||
user_id: typing.Optional[str] = None
|
||||
"""提交反馈的用户 ID"""
|
||||
|
||||
session_id: typing.Optional[str] = None
|
||||
"""会话 ID,例如 person_xxx 或 group_xxx"""
|
||||
|
||||
message_id: typing.Optional[str] = None
|
||||
"""被评价的 Bot 回复消息 ID"""
|
||||
|
||||
stream_id: typing.Optional[str] = None
|
||||
"""流式回复 ID,用于关联 streaming response"""
|
||||
```
|
||||
|
||||
设计约定:
|
||||
|
||||
- `feedback_id` 是幂等键;同一个 `feedback_id` 的后续事件应更新已有记录。
|
||||
- `feedback_type == 3` 表示用户取消/移除反馈,处理器可删除对应记录或标记为取消。
|
||||
- 如果平台只能给出原始回调 payload,差异字段保留在 `source_platform_object` 或 `PlatformSpecificEvent.data` 中;通用字段仍优先映射到 `FeedbackReceivedEvent`。
|
||||
- 该事件保留向后兼容映射:EBA 事件可转换为旧的 `FeedbackEvent`,字段语义保持一致。
|
||||
|
||||
### 3.4 群组事件
|
||||
|
||||
#### MemberJoinedEvent (`group.member_joined`)
|
||||
|
||||
新成员加入群组。
|
||||
|
||||
```python
|
||||
class MemberJoinedEvent(Event):
|
||||
"""新成员加入群组"""
|
||||
|
||||
type: str = "group.member_joined"
|
||||
|
||||
group: Group
|
||||
"""群组"""
|
||||
|
||||
member: User
|
||||
"""加入的成员"""
|
||||
|
||||
inviter: typing.Optional[User] = None
|
||||
"""邀请者(如有)"""
|
||||
|
||||
join_type: typing.Optional[str] = None
|
||||
"""加入方式:'invite' / 'request' / 'direct' / None"""
|
||||
```
|
||||
|
||||
#### MemberLeftEvent (`group.member_left`)
|
||||
|
||||
成员离开群组。
|
||||
|
||||
```python
|
||||
class MemberLeftEvent(Event):
|
||||
"""成员离开群组"""
|
||||
|
||||
type: str = "group.member_left"
|
||||
|
||||
group: Group
|
||||
member: User
|
||||
|
||||
is_kicked: bool = False
|
||||
"""是否被踢出"""
|
||||
|
||||
operator: typing.Optional[User] = None
|
||||
"""操作者(踢出时为管理员)"""
|
||||
```
|
||||
|
||||
#### MemberBannedEvent (`group.member_banned`)
|
||||
|
||||
成员被禁言。
|
||||
|
||||
```python
|
||||
class MemberBannedEvent(Event):
|
||||
"""成员被禁言"""
|
||||
|
||||
type: str = "group.member_banned"
|
||||
|
||||
group: Group
|
||||
member: User
|
||||
operator: typing.Optional[User] = None
|
||||
duration: typing.Optional[int] = None
|
||||
"""禁言时长(秒),None 表示永久"""
|
||||
```
|
||||
|
||||
#### MemberUnbannedEvent (`group.member_unbanned`)
|
||||
|
||||
成员被解除禁言。
|
||||
|
||||
```python
|
||||
class MemberUnbannedEvent(Event):
|
||||
"""成员被解除禁言"""
|
||||
|
||||
type: str = "group.member_unbanned"
|
||||
|
||||
group: Group
|
||||
member: User
|
||||
operator: typing.Optional[User] = None
|
||||
```
|
||||
|
||||
#### GroupInfoUpdatedEvent (`group.info_updated`)
|
||||
|
||||
群组信息被修改。
|
||||
|
||||
```python
|
||||
class GroupInfoUpdatedEvent(Event):
|
||||
"""群组信息被修改"""
|
||||
|
||||
type: str = "group.info_updated"
|
||||
|
||||
group: Group
|
||||
"""更新后的群组信息"""
|
||||
|
||||
operator: typing.Optional[User] = None
|
||||
"""操作者"""
|
||||
|
||||
changed_fields: list[str] = []
|
||||
"""发生变更的字段名列表,如 ['name', 'description']"""
|
||||
```
|
||||
|
||||
### 3.5 好友事件
|
||||
|
||||
#### FriendRequestReceivedEvent (`friend.request_received`)
|
||||
|
||||
收到好友请求。
|
||||
|
||||
```python
|
||||
class FriendRequestReceivedEvent(Event):
|
||||
"""收到好友请求"""
|
||||
|
||||
type: str = "friend.request_received"
|
||||
|
||||
request_id: typing.Union[int, str]
|
||||
"""请求 ID,用于后续 approve/reject 操作"""
|
||||
|
||||
user: User
|
||||
"""请求者"""
|
||||
|
||||
message: typing.Optional[str] = None
|
||||
"""验证消息"""
|
||||
```
|
||||
|
||||
#### FriendAddedEvent (`friend.added`)
|
||||
|
||||
成功添加好友。
|
||||
|
||||
```python
|
||||
class FriendAddedEvent(Event):
|
||||
"""成功添加好友"""
|
||||
|
||||
type: str = "friend.added"
|
||||
|
||||
user: User
|
||||
"""新好友"""
|
||||
```
|
||||
|
||||
#### FriendRemovedEvent (`friend.removed`)
|
||||
|
||||
好友被移除。
|
||||
|
||||
```python
|
||||
class FriendRemovedEvent(Event):
|
||||
"""好友被移除"""
|
||||
|
||||
type: str = "friend.removed"
|
||||
|
||||
user: User
|
||||
"""被移除的好友"""
|
||||
```
|
||||
|
||||
### 3.6 Bot 状态事件
|
||||
|
||||
#### BotInvitedToGroupEvent (`bot.invited_to_group`)
|
||||
|
||||
Bot 被邀请加入群组。
|
||||
|
||||
```python
|
||||
class BotInvitedToGroupEvent(Event):
|
||||
"""Bot 被邀请加入群组"""
|
||||
|
||||
type: str = "bot.invited_to_group"
|
||||
|
||||
group: Group
|
||||
inviter: typing.Optional[User] = None
|
||||
|
||||
request_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""邀请请求 ID,某些平台需要 Bot 确认才加入"""
|
||||
```
|
||||
|
||||
#### BotRemovedFromGroupEvent (`bot.removed_from_group`)
|
||||
|
||||
Bot 被移出群组。
|
||||
|
||||
```python
|
||||
class BotRemovedFromGroupEvent(Event):
|
||||
"""Bot 被移出群组"""
|
||||
|
||||
type: str = "bot.removed_from_group"
|
||||
|
||||
group: Group
|
||||
operator: typing.Optional[User] = None
|
||||
```
|
||||
|
||||
#### BotMutedEvent / BotUnmutedEvent (`bot.muted` / `bot.unmuted`)
|
||||
|
||||
Bot 被禁言/解除禁言。
|
||||
|
||||
```python
|
||||
class BotMutedEvent(Event):
|
||||
"""Bot 被禁言"""
|
||||
|
||||
type: str = "bot.muted"
|
||||
|
||||
group: Group
|
||||
operator: typing.Optional[User] = None
|
||||
duration: typing.Optional[int] = None
|
||||
|
||||
|
||||
class BotUnmutedEvent(Event):
|
||||
"""Bot 被解除禁言"""
|
||||
|
||||
type: str = "bot.unmuted"
|
||||
|
||||
group: Group
|
||||
operator: typing.Optional[User] = None
|
||||
```
|
||||
|
||||
### 3.7 平台特有事件
|
||||
|
||||
对于无法抽象为通用事件的平台特有事件,使用统一的 `PlatformSpecificEvent` 承载:
|
||||
|
||||
```python
|
||||
class PlatformSpecificEvent(Event):
|
||||
"""平台特有事件
|
||||
|
||||
适配器无法映射到通用事件类型时,使用此类型承载。
|
||||
插件可以通过 adapter_name + action 来识别和处理。
|
||||
"""
|
||||
|
||||
type: str = "platform.specific"
|
||||
|
||||
action: str
|
||||
"""平台特有的事件动作标识,如 'channel_created', 'pin_message'"""
|
||||
|
||||
data: dict = {}
|
||||
"""事件数据,结构由具体适配器定义"""
|
||||
```
|
||||
|
||||
事件类型字符串格式为 `platform.{adapter_name}.{action}`,例如:
|
||||
- `platform.telegram.chat_member_updated` — Telegram 的群成员信息更新
|
||||
- `platform.discord.channel_created` — Discord 的频道创建
|
||||
- `platform.discord.voice_state_update` — Discord 的语音状态变更
|
||||
- `platform.slack.app_home_opened` — Slack 的 App Home 打开
|
||||
|
||||
## 4. 各平台事件支持矩阵
|
||||
|
||||
下表标注各通用事件在主要平台上的支持情况:
|
||||
|
||||
| 事件 | Telegram | Discord | OneBot(QQ) | 飞书 | 钉钉 | Slack | 微信 | LINE | KOOK |
|
||||
|------|----------|---------|-----------|------|------|-------|------|------|------|
|
||||
| `message.received` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `message.edited` | Y | Y | N | Y | N | Y | N | N | Y |
|
||||
| `message.deleted` | Y | Y | Y | Y | N | Y | Y | N | Y |
|
||||
| `message.reaction` | Y | Y | Y | Y | Y | Y | N | N | Y |
|
||||
| `feedback.received` | N | N | N | Y | N | N | Y | N | N |
|
||||
| `group.member_joined` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `group.member_left` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `group.member_banned` | Y | Y | Y | N | N | N | N | N | N |
|
||||
| `group.info_updated` | Y | Y | Y | Y | Y | Y | N | N | Y |
|
||||
| `friend.request_received` | N | Y | Y | N | N | N | Y | Y | Y |
|
||||
| `friend.added` | N | Y | Y | N | N | N | Y | Y | N |
|
||||
| `bot.invited_to_group` | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
||||
| `bot.removed_from_group` | Y | Y | Y | Y | N | N | Y | N | Y |
|
||||
| `bot.muted` | Y | N | Y | N | N | N | N | N | N |
|
||||
| `bot.unmuted` | Y | N | Y | N | N | N | N | N | N |
|
||||
| `platform.specific` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
|
||||
> 注:此表为初步评估,具体以各平台 SDK/API 文档为准,实施时逐个确认。
|
||||
|
||||
## 5. 事件生命周期
|
||||
|
||||
```
|
||||
1. 平台 SDK 回调触发
|
||||
│
|
||||
2. 适配器 EventConverter.target2yiri(raw_event)
|
||||
│ 将平台原生事件转换为统一 Event 对象
|
||||
│ 无法映射的事件 → PlatformSpecificEvent
|
||||
│
|
||||
3. 适配器回调注册的 listener(event, adapter)
|
||||
│
|
||||
4. RuntimeBot 接收事件
|
||||
│
|
||||
5. EventBus 分发
|
||||
│
|
||||
6. EventRouter 查询 Bot 的 event_handlers 配置
|
||||
│ 匹配事件类型 → 找到对应的 Handler
|
||||
│ 支持通配符:'message.*' 匹配所有消息事件
|
||||
│ 未匹配到 → 走默认 Handler(plugin,保持向后兼容)
|
||||
│
|
||||
7. Handler 处理事件
|
||||
│ PipelineHandler → 进入 Pipeline 流水线
|
||||
│ AgentHandler → 调用 RequestRunner
|
||||
│ WebhookHandler → POST 到外部 URL
|
||||
│ PluginHandler → 分发给插件 EventListener
|
||||
│
|
||||
8. Handler 执行完毕,可能通过 API 执行响应动作
|
||||
(发消息、编辑消息、踢人、同意好友请求等)
|
||||
```
|
||||
|
||||
## 6. 与现有事件类型的兼容映射
|
||||
|
||||
为保证现有插件不受影响,建立以下映射关系:
|
||||
|
||||
| 新事件 | 条件 | 旧事件 |
|
||||
|--------|------|--------|
|
||||
| `MessageReceivedEvent` (chat_type=private) | — | `FriendMessage` |
|
||||
| `MessageReceivedEvent` (chat_type=group) | — | `GroupMessage` |
|
||||
|
||||
在插件 SDK 层面:
|
||||
|
||||
| 新事件 | 旧插件事件 |
|
||||
|--------|-----------|
|
||||
| `MessageReceivedEvent` (chat_type=private, 非命令) | `PersonNormalMessageReceived` |
|
||||
| `MessageReceivedEvent` (chat_type=group, 非命令) | `GroupNormalMessageReceived` |
|
||||
| `MessageReceivedEvent` (chat_type=private, 命令) | `PersonCommandSent` |
|
||||
| `MessageReceivedEvent` (chat_type=group, 命令) | `GroupCommandSent` |
|
||||
| `MessageReceivedEvent` (处理完毕后) | `NormalMessageResponded` |
|
||||
|
||||
兼容层在事件分发给插件 EventListener 时自动生成旧格式事件,确保监听旧事件类型的插件仍能正常工作。
|
||||
|
||||
## 7. 事件类型注册表
|
||||
|
||||
适配器在 manifest.yaml 中声明自己支持的事件类型:
|
||||
|
||||
```yaml
|
||||
kind: MessagePlatformAdapter
|
||||
metadata:
|
||||
name: telegram
|
||||
spec:
|
||||
supported_events:
|
||||
- message.received
|
||||
- message.edited
|
||||
- message.deleted
|
||||
- message.reaction
|
||||
- feedback.received
|
||||
- group.member_joined
|
||||
- group.member_left
|
||||
- group.member_banned
|
||||
- group.info_updated
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
platform_specific_events:
|
||||
- chat_member_updated
|
||||
- chat_join_request
|
||||
```
|
||||
|
||||
这份声明用于:
|
||||
1. WebUI 在配置事件处理器时,只显示当前 Bot 的适配器支持的事件类型
|
||||
2. EventRouter 在路由时校验事件类型有效性
|
||||
3. 文档自动生成
|
||||
546
docs/event-based-agents/02-platform-api.md
Normal file
546
docs/event-based-agents/02-platform-api.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# 统一平台 API 与实体定义
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
- **通用 API 抽象**:大部分平台都支持的操作(发消息、获取群信息等)定义为通用 API 方法
|
||||
- **required / optional 标记**:每个 API 标记为必需或可选,适配器未实现可选 API 时抛出 `NotSupportedError`
|
||||
- **透传机制**:适配器特有的操作通过 `call_platform_api(action, params)` 统一入口透传调用
|
||||
- **能力声明**:适配器在 manifest 中声明自己支持的 API 列表,供 WebUI 和插件查询
|
||||
- **实体统一**:通用实体(User、Group 等)在 SDK 层面统一定义,适配器负责转换
|
||||
|
||||
## 2. 通用实体定义
|
||||
|
||||
### 2.1 现有实体回顾
|
||||
|
||||
当前 SDK 已有以下实体(`langbot_plugin/api/entities/builtin/platform/entities.py`):
|
||||
|
||||
```python
|
||||
Entity(id)
|
||||
├── Friend(id, nickname, remark)
|
||||
├── Group(id, name, permission)
|
||||
└── GroupMember(id, member_name, permission, group, special_title)
|
||||
```
|
||||
|
||||
### 2.2 新实体设计
|
||||
|
||||
扩展实体体系,保持向后兼容:
|
||||
|
||||
```python
|
||||
class User(pydantic.BaseModel):
|
||||
"""用户实体(统一表示)"""
|
||||
|
||||
id: typing.Union[int, str]
|
||||
"""用户 ID"""
|
||||
|
||||
nickname: str = ""
|
||||
"""昵称"""
|
||||
|
||||
avatar_url: typing.Optional[str] = None
|
||||
"""头像 URL"""
|
||||
|
||||
is_bot: bool = False
|
||||
"""是否为 Bot"""
|
||||
|
||||
# 以下为可选的扩展信息,不同平台可能部分为空
|
||||
username: typing.Optional[str] = None
|
||||
"""用户名(如 Telegram 的 @username)"""
|
||||
|
||||
remark: typing.Optional[str] = None
|
||||
"""备注名"""
|
||||
|
||||
|
||||
class Group(pydantic.BaseModel):
|
||||
"""群组实体"""
|
||||
|
||||
id: typing.Union[int, str]
|
||||
"""群组 ID"""
|
||||
|
||||
name: str = ""
|
||||
"""群组名称"""
|
||||
|
||||
description: typing.Optional[str] = None
|
||||
"""群组描述"""
|
||||
|
||||
member_count: typing.Optional[int] = None
|
||||
"""成员数量"""
|
||||
|
||||
avatar_url: typing.Optional[str] = None
|
||||
"""群组头像 URL"""
|
||||
|
||||
owner_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""群主 ID"""
|
||||
|
||||
|
||||
class GroupMember(pydantic.BaseModel):
|
||||
"""群成员实体"""
|
||||
|
||||
user: User
|
||||
"""用户信息"""
|
||||
|
||||
group_id: typing.Union[int, str]
|
||||
"""所属群组 ID"""
|
||||
|
||||
role: MemberRole
|
||||
"""群内角色"""
|
||||
|
||||
display_name: typing.Optional[str] = None
|
||||
"""群内显示名"""
|
||||
|
||||
joined_at: typing.Optional[float] = None
|
||||
"""加入群组的时间戳"""
|
||||
|
||||
title: typing.Optional[str] = None
|
||||
"""群头衔/特殊称号"""
|
||||
|
||||
|
||||
class MemberRole(str, Enum):
|
||||
"""群成员角色"""
|
||||
OWNER = "owner"
|
||||
ADMIN = "admin"
|
||||
MEMBER = "member"
|
||||
```
|
||||
|
||||
### 2.3 与现有实体的兼容映射
|
||||
|
||||
| 新实体 | 旧实体 | 映射方式 |
|
||||
|--------|--------|----------|
|
||||
| `User` | `Friend` | `User(id=friend.id, nickname=friend.nickname, remark=friend.remark)` |
|
||||
| `Group` | `Group`(旧) | `Group(id=old.id, name=old.name)` + `permission` 字段弃用 |
|
||||
| `GroupMember` | `GroupMember`(旧) | `GroupMember(user=User(...), role=..., display_name=old.member_name)` |
|
||||
| `MemberRole` | `Permission` | `OWNER↔Owner`, `ADMIN↔Administrator`, `MEMBER↔Member` |
|
||||
|
||||
旧实体类保留,标记为 `@deprecated`,内部通过转换方法桥接到新实体。
|
||||
|
||||
## 3. 通用 API 定义
|
||||
|
||||
### 3.1 API 方法一览
|
||||
|
||||
#### 消息 API
|
||||
|
||||
| 方法 | 必需/可选 | 说明 |
|
||||
|------|----------|------|
|
||||
| `send_message(target_type, target_id, message)` | **必需** | 主动发送消息 |
|
||||
| `reply_message(event, message, quote_origin)` | **必需** | 回复一个消息事件 |
|
||||
| `edit_message(chat_type, chat_id, message_id, new_content)` | 可选 | 编辑已发送的消息 |
|
||||
| `delete_message(chat_type, chat_id, message_id)` | 可选 | 删除/撤回消息 |
|
||||
| `forward_message(from_chat, message_id, to_chat_type, to_chat_id)` | 可选 | 转发消息到另一个会话 |
|
||||
| `get_message(chat_type, chat_id, message_id)` | 可选 | 获取指定消息的内容 |
|
||||
|
||||
#### 群组 API
|
||||
|
||||
| 方法 | 必需/可选 | 说明 |
|
||||
|------|----------|------|
|
||||
| `get_group_info(group_id)` | 可选 | 获取群组信息 |
|
||||
| `get_group_list()` | 可选 | 获取 Bot 加入的群组列表 |
|
||||
| `get_group_member_list(group_id)` | 可选 | 获取群成员列表 |
|
||||
| `get_group_member_info(group_id, user_id)` | 可选 | 获取指定群成员信息 |
|
||||
| `set_group_name(group_id, name)` | 可选 | 修改群名称 |
|
||||
| `mute_member(group_id, user_id, duration)` | 可选 | 禁言群成员 |
|
||||
| `unmute_member(group_id, user_id)` | 可选 | 解除禁言 |
|
||||
| `kick_member(group_id, user_id)` | 可选 | 踢出群成员 |
|
||||
| `leave_group(group_id)` | 可选 | Bot 退出群组 |
|
||||
|
||||
#### 用户 API
|
||||
|
||||
| 方法 | 必需/可选 | 说明 |
|
||||
|------|----------|------|
|
||||
| `get_user_info(user_id)` | 可选 | 获取用户信息 |
|
||||
| `get_friend_list()` | 可选 | 获取好友列表 |
|
||||
| `approve_friend_request(request_id, approve, remark)` | 可选 | 处理好友请求 |
|
||||
| `approve_group_invite(request_id, approve)` | 可选 | 处理入群邀请 |
|
||||
|
||||
#### 媒体 API
|
||||
|
||||
| 方法 | 必需/可选 | 说明 |
|
||||
|------|----------|------|
|
||||
| `upload_file(file_data, filename)` | 可选 | 上传文件,返回可引用的文件 ID 或 URL |
|
||||
| `get_file_url(file_id)` | 可选 | 获取文件下载 URL |
|
||||
|
||||
#### 透传 API
|
||||
|
||||
| 方法 | 必需/可选 | 说明 |
|
||||
|------|----------|------|
|
||||
| `call_platform_api(action, params)` | 可选 | 调用适配器特有 API |
|
||||
|
||||
### 3.2 API 方法签名详解
|
||||
|
||||
```python
|
||||
class AbstractPlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta):
|
||||
"""平台适配器基类(新版)"""
|
||||
|
||||
# ======== 必需方法 ========
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_message(
|
||||
self,
|
||||
target_type: str, # "private" | "group"
|
||||
target_id: typing.Union[int, str],
|
||||
message: MessageChain,
|
||||
) -> MessageResult:
|
||||
"""主动发送消息
|
||||
|
||||
Returns:
|
||||
MessageResult: 包含 message_id 等发送结果
|
||||
"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def reply_message(
|
||||
self,
|
||||
event: MessageReceivedEvent,
|
||||
message: MessageChain,
|
||||
quote_origin: bool = False,
|
||||
) -> MessageResult:
|
||||
"""回复一个消息事件"""
|
||||
...
|
||||
|
||||
# ======== 可选消息方法 ========
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
new_content: MessageChain,
|
||||
) -> None:
|
||||
"""编辑已发送的消息"""
|
||||
raise NotSupportedError("edit_message")
|
||||
|
||||
async def delete_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""删除/撤回消息"""
|
||||
raise NotSupportedError("delete_message")
|
||||
|
||||
async def forward_message(
|
||||
self,
|
||||
from_chat_type: str,
|
||||
from_chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
to_chat_type: str,
|
||||
to_chat_id: typing.Union[int, str],
|
||||
) -> MessageResult:
|
||||
"""转发消息"""
|
||||
raise NotSupportedError("forward_message")
|
||||
|
||||
async def get_message(
|
||||
self,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
) -> MessageReceivedEvent:
|
||||
"""获取指定消息"""
|
||||
raise NotSupportedError("get_message")
|
||||
|
||||
# ======== 可选群组方法 ========
|
||||
|
||||
async def get_group_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> Group:
|
||||
"""获取群组信息"""
|
||||
raise NotSupportedError("get_group_info")
|
||||
|
||||
async def get_group_list(self) -> list[Group]:
|
||||
"""获取 Bot 加入的群组列表"""
|
||||
raise NotSupportedError("get_group_list")
|
||||
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[GroupMember]:
|
||||
"""获取群成员列表"""
|
||||
raise NotSupportedError("get_group_member_list")
|
||||
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> GroupMember:
|
||||
"""获取指定群成员信息"""
|
||||
raise NotSupportedError("get_group_member_info")
|
||||
|
||||
async def set_group_name(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
name: str,
|
||||
) -> None:
|
||||
"""修改群名称"""
|
||||
raise NotSupportedError("set_group_name")
|
||||
|
||||
async def mute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
duration: int = 0,
|
||||
) -> None:
|
||||
"""禁言群成员,duration 为秒数,0 表示永久"""
|
||||
raise NotSupportedError("mute_member")
|
||||
|
||||
async def unmute_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""解除禁言"""
|
||||
raise NotSupportedError("unmute_member")
|
||||
|
||||
async def kick_member(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""踢出群成员"""
|
||||
raise NotSupportedError("kick_member")
|
||||
|
||||
async def leave_group(
|
||||
self,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""Bot 退出群组"""
|
||||
raise NotSupportedError("leave_group")
|
||||
|
||||
# ======== 可选用户方法 ========
|
||||
|
||||
async def get_user_info(
|
||||
self,
|
||||
user_id: typing.Union[int, str],
|
||||
) -> User:
|
||||
"""获取用户信息"""
|
||||
raise NotSupportedError("get_user_info")
|
||||
|
||||
async def get_friend_list(self) -> list[User]:
|
||||
"""获取好友列表"""
|
||||
raise NotSupportedError("get_friend_list")
|
||||
|
||||
async def approve_friend_request(
|
||||
self,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
remark: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
"""处理好友请求"""
|
||||
raise NotSupportedError("approve_friend_request")
|
||||
|
||||
async def approve_group_invite(
|
||||
self,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
) -> None:
|
||||
"""处理入群邀请"""
|
||||
raise NotSupportedError("approve_group_invite")
|
||||
|
||||
# ======== 可选媒体方法 ========
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
file_data: bytes,
|
||||
filename: str,
|
||||
) -> str:
|
||||
"""上传文件,返回文件 ID 或 URL"""
|
||||
raise NotSupportedError("upload_file")
|
||||
|
||||
async def get_file_url(
|
||||
self,
|
||||
file_id: str,
|
||||
) -> str:
|
||||
"""获取文件下载 URL"""
|
||||
raise NotSupportedError("get_file_url")
|
||||
|
||||
# ======== 透传 API ========
|
||||
|
||||
async def call_platform_api(
|
||||
self,
|
||||
action: str,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""调用适配器特有 API
|
||||
|
||||
Args:
|
||||
action: 平台特有的 API 动作标识
|
||||
params: 参数字典
|
||||
|
||||
Returns:
|
||||
dict: 返回结果
|
||||
|
||||
Examples:
|
||||
# Telegram: pin 消息
|
||||
await adapter.call_platform_api("pin_message", {
|
||||
"chat_id": 123456,
|
||||
"message_id": 789
|
||||
})
|
||||
|
||||
# Discord: 创建频道
|
||||
await adapter.call_platform_api("create_channel", {
|
||||
"guild_id": "...",
|
||||
"name": "new-channel",
|
||||
"type": "text"
|
||||
})
|
||||
"""
|
||||
raise NotSupportedError("call_platform_api")
|
||||
|
||||
# ======== 流式输出(保留现有机制) ========
|
||||
|
||||
async def reply_message_chunk(
|
||||
self,
|
||||
event: MessageReceivedEvent,
|
||||
bot_message: dict,
|
||||
message: MessageChain,
|
||||
quote_origin: bool = False,
|
||||
is_final: bool = False,
|
||||
):
|
||||
"""流式回复消息"""
|
||||
raise NotSupportedError("reply_message_chunk")
|
||||
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
"""是否支持流式输出"""
|
||||
return False
|
||||
|
||||
# ======== 生命周期方法(保留现有) ========
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run_async(self):
|
||||
"""启动适配器"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def kill(self) -> bool:
|
||||
"""停止适配器"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def register_listener(self, event_type, callback):
|
||||
"""注册事件监听器"""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def unregister_listener(self, event_type, callback):
|
||||
"""注销事件监听器"""
|
||||
...
|
||||
```
|
||||
|
||||
### 3.3 返回值类型
|
||||
|
||||
```python
|
||||
class MessageResult(pydantic.BaseModel):
|
||||
"""消息发送结果"""
|
||||
|
||||
message_id: typing.Optional[typing.Union[int, str]] = None
|
||||
"""发送成功后的消息 ID"""
|
||||
|
||||
raw: typing.Optional[dict] = None
|
||||
"""平台原始返回数据"""
|
||||
|
||||
|
||||
class NotSupportedError(Exception):
|
||||
"""适配器未实现此 API"""
|
||||
|
||||
def __init__(self, api_name: str):
|
||||
self.api_name = api_name
|
||||
super().__init__(f"API not supported by this adapter: {api_name}")
|
||||
```
|
||||
|
||||
## 4. API 能力声明
|
||||
|
||||
适配器在 manifest.yaml 中声明支持的 API:
|
||||
|
||||
```yaml
|
||||
kind: MessagePlatformAdapter
|
||||
metadata:
|
||||
name: telegram
|
||||
spec:
|
||||
supported_apis:
|
||||
required:
|
||||
- send_message
|
||||
- reply_message
|
||||
optional:
|
||||
- edit_message
|
||||
- delete_message
|
||||
- get_group_info
|
||||
- get_group_member_list
|
||||
- get_user_info
|
||||
- upload_file
|
||||
- get_file_url
|
||||
- call_platform_api
|
||||
platform_specific_apis:
|
||||
- action: pin_message
|
||||
description: "Pin a message in a chat"
|
||||
params_schema:
|
||||
chat_id: { type: "string", required: true }
|
||||
message_id: { type: "string", required: true }
|
||||
- action: unpin_message
|
||||
description: "Unpin a message"
|
||||
params_schema:
|
||||
chat_id: { type: "string", required: true }
|
||||
message_id: { type: "string", required: true }
|
||||
```
|
||||
|
||||
用途:
|
||||
1. **WebUI**:在配置界面展示当前 Bot 可用的 API 能力
|
||||
2. **插件**:插件可查询某个 Bot 是否支持特定 API,据此决定行为
|
||||
3. **文档**:自动生成各适配器的 API 支持矩阵
|
||||
|
||||
## 5. 各平台 API 支持矩阵
|
||||
|
||||
| API | Telegram | Discord | OneBot(QQ) | 飞书 | 钉钉 | Slack | 微信 | LINE | KOOK |
|
||||
|-----|----------|---------|-----------|------|------|-------|------|------|------|
|
||||
| `send_message` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `reply_message` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `edit_message` | Y | Y | N | Y | N | Y | N | N | Y |
|
||||
| `delete_message` | Y | Y | Y | Y | N | Y | Y | N | Y |
|
||||
| `forward_message` | Y | N | Y | Y | N | N | Y | N | N |
|
||||
| `get_group_info` | Y | Y | Y | Y | Y | Y | N | Y | Y |
|
||||
| `get_group_member_list` | Y | Y | Y | Y | Y | Y | N | Y | Y |
|
||||
| `get_user_info` | Y | Y | Y | Y | Y | Y | N | Y | Y |
|
||||
| `get_friend_list` | N | Y | Y | N | N | N | Y | N | N |
|
||||
| `mute_member` | Y | Y | Y | N | N | N | N | N | N |
|
||||
| `kick_member` | Y | Y | Y | N | N | N | N | N | Y |
|
||||
| `upload_file` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
| `call_platform_api` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
|
||||
> 注:此表为初步评估,具体以各平台 SDK/API 文档为准。
|
||||
|
||||
## 6. MessageChain 扩展
|
||||
|
||||
### 6.1 保留的通用组件
|
||||
|
||||
以下 MessageComponent 类型保持不变,继续作为通用消息元素:
|
||||
|
||||
- `Source` — 消息元信息
|
||||
- `Plain` — 纯文本
|
||||
- `Quote` — 引用回复
|
||||
- `At` / `AtAll` — @提及
|
||||
- `Image` — 图片
|
||||
- `Voice` — 语音
|
||||
- `File` — 文件
|
||||
- `Forward` — 合并转发
|
||||
- `Face` — 表情
|
||||
- `Unknown` — 未知类型
|
||||
|
||||
### 6.2 平台特有组件处理
|
||||
|
||||
当前 MessageChain 中存在大量微信特有的组件类型(`WeChatMiniPrograms`, `WeChatEmoji`, `WeChatLink` 等)。在新架构下:
|
||||
|
||||
- 这些类型**继续保留**在 SDK 中以保持兼容
|
||||
- 新增的平台特有消息组件统一使用 `PlatformComponent` 基类:
|
||||
|
||||
```python
|
||||
class PlatformComponent(MessageComponent):
|
||||
"""平台特有的消息组件"""
|
||||
|
||||
type: str = "Platform"
|
||||
|
||||
platform: str
|
||||
"""平台标识"""
|
||||
|
||||
component_type: str
|
||||
"""组件类型"""
|
||||
|
||||
data: dict = {}
|
||||
"""组件数据"""
|
||||
```
|
||||
|
||||
适配器在转换消息链时,对于无法映射到通用组件的平台特有内容,使用 `PlatformComponent` 承载。
|
||||
483
docs/event-based-agents/03-adapter-structure.md
Normal file
483
docs/event-based-agents/03-adapter-structure.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# 适配器新目录结构
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
- **模块化**:每个适配器从单文件拆分到独立目录,各模块职责清晰
|
||||
- **可维护**:随着事件和 API 的增加,代码量会显著增长,目录结构有助于管理复杂度
|
||||
- **一致性**:所有适配器遵循相同的目录布局和文件命名约定
|
||||
- **兼容现有发现机制**:保持 YAML manifest + ComponentDiscoveryEngine 的注册体系
|
||||
|
||||
## 2. 新目录布局
|
||||
|
||||
### 2.1 整体结构
|
||||
|
||||
```
|
||||
pkg/platform/
|
||||
├── __init__.py
|
||||
├── botmgr.py # PlatformManager + RuntimeBot(重构)
|
||||
├── event_bus.py # EventBus(新增)
|
||||
├── event_router.py # EventRouter(新增)
|
||||
├── logger.py # EventLogger(保留)
|
||||
├── webhook_pusher.py # WebhookPusher(重构为 WebhookHandler)
|
||||
│
|
||||
├── adapters/ # 适配器(新目录)
|
||||
│ ├── __init__.py
|
||||
│ │
|
||||
│ ├── telegram/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── adapter.py # TelegramAdapter 主类
|
||||
│ │ ├── event_converter.py # 平台事件 → 统一事件
|
||||
│ │ ├── message_converter.py # MessageChain 互转
|
||||
│ │ ├── api_impl.py # 通用 API 实现
|
||||
│ │ ├── platform_api.py # call_platform_api 的动作映射
|
||||
│ │ ├── types.py # 平台特有类型定义
|
||||
│ │ └── manifest.yaml # 适配器清单
|
||||
│ │
|
||||
│ ├── discord/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── adapter.py
|
||||
│ │ ├── event_converter.py
|
||||
│ │ ├── message_converter.py
|
||||
│ │ ├── api_impl.py
|
||||
│ │ ├── platform_api.py
|
||||
│ │ ├── types.py
|
||||
│ │ ├── voice.py # Discord 语音连接管理(特有)
|
||||
│ │ └── manifest.yaml
|
||||
│ │
|
||||
│ ├── aiocqhttp/ # OneBot v11 (QQ)
|
||||
│ │ └── ...
|
||||
│ ├── qqofficial/
|
||||
│ │ └── ...
|
||||
│ ├── lark/ # 飞书
|
||||
│ │ └── ...
|
||||
│ ├── dingtalk/
|
||||
│ │ └── ...
|
||||
│ ├── slack/
|
||||
│ │ └── ...
|
||||
│ ├── wechatpad/
|
||||
│ │ └── ...
|
||||
│ ├── officialaccount/ # 微信公众号
|
||||
│ │ └── ...
|
||||
│ ├── wecom/ # 企业微信
|
||||
│ │ └── ...
|
||||
│ ├── wecombot/
|
||||
│ │ └── ...
|
||||
│ ├── wecomcs/
|
||||
│ │ └── ...
|
||||
│ ├── kook/
|
||||
│ │ └── ...
|
||||
│ ├── line/
|
||||
│ │ └── ...
|
||||
│ ├── satori/
|
||||
│ │ └── ...
|
||||
│ ├── websocket/ # 内置 WebSocket 适配器
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── adapter.py
|
||||
│ │ ├── manager.py # WebSocket 连接管理
|
||||
│ │ └── manifest.yaml
|
||||
│ │
|
||||
│ └── legacy/ # 旧版适配器(保留一段时间后移除)
|
||||
│ ├── gewechat/
|
||||
│ ├── nakuru/
|
||||
│ └── qqbotpy/
|
||||
│
|
||||
└── handlers/ # 事件处理器实现(新增)
|
||||
├── __init__.py
|
||||
├── base.py # AbstractEventHandler 基类
|
||||
├── pipeline_handler.py # PipelineHandler
|
||||
├── agent_handler.py # AgentHandler
|
||||
├── webhook_handler.py # WebhookHandler
|
||||
└── plugin_handler.py # PluginHandler
|
||||
```
|
||||
|
||||
### 2.2 适配器目录内各文件职责
|
||||
|
||||
以 Telegram 为例:
|
||||
|
||||
| 文件 | 职责 | 关键类/函数 |
|
||||
|------|------|------------|
|
||||
| `adapter.py` | 主入口,继承 `AbstractPlatformAdapter`,组装其他模块 | `TelegramAdapter` |
|
||||
| `event_converter.py` | 将 Telegram 原生事件转换为统一事件类型 | `TelegramEventConverter` — 支持 Message/Edit/Delete/Reaction/MemberJoin 等所有事件 |
|
||||
| `message_converter.py` | `MessageChain` 与 Telegram 消息格式互转 | `TelegramMessageConverter.yiri2target()` / `target2yiri()` |
|
||||
| `api_impl.py` | 实现通用 API 方法(edit_message, delete_message, get_group_info 等) | 各 API 方法的 Telegram 实现 |
|
||||
| `platform_api.py` | 实现 `call_platform_api` 的动作分发表 | `PLATFORM_API_MAP = {"pin_message": ..., "unpin_message": ...}` |
|
||||
| `types.py` | 平台特有的类型定义 | Telegram 特有的枚举、配置结构等 |
|
||||
| `manifest.yaml` | 适配器清单:名称、配置 schema、支持的事件和 API 列表 | — |
|
||||
|
||||
## 3. 新基类设计
|
||||
|
||||
### 3.1 AbstractPlatformAdapter
|
||||
|
||||
新基类继承自现有 `AbstractMessagePlatformAdapter` 并扩展,位于 `langbot-plugin-sdk` 中:
|
||||
|
||||
```python
|
||||
# langbot_plugin/api/definition/abstract/platform/adapter.py
|
||||
|
||||
class AbstractPlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta):
|
||||
"""平台适配器基类(EBA 版本)
|
||||
|
||||
相比旧版 AbstractMessagePlatformAdapter:
|
||||
- 新增通用 API 方法(edit_message, delete_message, get_group_info 等)
|
||||
- 新增透传 API(call_platform_api)
|
||||
- 新增能力声明(get_supported_events, get_supported_apis)
|
||||
- 事件监听器支持所有事件类型,不仅限于消息事件
|
||||
"""
|
||||
|
||||
bot_account_id: str = ""
|
||||
config: dict
|
||||
logger: AbstractEventLogger = pydantic.Field(exclude=True)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
# ---- 能力声明 ----
|
||||
|
||||
def get_supported_events(self) -> list[str]:
|
||||
"""返回此适配器支持的事件类型列表
|
||||
|
||||
默认实现从 manifest.yaml 读取。
|
||||
适配器也可以 override 此方法动态声明。
|
||||
"""
|
||||
return ["message.received"]
|
||||
|
||||
def get_supported_apis(self) -> list[str]:
|
||||
"""返回此适配器支持的 API 列表
|
||||
|
||||
默认实现从 manifest.yaml 读取。
|
||||
"""
|
||||
return ["send_message", "reply_message"]
|
||||
|
||||
# ---- 必需方法(抽象) ----
|
||||
|
||||
@abc.abstractmethod
|
||||
async def send_message(self, target_type, target_id, message) -> MessageResult:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def reply_message(self, event, message, quote_origin=False) -> MessageResult:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run_async(self):
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def kill(self) -> bool:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def register_listener(self, event_type, callback):
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def unregister_listener(self, event_type, callback):
|
||||
...
|
||||
|
||||
# ---- 可选方法(默认抛 NotSupportedError) ----
|
||||
# edit_message, delete_message, forward_message,
|
||||
# get_group_info, get_group_member_list, ...
|
||||
# call_platform_api, ...
|
||||
# (完整签名见 02-platform-api.md)
|
||||
|
||||
# ---- 流式输出(保留) ----
|
||||
|
||||
async def reply_message_chunk(self, event, bot_message, message,
|
||||
quote_origin=False, is_final=False):
|
||||
raise NotSupportedError("reply_message_chunk")
|
||||
|
||||
async def is_stream_output_supported(self) -> bool:
|
||||
return False
|
||||
|
||||
# ---- 消息卡片(保留) ----
|
||||
|
||||
async def create_message_card(self, message_id, event) -> bool:
|
||||
return False
|
||||
|
||||
async def is_muted(self, group_id) -> bool:
|
||||
return False
|
||||
```
|
||||
|
||||
### 3.2 AbstractMessagePlatformAdapter 兼容
|
||||
|
||||
旧的 `AbstractMessagePlatformAdapter` 保留为 `AbstractPlatformAdapter` 的类型别名:
|
||||
|
||||
```python
|
||||
# 向后兼容
|
||||
AbstractMessagePlatformAdapter = AbstractPlatformAdapter
|
||||
```
|
||||
|
||||
现有适配器代码中的 `AbstractMessagePlatformAdapter` 引用不需要立即修改。
|
||||
|
||||
### 3.3 EventConverter 新设计
|
||||
|
||||
现有 `AbstractEventConverter` 只有 `target2yiri` 和 `yiri2target` 两个静态方法,且只处理消息事件。
|
||||
|
||||
新设计支持多种事件类型:
|
||||
|
||||
```python
|
||||
class AbstractEventConverter:
|
||||
"""事件转换器基类(EBA 版本)
|
||||
|
||||
适配器需要实现此转换器,将平台原生事件转换为统一事件。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def target2yiri(raw_event: typing.Any) -> typing.Optional[Event]:
|
||||
"""将平台原生事件转换为统一事件
|
||||
|
||||
Args:
|
||||
raw_event: 平台 SDK 回调传入的原始事件对象
|
||||
|
||||
Returns:
|
||||
统一 Event 对象,如果无法转换或不需要处理则返回 None
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def yiri2target(event: Event) -> typing.Any:
|
||||
"""将统一事件转换为平台原生事件(一般不需要)"""
|
||||
raise NotImplementedError
|
||||
```
|
||||
|
||||
具体适配器的 EventConverter 实现会是一个分发式的结构:
|
||||
|
||||
```python
|
||||
class TelegramEventConverter(AbstractEventConverter):
|
||||
"""Telegram 事件转换器"""
|
||||
|
||||
@staticmethod
|
||||
def target2yiri(update: telegram.Update) -> typing.Optional[Event]:
|
||||
# 消息事件
|
||||
if update.message:
|
||||
return TelegramEventConverter._convert_message(update)
|
||||
# 消息编辑
|
||||
if update.edited_message:
|
||||
return TelegramEventConverter._convert_edited_message(update)
|
||||
# 成员变动
|
||||
if update.chat_member:
|
||||
return TelegramEventConverter._convert_chat_member(update)
|
||||
# 回调查询(按钮点击等)
|
||||
if update.callback_query:
|
||||
return TelegramEventConverter._convert_callback_query(update)
|
||||
# 其他 → PlatformSpecificEvent
|
||||
return TelegramEventConverter._convert_platform_specific(update)
|
||||
|
||||
@staticmethod
|
||||
def _convert_message(update) -> MessageReceivedEvent:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _convert_edited_message(update) -> MessageEditedEvent:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _convert_chat_member(update) -> typing.Union[
|
||||
MemberJoinedEvent, MemberLeftEvent, ...
|
||||
]:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _convert_platform_specific(update) -> PlatformSpecificEvent:
|
||||
...
|
||||
```
|
||||
|
||||
## 4. Manifest 文件格式扩展
|
||||
|
||||
现有 manifest.yaml 只声明 `kind`, `metadata`, `spec.config`, `execution`。
|
||||
|
||||
新增 `spec.supported_events` 和 `spec.supported_apis`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: MessagePlatformAdapter
|
||||
|
||||
metadata:
|
||||
name: telegram
|
||||
label:
|
||||
en_US: Telegram
|
||||
zh_Hans: Telegram
|
||||
icon: telegram.svg
|
||||
description:
|
||||
en_US: Telegram Bot adapter
|
||||
zh_Hans: Telegram Bot 适配器
|
||||
|
||||
spec:
|
||||
config:
|
||||
# 现有配置 schema(保持不变)
|
||||
- key: token
|
||||
label: { en_US: "Bot Token", zh_Hans: "Bot Token" }
|
||||
type: string
|
||||
required: true
|
||||
sensitive: true
|
||||
# ...
|
||||
|
||||
supported_events:
|
||||
- message.received
|
||||
- message.edited
|
||||
- message.deleted
|
||||
- message.reaction
|
||||
- feedback.received
|
||||
- group.member_joined
|
||||
- group.member_left
|
||||
- group.member_banned
|
||||
- group.info_updated
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
|
||||
supported_apis:
|
||||
required:
|
||||
- send_message
|
||||
- reply_message
|
||||
optional:
|
||||
- edit_message
|
||||
- delete_message
|
||||
- get_group_info
|
||||
- get_group_member_list
|
||||
- get_group_member_info
|
||||
- get_user_info
|
||||
- upload_file
|
||||
- get_file_url
|
||||
- call_platform_api
|
||||
|
||||
platform_specific_apis:
|
||||
- action: pin_message
|
||||
description: { en_US: "Pin a message", zh_Hans: "置顶消息" }
|
||||
- action: unpin_message
|
||||
description: { en_US: "Unpin a message", zh_Hans: "取消置顶" }
|
||||
- action: get_chat_administrators
|
||||
description: { en_US: "Get chat admins", zh_Hans: "获取群管理员列表" }
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: pkg/platform/adapters/telegram/adapter.py
|
||||
attr: TelegramAdapter
|
||||
```
|
||||
|
||||
## 5. 适配器注册与发现
|
||||
|
||||
### 5.1 Blueprint 更新
|
||||
|
||||
`templates/components.yaml` 中更新扫描路径:
|
||||
|
||||
```yaml
|
||||
kind: Blueprint
|
||||
spec:
|
||||
components:
|
||||
MessagePlatformAdapter:
|
||||
fromDirs:
|
||||
- path: pkg/platform/adapters/ # 新路径
|
||||
```
|
||||
|
||||
`ComponentDiscoveryEngine` 的递归扫描逻辑不变——它会扫描所有子目录中的 `.yaml` 文件。因此每个适配器目录下的 `manifest.yaml` 会被自动发现。
|
||||
|
||||
### 5.2 PlatformManager 适配
|
||||
|
||||
`PlatformManager.initialize()` 的核心逻辑基本不变:
|
||||
|
||||
```python
|
||||
async def initialize(self):
|
||||
# 1. 发现适配器组件(自动扫描新目录结构)
|
||||
self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter')
|
||||
|
||||
# 2. 动态导入适配器类
|
||||
for component in self.adapter_components:
|
||||
self.adapter_dict[component.metadata.name] = component.get_python_component_class()
|
||||
|
||||
# 3. 从数据库加载 Bot 并实例化适配器(不变)
|
||||
await self.load_bots_from_db()
|
||||
```
|
||||
|
||||
变更点:
|
||||
- `execution.python.path` 从 `pkg/platform/sources/telegram.py` 变为 `pkg/platform/adapters/telegram/adapter.py`
|
||||
- `get_python_component_class()` 正常工作,因为它按路径动态导入
|
||||
|
||||
## 6. RuntimeBot 重构
|
||||
|
||||
### 6.1 现有问题
|
||||
|
||||
当前 `RuntimeBot.initialize()` 硬编码注册了两个回调:
|
||||
|
||||
```python
|
||||
# 现有代码
|
||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
||||
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
|
||||
```
|
||||
|
||||
### 6.2 新设计
|
||||
|
||||
`RuntimeBot` 改为注册一个通用的事件回调:
|
||||
|
||||
```python
|
||||
class RuntimeBot:
|
||||
async def initialize(self):
|
||||
# 注册通用事件回调,接收所有事件类型
|
||||
self.adapter.register_listener(Event, self._on_event)
|
||||
|
||||
async def _on_event(
|
||||
self,
|
||||
event: Event,
|
||||
adapter: AbstractPlatformAdapter,
|
||||
):
|
||||
"""统一事件入口"""
|
||||
|
||||
# 1. 设置事件的 bot_uuid 和 adapter_name
|
||||
event.bot_uuid = self.bot_entity.uuid
|
||||
event.adapter_name = self.bot_entity.adapter
|
||||
|
||||
# 2. 日志记录
|
||||
await self._log_event(event)
|
||||
|
||||
# 3. 提交给 EventBus
|
||||
await self.ap.event_bus.emit(event, adapter)
|
||||
```
|
||||
|
||||
适配器侧的 `register_listener` 实现也需调整:
|
||||
- 当 `event_type` 为 `Event`(基类)时,注册为"接收所有事件"的通配回调
|
||||
- 适配器在收到平台原生事件时,通过 `EventConverter.target2yiri()` 转换后,调用所有匹配的回调
|
||||
|
||||
## 7. 从现有单文件适配器迁移
|
||||
|
||||
### 7.1 迁移模式
|
||||
|
||||
以 Telegram 为例,从 `sources/telegram.py`(445 行)拆分:
|
||||
|
||||
| 原代码位置 | → 新文件 |
|
||||
|-----------|----------|
|
||||
| `TelegramMessageConverter` 类 | `telegram/message_converter.py` |
|
||||
| `TelegramEventConverter` 类 | `telegram/event_converter.py`(扩展,支持更多事件) |
|
||||
| `TelegramAdapter.__init__` / `run_async` / `kill` / `register_listener` | `telegram/adapter.py` |
|
||||
| `TelegramAdapter.send_message` / `reply_message` / `reply_message_chunk` | `telegram/adapter.py`(消息方法保留在主类)+ `telegram/api_impl.py`(新增 API) |
|
||||
| 新增代码 | `telegram/api_impl.py`(edit_message, delete_message, get_group_info 等) |
|
||||
| 新增代码 | `telegram/platform_api.py`(pin_message, unpin_message 等的映射) |
|
||||
| `telegram.yaml` | `telegram/manifest.yaml`(扩展 supported_events/apis) |
|
||||
|
||||
### 7.2 迁移顺序建议
|
||||
|
||||
1. **Telegram** — 功能最完整的适配器之一,适合作为模板
|
||||
2. **Discord** — 第二个迁移,验证模式的通用性
|
||||
3. **AioCQHTTP (OneBot)** — 国内最常用,确保兼容
|
||||
4. **其他适配器** — 按使用频率排序
|
||||
|
||||
### 7.3 渐进式迁移
|
||||
|
||||
不需要一次性迁移所有适配器。可以采用渐进策略:
|
||||
|
||||
1. 先在 `adapters/` 下建立新适配器
|
||||
2. `Blueprint` 同时扫描 `sources/` 和 `adapters/` 两个目录
|
||||
3. 旧适配器在 `sources/` 中继续工作
|
||||
4. 逐个迁移到新结构
|
||||
5. 全部迁移完成后移除 `sources/` 目录
|
||||
|
||||
```yaml
|
||||
# 过渡期的 Blueprint
|
||||
kind: Blueprint
|
||||
spec:
|
||||
components:
|
||||
MessagePlatformAdapter:
|
||||
fromDirs:
|
||||
- path: pkg/platform/sources/ # 旧路径(尚未迁移的适配器)
|
||||
- path: pkg/platform/adapters/ # 新路径(已迁移的适配器)
|
||||
```
|
||||
743
docs/event-based-agents/04-event-routing.md
Normal file
743
docs/event-based-agents/04-event-routing.md
Normal file
@@ -0,0 +1,743 @@
|
||||
# 事件路由与编排
|
||||
|
||||
## 1. 概述
|
||||
|
||||
事件路由是 EBA 架构的核心机制:事件从适配器产生后,经由 EventBus 进入 EventRouter,由 EventRouter 根据 Bot 的配置将事件分发到对应的处理器(Handler)。
|
||||
|
||||
**配置方式**:用户在 WebUI 的 Bot 管理页面通过可视化编排面板管理事件处理器配置,配置数据存储在数据库的 Bot 表 `event_handlers` JSON 字段中。
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
### 2.1 Bot 实体扩展
|
||||
|
||||
在 `bots` 表新增 `event_handlers` 字段:
|
||||
|
||||
```python
|
||||
class Bot(Base):
|
||||
__tablename__ = "bots"
|
||||
|
||||
uuid: str # 主键
|
||||
name: str
|
||||
description: str
|
||||
adapter: str
|
||||
adapter_config: dict # JSON
|
||||
enable: bool
|
||||
|
||||
# 新增
|
||||
event_handlers: list # JSON — 事件处理器配置列表
|
||||
|
||||
# 保留(过渡期后弃用)
|
||||
use_pipeline_name: str # deprecated
|
||||
use_pipeline_uuid: str # deprecated
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
```
|
||||
|
||||
### 2.2 EventHandler 配置结构
|
||||
|
||||
`event_handlers` 字段存储一个 JSON 数组,每个元素定义一条事件路由规则:
|
||||
|
||||
```python
|
||||
class EventHandlerConfig(pydantic.BaseModel):
|
||||
"""单条事件处理器配置"""
|
||||
|
||||
event_type: str
|
||||
"""匹配的事件类型
|
||||
|
||||
支持精确匹配和通配符:
|
||||
- "message.received" — 精确匹配
|
||||
- "message.*" — 匹配 message 命名空间下所有事件
|
||||
- "group.*" — 匹配 group 命名空间下所有事件
|
||||
- "*" — 匹配所有事件(兜底)
|
||||
"""
|
||||
|
||||
handler_type: str
|
||||
"""处理器类型: "pipeline" | "agent" | "webhook" | "plugin" """
|
||||
|
||||
handler_config: dict = {}
|
||||
"""处理器的具体配置,结构取决于 handler_type"""
|
||||
|
||||
enabled: bool = True
|
||||
"""是否启用此规则"""
|
||||
|
||||
priority: int = 0
|
||||
"""优先级,数字越大越先匹配(同一事件类型有多条规则时)"""
|
||||
|
||||
description: str = ""
|
||||
"""规则描述(供 WebUI 显示)"""
|
||||
```
|
||||
|
||||
### 2.3 各 Handler 类型的 handler_config 结构
|
||||
|
||||
#### pipeline
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "pipeline",
|
||||
"handler_config": {
|
||||
"pipeline_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将事件作为消息事件传入现有 Pipeline 流水线。仅适用于 `message.received` 事件。
|
||||
|
||||
#### agent
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "agent",
|
||||
"handler_config": {
|
||||
"runner": "local-agent",
|
||||
"runner_config": {
|
||||
"model_uuid": "...",
|
||||
"prompt": "你是一个群组助理,请处理以下事件:{event_summary}",
|
||||
"tools_enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "agent",
|
||||
"handler_config": {
|
||||
"runner": "dify-service-api",
|
||||
"runner_config": {
|
||||
"base_url": "https://api.dify.ai/v1",
|
||||
"api_key": "...",
|
||||
"app_type": "agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
直接调用 RequestRunner 处理事件。可用的 runner 包括:
|
||||
- `local-agent` — 内置 LLM Agent
|
||||
- `dify-service-api` — Dify 平台
|
||||
- `n8n-service-api` — n8n 工作流
|
||||
- `coze-api` — Coze (扣子)
|
||||
- `dashscope-app-api` — 阿里百炼
|
||||
- `langflow-api` — Langflow
|
||||
- `tbox-app-api` — 蚂蚁 Tbox
|
||||
|
||||
Agent 处理器不经过 Pipeline 的多 Stage 流程,而是直接构建上下文并调用 Runner。适用于所有事件类型。
|
||||
|
||||
**Agent Handler 与 Pipeline 的关系**:
|
||||
- Pipeline 是完整的多 Stage 处理链(PreProcessor → MessageProcessor(内含Runner) → PostProcessor → ...),适合复杂消息处理
|
||||
- Agent Handler 是轻量级的,直接调用 Runner,跳过 PreProcessor/PostProcessor 等阶段
|
||||
- Pipeline 内部的 AI Stage 仍然使用 Runner,所以 Runner 本身被两种 Handler 共享
|
||||
- 用户可以根据场景选择:消息处理用 Pipeline(更多控制),其他事件用 Agent(更直接)
|
||||
|
||||
#### webhook
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "webhook",
|
||||
"handler_config": {
|
||||
"url": "https://example.com/webhook/langbot-events",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": "Bearer xxx"
|
||||
},
|
||||
"timeout": 30,
|
||||
"retry_count": 3,
|
||||
"retry_interval": 5,
|
||||
"response_actions": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将事件序列化为 JSON POST 到外部 URL。支持的特性:
|
||||
- **认证**:通过 headers 配置(Bearer Token、API Key 等)
|
||||
- **重试**:配置重试次数和间隔
|
||||
- **响应动作**:如果 `response_actions` 为 true,解析响应 JSON 中的 `actions` 字段并执行(如发送消息、同意好友请求等)
|
||||
|
||||
Webhook 请求体格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": {
|
||||
"type": "group.member_joined",
|
||||
"timestamp": 1700000000.0,
|
||||
"bot_uuid": "...",
|
||||
"adapter_name": "telegram",
|
||||
"group": { "id": "...", "name": "..." },
|
||||
"member": { "id": "...", "nickname": "..." }
|
||||
},
|
||||
"bot": {
|
||||
"uuid": "...",
|
||||
"name": "...",
|
||||
"adapter": "telegram"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
响应体格式(当 `response_actions` 为 true 时):
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"type": "send_message",
|
||||
"params": {
|
||||
"target_type": "group",
|
||||
"target_id": "123456",
|
||||
"message": [{ "type": "Plain", "text": "欢迎新成员!" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "call_platform_api",
|
||||
"params": {
|
||||
"action": "pin_message",
|
||||
"params": { "chat_id": "123456", "message_id": "789" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### plugin
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "plugin",
|
||||
"handler_config": {
|
||||
"plugin_filter": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将事件分发给插件的 EventListener 处理。
|
||||
|
||||
- `plugin_filter`:可选的插件名过滤列表,为空表示分发给所有插件
|
||||
- 沿用现有的插件事件分发机制(按优先级遍历插件,支持 `prevent_postorder`)
|
||||
|
||||
### 2.4 完整配置示例
|
||||
|
||||
一个 Bot 的 `event_handlers` 配置示例:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"event_type": "message.received",
|
||||
"handler_type": "pipeline",
|
||||
"handler_config": {
|
||||
"pipeline_uuid": "default-pipeline-uuid"
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 10,
|
||||
"description": "消息事件使用默认流水线处理"
|
||||
},
|
||||
{
|
||||
"event_type": "group.member_joined",
|
||||
"handler_type": "agent",
|
||||
"handler_config": {
|
||||
"runner": "local-agent",
|
||||
"runner_config": {
|
||||
"model_uuid": "gpt-4o-mini",
|
||||
"prompt": "有新成员 {member_name} 加入了群组 {group_name},请生成一条欢迎消息。"
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 0,
|
||||
"description": "新成员入群时用 AI 生成欢迎消息"
|
||||
},
|
||||
{
|
||||
"event_type": "friend.request_received",
|
||||
"handler_type": "webhook",
|
||||
"handler_config": {
|
||||
"url": "https://my-server.com/api/friend-request",
|
||||
"response_actions": true
|
||||
},
|
||||
"enabled": true,
|
||||
"priority": 0,
|
||||
"description": "好友请求转发到自建服务处理"
|
||||
},
|
||||
{
|
||||
"event_type": "*",
|
||||
"handler_type": "plugin",
|
||||
"handler_config": {},
|
||||
"enabled": true,
|
||||
"priority": -100,
|
||||
"description": "所有事件兜底发给插件处理"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 3. EventBus 设计
|
||||
|
||||
EventBus 是事件的中转站,接收来自各个 RuntimeBot 的事件,交由 EventRouter 处理。
|
||||
|
||||
```python
|
||||
class EventBus:
|
||||
"""事件总线"""
|
||||
|
||||
def __init__(self, ap: Application):
|
||||
self.ap = ap
|
||||
self.event_router = EventRouter(ap)
|
||||
|
||||
async def emit(
|
||||
self,
|
||||
event: Event,
|
||||
adapter: AbstractPlatformAdapter,
|
||||
):
|
||||
"""接收并分发事件
|
||||
|
||||
Args:
|
||||
event: 统一事件对象
|
||||
adapter: 产生此事件的适配器实例
|
||||
"""
|
||||
# 1. 全局事件日志
|
||||
self.ap.logger.debug(
|
||||
f"EventBus: {event.type} from bot {event.bot_uuid}"
|
||||
)
|
||||
|
||||
# 2. 交由 EventRouter 路由处理
|
||||
await self.event_router.route(event, adapter)
|
||||
```
|
||||
|
||||
## 4. EventRouter 设计
|
||||
|
||||
EventRouter 是事件路由引擎,根据 Bot 的 `event_handlers` 配置决定事件的处理方式。
|
||||
|
||||
```python
|
||||
class EventRouter:
|
||||
"""事件路由引擎"""
|
||||
|
||||
def __init__(self, ap: Application):
|
||||
self.ap = ap
|
||||
self.handlers: dict[str, AbstractEventHandler] = {
|
||||
"pipeline": PipelineHandler(ap),
|
||||
"agent": AgentHandler(ap),
|
||||
"webhook": WebhookHandler(ap),
|
||||
"plugin": PluginHandler(ap),
|
||||
}
|
||||
|
||||
async def route(
|
||||
self,
|
||||
event: Event,
|
||||
adapter: AbstractPlatformAdapter,
|
||||
):
|
||||
"""路由事件到对应处理器"""
|
||||
|
||||
# 1. 获取 Bot 配置
|
||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(event.bot_uuid)
|
||||
if not bot:
|
||||
return
|
||||
|
||||
# 2. 获取事件处理器配置
|
||||
event_handlers = bot.bot_entity.event_handlers or []
|
||||
|
||||
# 3. 匹配规则(按 priority 降序排列)
|
||||
matched_handlers = self._match_handlers(event.type, event_handlers)
|
||||
|
||||
if not matched_handlers:
|
||||
# 未匹配到任何规则 → 默认交给插件处理(向后兼容)
|
||||
await self.handlers["plugin"].handle(event, adapter, {})
|
||||
return
|
||||
|
||||
# 4. 执行第一个匹配的 Handler
|
||||
# (未来可扩展为多个 Handler 串行/并行执行)
|
||||
handler_config = matched_handlers[0]
|
||||
handler = self.handlers.get(handler_config.handler_type)
|
||||
|
||||
if handler:
|
||||
await handler.handle(event, adapter, handler_config.handler_config)
|
||||
else:
|
||||
self.ap.logger.warning(
|
||||
f"Unknown handler type: {handler_config.handler_type}"
|
||||
)
|
||||
|
||||
def _match_handlers(
|
||||
self,
|
||||
event_type: str,
|
||||
handlers: list[EventHandlerConfig],
|
||||
) -> list[EventHandlerConfig]:
|
||||
"""匹配事件类型到处理器配置
|
||||
|
||||
匹配规则:
|
||||
1. 精确匹配:event_type == handler.event_type
|
||||
2. 命名空间通配:handler.event_type 为 "message.*" 时匹配所有 "message.xxx"
|
||||
3. 全局通配:handler.event_type 为 "*" 时匹配所有事件
|
||||
4. 按 priority 降序排列
|
||||
5. 只返回 enabled=True 的规则
|
||||
"""
|
||||
matched = []
|
||||
for handler in handlers:
|
||||
if not handler.enabled:
|
||||
continue
|
||||
if self._event_type_matches(event_type, handler.event_type):
|
||||
matched.append(handler)
|
||||
|
||||
matched.sort(key=lambda h: h.priority, reverse=True)
|
||||
return matched
|
||||
|
||||
@staticmethod
|
||||
def _event_type_matches(event_type: str, pattern: str) -> bool:
|
||||
"""判断事件类型是否匹配模式"""
|
||||
if pattern == "*":
|
||||
return True
|
||||
if pattern == event_type:
|
||||
return True
|
||||
if pattern.endswith(".*"):
|
||||
namespace = pattern[:-2]
|
||||
return event_type.startswith(namespace + ".")
|
||||
return False
|
||||
```
|
||||
|
||||
## 5. 事件处理器(Handler)实现
|
||||
|
||||
### 5.1 Handler 基类
|
||||
|
||||
```python
|
||||
class AbstractEventHandler(abc.ABC):
|
||||
"""事件处理器基类"""
|
||||
|
||||
def __init__(self, ap: Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def handle(
|
||||
self,
|
||||
event: Event,
|
||||
adapter: AbstractPlatformAdapter,
|
||||
config: dict,
|
||||
) -> None:
|
||||
"""处理事件
|
||||
|
||||
Args:
|
||||
event: 统一事件对象
|
||||
adapter: 适配器实例(用于调用平台 API 发送响应)
|
||||
config: handler_config 配置
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### 5.2 PipelineHandler
|
||||
|
||||
将消息事件注入现有 Pipeline 流水线处理。
|
||||
|
||||
```python
|
||||
class PipelineHandler(AbstractEventHandler):
|
||||
"""Pipeline 处理器 — 将事件送入现有 Pipeline 流水线"""
|
||||
|
||||
async def handle(self, event, adapter, config):
|
||||
pipeline_uuid = config.get("pipeline_uuid")
|
||||
|
||||
if not isinstance(event, MessageReceivedEvent):
|
||||
self.ap.logger.warning(
|
||||
f"PipelineHandler only supports MessageReceivedEvent, "
|
||||
f"got {event.type}"
|
||||
)
|
||||
return
|
||||
|
||||
# 将 MessageReceivedEvent 转换为现有的 Query 并投入 QueryPool
|
||||
# 复用现有的 MessageAggregator + QueryPool + Pipeline 机制
|
||||
launcher_type = (
|
||||
LauncherTypes.PERSON
|
||||
if event.chat_type == ChatType.PRIVATE
|
||||
else LauncherTypes.GROUP
|
||||
)
|
||||
|
||||
await self.ap.msg_aggregator.add_message(
|
||||
bot_uuid=event.bot_uuid,
|
||||
launcher_type=launcher_type,
|
||||
launcher_id=event.chat_id,
|
||||
sender_id=event.sender.id,
|
||||
message_event=event.to_legacy_event(), # 转换为 FriendMessage/GroupMessage
|
||||
message_chain=event.message_chain,
|
||||
adapter=adapter,
|
||||
pipeline_uuid=pipeline_uuid,
|
||||
)
|
||||
```
|
||||
|
||||
### 5.3 AgentHandler
|
||||
|
||||
直接调用 RequestRunner 处理事件,不经过 Pipeline Stage 链。
|
||||
|
||||
```python
|
||||
class AgentHandler(AbstractEventHandler):
|
||||
"""Agent 处理器 — 直接调用 RequestRunner 处理事件"""
|
||||
|
||||
async def handle(self, event, adapter, config):
|
||||
runner_name = config.get("runner", "local-agent")
|
||||
runner_config = config.get("runner_config", {})
|
||||
|
||||
# 1. 查找 Runner 类
|
||||
runner_cls = None
|
||||
for r in preregistered_runners:
|
||||
if r.name == runner_name:
|
||||
runner_cls = r
|
||||
break
|
||||
|
||||
if not runner_cls:
|
||||
self.ap.logger.error(f"Runner not found: {runner_name}")
|
||||
return
|
||||
|
||||
# 2. 构建事件上下文(将事件信息整理为 Runner 可处理的格式)
|
||||
event_context = self._build_event_context(event, runner_config)
|
||||
|
||||
# 3. 实例化并调用 Runner
|
||||
runner = runner_cls(self.ap, self._build_runner_pipeline_config(config))
|
||||
|
||||
response_messages = []
|
||||
async for result in runner.run(event_context):
|
||||
response_messages.append(result)
|
||||
|
||||
# 4. 发送响应(如果 Runner 产生了回复)
|
||||
if response_messages and isinstance(event, MessageReceivedEvent):
|
||||
# 将 Runner 输出转换为 MessageChain 并回复
|
||||
reply_chain = self._build_reply_chain(response_messages)
|
||||
await adapter.reply_message(event, reply_chain)
|
||||
|
||||
def _build_event_context(self, event, runner_config):
|
||||
"""将事件构建为 Runner 可处理的上下文
|
||||
|
||||
对于消息事件,直接使用消息内容。
|
||||
对于其他事件,根据 runner_config 中的 prompt 模板生成描述文本。
|
||||
"""
|
||||
...
|
||||
|
||||
def _build_runner_pipeline_config(self, config):
|
||||
"""将 handler_config 转换为 Runner 需要的 pipeline_config 格式"""
|
||||
...
|
||||
```
|
||||
|
||||
### 5.4 WebhookHandler
|
||||
|
||||
将事件 POST 到外部 URL。
|
||||
|
||||
```python
|
||||
class WebhookHandler(AbstractEventHandler):
|
||||
"""Webhook 处理器 — 将事件 POST 到外部 URL"""
|
||||
|
||||
async def handle(self, event, adapter, config):
|
||||
url = config.get("url")
|
||||
method = config.get("method", "POST")
|
||||
headers = config.get("headers", {})
|
||||
timeout = config.get("timeout", 30)
|
||||
retry_count = config.get("retry_count", 3)
|
||||
response_actions = config.get("response_actions", False)
|
||||
|
||||
# 1. 构建请求体
|
||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(event.bot_uuid)
|
||||
payload = {
|
||||
"event": event.model_dump(),
|
||||
"bot": {
|
||||
"uuid": bot.bot_entity.uuid,
|
||||
"name": bot.bot_entity.name,
|
||||
"adapter": bot.bot_entity.adapter,
|
||||
}
|
||||
}
|
||||
|
||||
# 2. 发送请求(带重试)
|
||||
response = await self._send_with_retry(
|
||||
url, method, headers, payload, timeout, retry_count
|
||||
)
|
||||
|
||||
# 3. 处理响应动作
|
||||
if response_actions and response:
|
||||
await self._execute_response_actions(
|
||||
response, adapter, event
|
||||
)
|
||||
|
||||
async def _execute_response_actions(self, response, adapter, event):
|
||||
"""执行响应中的动作列表"""
|
||||
actions = response.get("actions", [])
|
||||
for action in actions:
|
||||
action_type = action.get("type")
|
||||
params = action.get("params", {})
|
||||
|
||||
if action_type == "send_message":
|
||||
chain = MessageChain.model_validate(params.get("message", []))
|
||||
await adapter.send_message(
|
||||
params["target_type"],
|
||||
params["target_id"],
|
||||
chain,
|
||||
)
|
||||
elif action_type == "reply":
|
||||
chain = MessageChain.model_validate(params.get("message", []))
|
||||
await adapter.reply_message(event, chain)
|
||||
elif action_type == "call_platform_api":
|
||||
await adapter.call_platform_api(
|
||||
params["action"],
|
||||
params.get("params", {}),
|
||||
)
|
||||
elif action_type == "approve_friend_request":
|
||||
await adapter.approve_friend_request(
|
||||
params["request_id"],
|
||||
params.get("approve", True),
|
||||
)
|
||||
# ... 更多动作类型
|
||||
```
|
||||
|
||||
### 5.5 PluginHandler
|
||||
|
||||
将事件分发给插件的 EventListener。
|
||||
|
||||
```python
|
||||
class PluginHandler(AbstractEventHandler):
|
||||
"""Plugin 处理器 — 分发给插件 EventListener"""
|
||||
|
||||
async def handle(self, event, adapter, config):
|
||||
plugin_filter = config.get("plugin_filter", [])
|
||||
|
||||
# 复用现有的插件事件分发机制
|
||||
# 通过 plugin_connector 将事件发送给 Plugin Runtime
|
||||
await self.ap.plugin_connector.emit_event(
|
||||
event=event,
|
||||
adapter=adapter,
|
||||
plugin_filter=plugin_filter,
|
||||
)
|
||||
```
|
||||
|
||||
## 6. use_pipeline_uuid 迁移
|
||||
|
||||
### 6.1 自动迁移
|
||||
|
||||
数据库迁移脚本将现有的 `use_pipeline_uuid` 自动转换为 `event_handlers`:
|
||||
|
||||
```python
|
||||
# 迁移逻辑
|
||||
for bot in all_bots:
|
||||
if bot.use_pipeline_uuid and not bot.event_handlers:
|
||||
bot.event_handlers = [
|
||||
{
|
||||
"event_type": "message.received",
|
||||
"handler_type": "pipeline",
|
||||
"handler_config": {
|
||||
"pipeline_uuid": bot.use_pipeline_uuid
|
||||
},
|
||||
"enabled": True,
|
||||
"priority": 10,
|
||||
"description": "Auto-migrated from use_pipeline_uuid"
|
||||
},
|
||||
{
|
||||
"event_type": "*",
|
||||
"handler_type": "plugin",
|
||||
"handler_config": {},
|
||||
"enabled": True,
|
||||
"priority": -100,
|
||||
"description": "Default plugin handler"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 6.2 过渡期兼容
|
||||
|
||||
在过渡期内,如果 `event_handlers` 为空且 `use_pipeline_uuid` 非空,EventRouter 自动回退到旧行为:
|
||||
|
||||
```python
|
||||
# EventRouter.route() 中的兼容逻辑
|
||||
if not event_handlers and bot.bot_entity.use_pipeline_uuid:
|
||||
# 回退:消息事件走 Pipeline,其他事件走 Plugin
|
||||
if isinstance(event, MessageReceivedEvent):
|
||||
await self.handlers["pipeline"].handle(
|
||||
event, adapter,
|
||||
{"pipeline_uuid": bot.bot_entity.use_pipeline_uuid}
|
||||
)
|
||||
else:
|
||||
await self.handlers["plugin"].handle(event, adapter, {})
|
||||
return
|
||||
```
|
||||
|
||||
## 7. WebUI 编排面板数据模型
|
||||
|
||||
### 7.1 交互设计概要
|
||||
|
||||
在 WebUI 的 Bot 管理页面,新增"事件处理器"标签页(或区域),呈现为一个**规则列表**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 事件处理器 [+ 添加规则] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─ 规则 1 ─────────────────────────────────── [启用] [删除] ─┐ │
|
||||
│ │ 事件类型: [message.received ▾] │ │
|
||||
│ │ 处理器: [Pipeline ▾] │ │
|
||||
│ │ Pipeline: [默认流水线 ▾] │ │
|
||||
│ │ 优先级: [10] │ │
|
||||
│ │ 描述: 消息事件使用默认流水线处理 │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 规则 2 ─────────────────────────────────── [启用] [删除] ─┐ │
|
||||
│ │ 事件类型: [group.member_joined ▾] │ │
|
||||
│ │ 处理器: [Agent ▾] │ │
|
||||
│ │ Runner: [local-agent ▾] │ │
|
||||
│ │ 模型: [gpt-4o-mini ▾] │ │
|
||||
│ │ Prompt: [有新成员加入...] │ │
|
||||
│ │ 优先级: [0] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ 规则 3 (兜底) ──────────────────────────── [启用] [删除] ─┐ │
|
||||
│ │ 事件类型: [* ▾] │ │
|
||||
│ │ 处理器: [Plugin ▾] │ │
|
||||
│ │ 优先级: [-100] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 前端数据结构
|
||||
|
||||
```typescript
|
||||
interface EventHandlerRule {
|
||||
event_type: string; // 下拉选择,选项从适配器 manifest 的 supported_events 获取
|
||||
handler_type: string; // "pipeline" | "agent" | "webhook" | "plugin"
|
||||
handler_config: Record<string, any>; // 根据 handler_type 动态渲染不同的配置表单
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Bot 编辑接口扩展
|
||||
interface BotConfig {
|
||||
uuid: string;
|
||||
name: string;
|
||||
adapter: string;
|
||||
adapter_config: Record<string, any>;
|
||||
enable: boolean;
|
||||
event_handlers: EventHandlerRule[]; // 新增
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 事件类型下拉选项
|
||||
|
||||
从 Bot 关联的适配器 manifest 中获取 `supported_events`,加上通配符选项:
|
||||
|
||||
```
|
||||
- message.received
|
||||
- message.edited
|
||||
- message.deleted
|
||||
- message.reaction
|
||||
- feedback.received
|
||||
- group.member_joined
|
||||
- group.member_left
|
||||
- group.member_banned
|
||||
- group.info_updated
|
||||
- friend.request_received
|
||||
- friend.added
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
─────────────────
|
||||
- message.* (所有消息事件)
|
||||
- feedback.* (所有反馈事件)
|
||||
- group.* (所有群组事件)
|
||||
- friend.* (所有好友事件)
|
||||
- bot.* (所有 Bot 事件)
|
||||
- * (所有事件)
|
||||
```
|
||||
|
||||
### 7.4 HTTP API
|
||||
|
||||
```
|
||||
GET /api/v1/bots/{uuid}/event-handlers 获取 Bot 的事件处理器配置
|
||||
PUT /api/v1/bots/{uuid}/event-handlers 更新 Bot 的事件处理器配置
|
||||
GET /api/v1/adapters/{name}/supported-events 获取适配器支持的事件类型
|
||||
GET /api/v1/adapters/{name}/supported-apis 获取适配器支持的 API
|
||||
```
|
||||
738
docs/event-based-agents/05-plugin-sdk.md
Normal file
738
docs/event-based-agents/05-plugin-sdk.md
Normal file
@@ -0,0 +1,738 @@
|
||||
# 插件 SDK 改造
|
||||
|
||||
## 1. 概述
|
||||
|
||||
插件 SDK 需要配合 EBA 架构进行以下改造:
|
||||
|
||||
1. **新事件类型**:将所有通用事件暴露给插件
|
||||
2. **新 API**:将新增的平台 API 通过 `LangBotAPIProxy` 暴露给插件
|
||||
3. **兼容层**:保证现有插件零修改运行
|
||||
4. **通信协议扩展**:新增 action 枚举支持新 API
|
||||
|
||||
## 2. 新事件类型暴露
|
||||
|
||||
### 2.1 插件事件模型扩展
|
||||
|
||||
当前插件 SDK 的事件模型(`api/entities/events.py`)只有消息相关事件。需要新增所有通用事件的插件级包装:
|
||||
|
||||
```python
|
||||
# api/entities/events.py — 新增事件
|
||||
|
||||
# ---- 消息事件(扩展) ----
|
||||
|
||||
class MessageEditedReceived(BaseEventModel):
|
||||
"""消息被编辑事件"""
|
||||
launcher_type: str
|
||||
launcher_id: typing.Union[int, str]
|
||||
message_id: typing.Union[int, str]
|
||||
editor_id: typing.Union[int, str]
|
||||
new_content: MessageChain
|
||||
chat_type: str # "private" | "group"
|
||||
|
||||
class MessageDeletedReceived(BaseEventModel):
|
||||
"""消息被删除/撤回事件"""
|
||||
launcher_type: str
|
||||
launcher_id: typing.Union[int, str]
|
||||
message_id: typing.Union[int, str]
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
chat_type: str
|
||||
|
||||
class MessageReactionReceived(BaseEventModel):
|
||||
"""消息表情回应事件"""
|
||||
launcher_type: str
|
||||
launcher_id: typing.Union[int, str]
|
||||
message_id: typing.Union[int, str]
|
||||
user_id: typing.Union[int, str]
|
||||
reaction: str
|
||||
is_add: bool
|
||||
|
||||
# ---- 用户反馈事件 ----
|
||||
|
||||
class FeedbackReceived(BaseEventModel):
|
||||
"""用户对 Bot 回复提交反馈"""
|
||||
feedback_id: str
|
||||
feedback_type: int # 1=like, 2=dislike, 3=cancel/remove feedback
|
||||
feedback_content: typing.Optional[str] = None
|
||||
inaccurate_reasons: typing.Optional[list[str]] = None
|
||||
user_id: typing.Optional[str] = None
|
||||
session_id: typing.Optional[str] = None
|
||||
message_id: typing.Optional[str] = None
|
||||
stream_id: typing.Optional[str] = None
|
||||
|
||||
# ---- 群组事件 ----
|
||||
|
||||
class GroupMemberJoined(BaseEventModel):
|
||||
"""新成员加入群组"""
|
||||
group_id: typing.Union[int, str]
|
||||
group_name: str
|
||||
member_id: typing.Union[int, str]
|
||||
member_name: str
|
||||
inviter_id: typing.Optional[typing.Union[int, str]] = None
|
||||
join_type: typing.Optional[str] = None
|
||||
|
||||
class GroupMemberLeft(BaseEventModel):
|
||||
"""成员离开群组"""
|
||||
group_id: typing.Union[int, str]
|
||||
group_name: str
|
||||
member_id: typing.Union[int, str]
|
||||
member_name: str
|
||||
is_kicked: bool = False
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
|
||||
class GroupMemberBanned(BaseEventModel):
|
||||
"""成员被禁言"""
|
||||
group_id: typing.Union[int, str]
|
||||
member_id: typing.Union[int, str]
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
duration: typing.Optional[int] = None
|
||||
|
||||
class GroupMemberUnbanned(BaseEventModel):
|
||||
"""成员被解除禁言"""
|
||||
group_id: typing.Union[int, str]
|
||||
member_id: typing.Union[int, str]
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
|
||||
class GroupInfoUpdated(BaseEventModel):
|
||||
"""群组信息被修改"""
|
||||
group_id: typing.Union[int, str]
|
||||
group_name: str
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
changed_fields: list[str] = []
|
||||
|
||||
# ---- 好友事件 ----
|
||||
|
||||
class FriendRequestReceived(BaseEventModel):
|
||||
"""收到好友请求"""
|
||||
request_id: typing.Union[int, str]
|
||||
user_id: typing.Union[int, str]
|
||||
user_name: str
|
||||
message: typing.Optional[str] = None
|
||||
|
||||
class FriendAdded(BaseEventModel):
|
||||
"""成功添加好友"""
|
||||
user_id: typing.Union[int, str]
|
||||
user_name: str
|
||||
|
||||
class FriendRemoved(BaseEventModel):
|
||||
"""好友被移除"""
|
||||
user_id: typing.Union[int, str]
|
||||
user_name: str
|
||||
|
||||
# ---- Bot 状态事件 ----
|
||||
|
||||
class BotInvitedToGroup(BaseEventModel):
|
||||
"""Bot 被邀请加入群组"""
|
||||
group_id: typing.Union[int, str]
|
||||
group_name: str
|
||||
inviter_id: typing.Optional[typing.Union[int, str]] = None
|
||||
request_id: typing.Optional[typing.Union[int, str]] = None
|
||||
|
||||
class BotRemovedFromGroup(BaseEventModel):
|
||||
"""Bot 被移出群组"""
|
||||
group_id: typing.Union[int, str]
|
||||
group_name: str
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
|
||||
class BotMuted(BaseEventModel):
|
||||
"""Bot 被禁言"""
|
||||
group_id: typing.Union[int, str]
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
duration: typing.Optional[int] = None
|
||||
|
||||
class BotUnmuted(BaseEventModel):
|
||||
"""Bot 被解除禁言"""
|
||||
group_id: typing.Union[int, str]
|
||||
operator_id: typing.Optional[typing.Union[int, str]] = None
|
||||
|
||||
# ---- 平台特有事件 ----
|
||||
|
||||
class PlatformSpecificEventReceived(BaseEventModel):
|
||||
"""平台特有事件"""
|
||||
adapter_name: str
|
||||
action: str
|
||||
data: dict = {}
|
||||
```
|
||||
|
||||
### 2.2 EventListener 注册方式
|
||||
|
||||
插件的 EventListener 继续使用 `@self.handler(EventType)` 装饰器注册,只是可以注册的事件类型大幅增加:
|
||||
|
||||
```python
|
||||
class MyEventListener(EventListener):
|
||||
def __init__(self, host):
|
||||
super().__init__(host)
|
||||
|
||||
# 现有方式(继续工作)
|
||||
@self.handler(PersonNormalMessageReceived)
|
||||
async def on_person_message(ctx: EventContext):
|
||||
...
|
||||
|
||||
# 新事件类型
|
||||
@self.handler(GroupMemberJoined)
|
||||
async def on_member_joined(ctx: EventContext):
|
||||
group_name = ctx.event.group_name
|
||||
member_name = ctx.event.member_name
|
||||
await ctx.reply(MessageChain([
|
||||
Plain(f"欢迎 {member_name} 加入 {group_name}!")
|
||||
]))
|
||||
|
||||
@self.handler(FriendRequestReceived)
|
||||
async def on_friend_request(ctx: EventContext):
|
||||
# 自动通过好友请求
|
||||
await ctx.approve_friend_request(
|
||||
ctx.event.request_id, approve=True
|
||||
)
|
||||
|
||||
@self.handler(FeedbackReceived)
|
||||
async def on_feedback(ctx: EventContext):
|
||||
if ctx.event.feedback_type == 2:
|
||||
await self.log_warning(
|
||||
f"用户点踩了回复: {ctx.event.feedback_content or ''}"
|
||||
)
|
||||
|
||||
@self.handler(PlatformSpecificEventReceived)
|
||||
async def on_platform_event(ctx: EventContext):
|
||||
if ctx.event.adapter_name == "telegram" and ctx.event.action == "chat_join_request":
|
||||
...
|
||||
```
|
||||
|
||||
## 3. 新 API 暴露
|
||||
|
||||
### 3.1 LangBotAPIProxy 扩展
|
||||
|
||||
在 `LangBotAPIProxy` 中新增以下方法,插件通过 `self.xxx()` 调用(在 BasePlugin 中继承):
|
||||
|
||||
```python
|
||||
class LangBotAPIProxy:
|
||||
# ---- 现有方法(保留) ----
|
||||
# get_langbot_version, get_bots, get_bot_info,
|
||||
# send_message, invoke_llm, get/set/delete_plugin_storage, ...
|
||||
|
||||
# ---- 新增消息 API ----
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
new_content: MessageChain,
|
||||
) -> None:
|
||||
"""编辑已发送的消息"""
|
||||
...
|
||||
|
||||
async def delete_message(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""删除/撤回消息"""
|
||||
...
|
||||
|
||||
async def forward_message(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
from_chat_type: str,
|
||||
from_chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
to_chat_type: str,
|
||||
to_chat_id: typing.Union[int, str],
|
||||
) -> dict:
|
||||
"""转发消息"""
|
||||
...
|
||||
|
||||
async def get_message(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
chat_type: str,
|
||||
chat_id: typing.Union[int, str],
|
||||
message_id: typing.Union[int, str],
|
||||
) -> dict:
|
||||
"""获取指定消息"""
|
||||
...
|
||||
|
||||
# ---- 新增群组 API ----
|
||||
|
||||
async def get_group_info(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> dict:
|
||||
"""获取群组信息"""
|
||||
...
|
||||
|
||||
async def get_group_list(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
) -> list[dict]:
|
||||
"""获取 Bot 加入的群组列表"""
|
||||
...
|
||||
|
||||
async def get_group_member_list(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
) -> list[dict]:
|
||||
"""获取群成员列表"""
|
||||
...
|
||||
|
||||
async def get_group_member_info(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> dict:
|
||||
"""获取指定群成员信息"""
|
||||
...
|
||||
|
||||
async def mute_member(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
duration: int = 0,
|
||||
) -> None:
|
||||
"""禁言群成员"""
|
||||
...
|
||||
|
||||
async def unmute_member(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""解除禁言"""
|
||||
...
|
||||
|
||||
async def kick_member(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
group_id: typing.Union[int, str],
|
||||
user_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""踢出群成员"""
|
||||
...
|
||||
|
||||
# ---- 新增用户 API ----
|
||||
|
||||
async def get_user_info(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
user_id: typing.Union[int, str],
|
||||
) -> dict:
|
||||
"""获取用户信息"""
|
||||
...
|
||||
|
||||
async def get_friend_list(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
) -> list[dict]:
|
||||
"""获取好友列表"""
|
||||
...
|
||||
|
||||
async def approve_friend_request(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
remark: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
"""处理好友请求"""
|
||||
...
|
||||
|
||||
async def approve_group_invite(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
) -> None:
|
||||
"""处理入群邀请"""
|
||||
...
|
||||
|
||||
# ---- 新增透传 API ----
|
||||
|
||||
async def call_platform_api(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
action: str,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""调用适配器特有 API
|
||||
|
||||
Examples:
|
||||
# Telegram: pin 消息
|
||||
result = await self.call_platform_api(
|
||||
bot_uuid, "pin_message",
|
||||
{"chat_id": 123456, "message_id": 789}
|
||||
)
|
||||
|
||||
# Discord: 创建频道
|
||||
result = await self.call_platform_api(
|
||||
bot_uuid, "create_channel",
|
||||
{"guild_id": "...", "name": "new-channel"}
|
||||
)
|
||||
"""
|
||||
...
|
||||
|
||||
# ---- 新增能力查询 API ----
|
||||
|
||||
async def get_supported_events(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
) -> list[str]:
|
||||
"""获取指定 Bot 的适配器支持的事件类型"""
|
||||
...
|
||||
|
||||
async def get_supported_apis(
|
||||
self,
|
||||
bot_uuid: str,
|
||||
) -> list[str]:
|
||||
"""获取指定 Bot 的适配器支持的 API"""
|
||||
...
|
||||
```
|
||||
|
||||
### 3.2 QueryBasedAPIProxy 扩展
|
||||
|
||||
在事件处理上下文中(EventContext),通过 `QueryBasedAPIProxy` 新增便捷方法:
|
||||
|
||||
```python
|
||||
class QueryBasedAPIProxy:
|
||||
# ---- 现有方法(保留) ----
|
||||
# reply, get_bot_uuid, set_query_var, get_query_var,
|
||||
# create_new_conversation, ...
|
||||
|
||||
# ---- 新增便捷方法 ----
|
||||
|
||||
async def edit_message(
|
||||
self,
|
||||
message_id: typing.Union[int, str],
|
||||
new_content: MessageChain,
|
||||
) -> None:
|
||||
"""在当前会话中编辑消息(自动使用当前 bot_uuid 和 chat 信息)"""
|
||||
...
|
||||
|
||||
async def delete_message(
|
||||
self,
|
||||
message_id: typing.Union[int, str],
|
||||
) -> None:
|
||||
"""在当前会话中删除消息"""
|
||||
...
|
||||
|
||||
async def approve_friend_request(
|
||||
self,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
remark: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
"""处理好友请求(上下文中自动获取 bot_uuid)"""
|
||||
...
|
||||
|
||||
async def approve_group_invite(
|
||||
self,
|
||||
request_id: typing.Union[int, str],
|
||||
approve: bool = True,
|
||||
) -> None:
|
||||
"""处理入群邀请"""
|
||||
...
|
||||
|
||||
async def get_group_info(self) -> dict:
|
||||
"""获取当前群组信息(仅群聊事件中可用)"""
|
||||
...
|
||||
|
||||
async def get_group_member_list(self) -> list[dict]:
|
||||
"""获取当前群组成员列表(仅群聊事件中可用)"""
|
||||
...
|
||||
|
||||
async def call_platform_api(
|
||||
self,
|
||||
action: str,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""调用平台特有 API(自动使用当前 bot_uuid)"""
|
||||
...
|
||||
```
|
||||
|
||||
## 4. 兼容层设计
|
||||
|
||||
### 4.1 事件兼容层
|
||||
|
||||
当 PluginHandler 将新的 `MessageReceivedEvent` 分发给插件时,需要同时生成旧格式事件:
|
||||
|
||||
```python
|
||||
class PluginEventCompatLayer:
|
||||
"""插件事件兼容层
|
||||
|
||||
将新的统一事件转换为旧的插件事件格式,
|
||||
确保监听旧事件类型的插件仍能正常工作。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def convert_to_legacy_events(
|
||||
event: Event,
|
||||
) -> list[BaseEventModel]:
|
||||
"""将统一事件转换为旧插件事件列表
|
||||
|
||||
一个统一事件可能生成多个旧插件事件。
|
||||
例如 MessageReceivedEvent 会同时生成:
|
||||
- PersonMessageReceived / GroupMessageReceived(总是生成)
|
||||
- PersonNormalMessageReceived / GroupNormalMessageReceived(非命令时)
|
||||
- PersonCommandSent / GroupCommandSent(命令时)
|
||||
"""
|
||||
legacy_events = []
|
||||
|
||||
if isinstance(event, MessageReceivedEvent):
|
||||
if event.chat_type == ChatType.PRIVATE:
|
||||
legacy_events.append(
|
||||
PersonMessageReceived(
|
||||
launcher_type="person",
|
||||
launcher_id=event.chat_id,
|
||||
sender_id=event.sender.id,
|
||||
message_event=event.to_legacy_friend_message(),
|
||||
message_chain=event.message_chain,
|
||||
)
|
||||
)
|
||||
# 命令检测后还会生成 PersonNormalMessageReceived
|
||||
# 或 PersonCommandSent,在 Pipeline 阶段处理
|
||||
elif event.chat_type == ChatType.GROUP:
|
||||
legacy_events.append(
|
||||
GroupMessageReceived(
|
||||
launcher_type="group",
|
||||
launcher_id=event.chat_id,
|
||||
sender_id=event.sender.id,
|
||||
message_event=event.to_legacy_group_message(),
|
||||
message_chain=event.message_chain,
|
||||
)
|
||||
)
|
||||
|
||||
# 新事件类型没有旧的对应物,不生成兼容事件
|
||||
# 只有监听了新事件类型的插件才会收到
|
||||
|
||||
return legacy_events
|
||||
```
|
||||
|
||||
### 4.2 分发流程
|
||||
|
||||
```
|
||||
统一事件 (MessageReceivedEvent)
|
||||
│
|
||||
├─→ 转换为旧格式 (PersonMessageReceived / GroupMessageReceived)
|
||||
│ └─→ 分发给监听旧事件类型的插件 EventListener
|
||||
│
|
||||
└─→ 直接分发为新格式 (MessageReceivedEvent → 对应的插件事件)
|
||||
└─→ 分发给监听新事件类型的插件 EventListener
|
||||
```
|
||||
|
||||
插件 Runtime 在分发事件时检查每个 EventListener 注册的事件类型:
|
||||
- 如果注册的是旧类型(`PersonMessageReceived` 等),发送兼容层生成的旧格式事件
|
||||
- 如果注册的是新类型(`GroupMemberJoined` 等),发送新格式事件
|
||||
- 两者可以共存,同一个插件可以同时监听新旧类型
|
||||
|
||||
### 4.3 API 兼容层
|
||||
|
||||
现有插件使用的 API 不受影响:
|
||||
|
||||
| 现有 API | 新架构行为 |
|
||||
|---------|----------|
|
||||
| `self.send_message(bot_uuid, target_type, target_id, message_chain)` | 不变,直接调用适配器的 `send_message` |
|
||||
| `ctx.reply(message_chain, quote_origin)` | 不变,在 MessageReceivedEvent 上下文中调用适配器的 `reply_message` |
|
||||
| `self.get_bots()` | 不变 |
|
||||
| `self.get_bot_info(bot_uuid)` | 不变 |
|
||||
|
||||
新 API 只是额外新增的方法,不影响现有方法。
|
||||
|
||||
## 5. 通信协议扩展
|
||||
|
||||
### 5.1 新增 Action 枚举
|
||||
|
||||
在 `entities/io/actions/enums.py` 中新增 action:
|
||||
|
||||
```python
|
||||
class PluginToRuntimeAction(str, Enum):
|
||||
# ---- 现有 actions(保留) ----
|
||||
REGISTER_PLUGIN = "register_plugin"
|
||||
REPLY = "reply"
|
||||
SEND_MESSAGE = "send_message"
|
||||
# ...
|
||||
|
||||
# ---- 新增消息 API ----
|
||||
EDIT_MESSAGE = "edit_message"
|
||||
DELETE_MESSAGE = "delete_message"
|
||||
FORWARD_MESSAGE = "forward_message"
|
||||
GET_MESSAGE = "get_message"
|
||||
|
||||
# ---- 新增群组 API ----
|
||||
GET_GROUP_INFO = "get_group_info"
|
||||
GET_GROUP_LIST = "get_group_list"
|
||||
GET_GROUP_MEMBER_LIST = "get_group_member_list"
|
||||
GET_GROUP_MEMBER_INFO = "get_group_member_info"
|
||||
MUTE_MEMBER = "mute_member"
|
||||
UNMUTE_MEMBER = "unmute_member"
|
||||
KICK_MEMBER = "kick_member"
|
||||
|
||||
# ---- 新增用户 API ----
|
||||
GET_USER_INFO = "get_user_info"
|
||||
GET_FRIEND_LIST = "get_friend_list"
|
||||
APPROVE_FRIEND_REQUEST = "approve_friend_request"
|
||||
APPROVE_GROUP_INVITE = "approve_group_invite"
|
||||
|
||||
# ---- 新增透传 API ----
|
||||
CALL_PLATFORM_API = "call_platform_api"
|
||||
|
||||
# ---- 新增能力查询 ----
|
||||
GET_SUPPORTED_EVENTS = "get_supported_events"
|
||||
GET_SUPPORTED_APIS = "get_supported_apis"
|
||||
|
||||
|
||||
class RuntimeToPluginAction(str, Enum):
|
||||
# ---- 现有 actions(保留) ----
|
||||
EMIT_EVENT = "emit_event"
|
||||
# ...
|
||||
# EMIT_EVENT 的 data 结构扩展以支持新事件类型
|
||||
```
|
||||
|
||||
### 5.2 新增 Action 的请求/响应格式
|
||||
|
||||
以 `EDIT_MESSAGE` 为例:
|
||||
|
||||
```json
|
||||
// 请求 (Plugin → Runtime)
|
||||
{
|
||||
"action": "edit_message",
|
||||
"seq_id": 12345,
|
||||
"data": {
|
||||
"bot_uuid": "...",
|
||||
"chat_type": "group",
|
||||
"chat_id": "123456",
|
||||
"message_id": "789",
|
||||
"new_content": [
|
||||
{ "type": "Plain", "text": "edited message" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 响应 (Runtime → Plugin)
|
||||
{
|
||||
"seq_id": 12345,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
以 `GET_GROUP_MEMBER_LIST` 为例:
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"action": "get_group_member_list",
|
||||
"seq_id": 12346,
|
||||
"data": {
|
||||
"bot_uuid": "...",
|
||||
"group_id": "123456"
|
||||
}
|
||||
}
|
||||
|
||||
// 响应
|
||||
{
|
||||
"seq_id": 12346,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"members": [
|
||||
{
|
||||
"user": { "id": "111", "nickname": "Alice" },
|
||||
"group_id": "123456",
|
||||
"role": "admin",
|
||||
"display_name": "管理员Alice"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以 `CALL_PLATFORM_API` 为例:
|
||||
|
||||
```json
|
||||
// 请求
|
||||
{
|
||||
"action": "call_platform_api",
|
||||
"seq_id": 12347,
|
||||
"data": {
|
||||
"bot_uuid": "...",
|
||||
"action": "pin_message",
|
||||
"params": {
|
||||
"chat_id": "123456",
|
||||
"message_id": "789"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应
|
||||
{
|
||||
"seq_id": 12347,
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"result": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 LangBot 侧 Handler 实现
|
||||
|
||||
在 `ControlConnectionHandler`(LangBot → Runtime 侧)和 `PluginConnectionHandler`(Runtime → Plugin 侧)中新增对应的 action 处理逻辑:
|
||||
|
||||
```python
|
||||
# PluginConnectionHandler 中新增
|
||||
async def _handle_edit_message(self, data):
|
||||
bot_uuid = data["bot_uuid"]
|
||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||
await bot.adapter.edit_message(
|
||||
chat_type=data["chat_type"],
|
||||
chat_id=data["chat_id"],
|
||||
message_id=data["message_id"],
|
||||
new_content=MessageChain.model_validate(data["new_content"]),
|
||||
)
|
||||
return {}
|
||||
|
||||
async def _handle_call_platform_api(self, data):
|
||||
bot_uuid = data["bot_uuid"]
|
||||
bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
||||
result = await bot.adapter.call_platform_api(
|
||||
action=data["action"],
|
||||
params=data.get("params", {}),
|
||||
)
|
||||
return {"result": result}
|
||||
```
|
||||
|
||||
## 6. 插件开发者迁移指南
|
||||
|
||||
### 6.1 无需迁移(零修改运行)
|
||||
|
||||
以下场景的现有插件**不需要任何修改**:
|
||||
|
||||
- 使用 `PersonNormalMessageReceived` / `GroupNormalMessageReceived` 监听消息
|
||||
- 使用 `PersonCommandSent` / `GroupCommandSent` 处理命令
|
||||
- 使用 `ctx.reply()` 回复消息
|
||||
- 使用 `self.send_message()` 主动发消息
|
||||
- 使用 LLM / 存储 / RAG 等现有 API
|
||||
|
||||
### 6.2 推荐迁移(获得新能力)
|
||||
|
||||
如果插件希望利用新功能,可以:
|
||||
|
||||
1. **监听新事件类型**:在 EventListener 中注册新事件类型的 handler
|
||||
2. **使用新 API**:调用 `self.edit_message()`, `self.get_group_info()` 等
|
||||
3. **使用透传 API**:调用 `self.call_platform_api()` 使用平台特有功能
|
||||
|
||||
### 6.3 SDK 版本号
|
||||
|
||||
新功能通过提升 SDK minor 版本发布:
|
||||
|
||||
- 现有版本:`langbot-plugin-sdk >= x.y.z`
|
||||
- 新版本:`langbot-plugin-sdk >= x.(y+1).0`
|
||||
|
||||
插件的 `manifest.yaml` 中的 `min_sdk_version` 决定是否能使用新 API。使用旧 SDK 版本的插件在新 LangBot 上正常运行(兼容层保证),只是无法调用新 API。
|
||||
429
docs/event-based-agents/06-migration-plan.md
Normal file
429
docs/event-based-agents/06-migration-plan.md
Normal file
@@ -0,0 +1,429 @@
|
||||
# 分阶段迁移计划
|
||||
|
||||
## 1. 概述
|
||||
|
||||
EBA 架构涉及 langbot-plugin-sdk、LangBot 后端、LangBot 前端、文档和示例插件等多个仓库的改动。为降低风险、保证系统稳定性,采用分阶段渐进式迁移策略。
|
||||
|
||||
### 1.1 阶段总览
|
||||
|
||||
| 阶段 | 名称 | 范围 | 依赖 |
|
||||
|------|------|------|------|
|
||||
| Phase 1 | SDK 实体层 | langbot-plugin-sdk | 无 |
|
||||
| Phase 2 | 适配器重构 | LangBot 后端 | Phase 1 |
|
||||
| Phase 3 | 核心系统 | LangBot 后端 | Phase 2 |
|
||||
| Phase 4 | 插件 SDK 集成 | langbot-plugin-sdk + LangBot | Phase 3 |
|
||||
| Phase 5 | WebUI 编排面板 | LangBot 前端 | Phase 3 |
|
||||
| Phase 6 | 文档与示例 | langbot-wiki + langbot-plugin-demo | Phase 4, 5 |
|
||||
|
||||
### 1.2 核心原则
|
||||
|
||||
- **每个阶段结束后系统可运行**:任何阶段完成后,现有功能不受影响
|
||||
- **向后兼容贯穿全程**:旧接口在整个迁移期间保持可用
|
||||
- **先 SDK 后实现**:先定义好接口和模型,再做具体实现
|
||||
- **先核心适配器后边缘**:优先迁移用户量大的适配器
|
||||
|
||||
---
|
||||
|
||||
## 2. Phase 1:SDK 实体层
|
||||
|
||||
**目标**:在 langbot-plugin-sdk 中定义新的事件体系、通用实体、API 接口和适配器基类。
|
||||
|
||||
**仓库**:`langbot-plugin-sdk`
|
||||
|
||||
### 2.1 任务清单
|
||||
|
||||
| # | 任务 | 文件/模块 | 说明 |
|
||||
|---|------|----------|------|
|
||||
| 1.1 | 定义通用事件基类层次 | `api/entities/builtin/platform/events.py` | 新增 `MessageReceivedEvent`, `MessageEditedEvent`, `GroupMemberJoinedEvent` 等,保留现有 `FriendMessage`/`GroupMessage` |
|
||||
| 1.2 | 定义平台特有事件基类 | `api/entities/builtin/platform/events.py` | 新增 `PlatformSpecificEvent` |
|
||||
| 1.3 | 扩展通用实体 | `api/entities/builtin/platform/entities.py` | 新增 `User`(统一 Friend/GroupMember 的基础)、`Channel` 等,保留现有实体 |
|
||||
| 1.4 | 清理消息组件 | `api/entities/builtin/platform/message.py` | 将 `WeChatMiniPrograms` 等 WeChat 特有组件标记为 platform-specific,不再作为通用组件 |
|
||||
| 1.5 | 定义新适配器基类 | `api/definition/abstract/platform/adapter.py` | 新增 `AbstractPlatformAdapter`(继承现有 `AbstractMessagePlatformAdapter` 并扩展通用 API 方法),保留旧基类 |
|
||||
| 1.6 | 定义 API 能力声明 | `api/definition/abstract/platform/capabilities.py`(新文件) | `AdapterCapabilities` 数据类,声明适配器支持的事件和 API |
|
||||
| 1.7 | 定义 `NotSupportedError` | `api/entities/builtin/platform/errors.py`(新文件) | 可选 API 未实现时抛出的异常 |
|
||||
|
||||
### 2.2 关键设计约束
|
||||
|
||||
- 所有新增定义以**新增文件或新增类**的方式引入,**不修改**现有类的字段和方法签名
|
||||
- 现有 `AbstractMessagePlatformAdapter` 保留不动,新基类 `AbstractPlatformAdapter` 继承它
|
||||
- 新事件类与旧事件类并存,通过 `event_type` 字段(命名空间字符串)区分
|
||||
|
||||
### 2.3 验收标准
|
||||
|
||||
- [ ] 所有新增类可正常 import 且通过类型检查
|
||||
- [ ] 现有 `FriendMessage`, `GroupMessage`, `AbstractMessagePlatformAdapter` 等类行为不变
|
||||
- [ ] 新增单元测试覆盖事件序列化/反序列化、实体构造
|
||||
- [ ] SDK 版本号 minor bump(如 `0.x.0` → `0.x+1.0`)
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 2:适配器重构
|
||||
|
||||
**目标**:将现有单文件适配器迁移到独立目录结构,实现新事件监听和通用 API。
|
||||
|
||||
**仓库**:`LangBot`(后端)
|
||||
|
||||
### 3.1 适配器迁移优先级
|
||||
|
||||
根据用户量和代表性,建议按以下顺序迁移:
|
||||
|
||||
| 优先级 | 适配器 | 理由 |
|
||||
|--------|--------|------|
|
||||
| P0 | **Telegram** | 用户量大,API 最完善,适合作为参考实现 |
|
||||
| P0 | **Discord** | 国际用户主要平台,事件类型丰富 |
|
||||
| P1 | **aiocqhttp**(OneBot v11) | 国内 QQ 用户主要适配器 |
|
||||
| P1 | **Satori** | 通用协议适配器,覆盖多个平台 |
|
||||
| P2 | **Lark** / **DingTalk** / **Slack** | 企业平台,用户量中等 |
|
||||
| P2 | **qqofficial** / **WeChat 系列** | 国内用户 |
|
||||
| P3 | **Kook** / **LINE** / **WeCom 系列** | 用户量较小 |
|
||||
| P3 | **WebSocket** | 内置适配器,相对简单 |
|
||||
| P4 | **legacy/*** | 遗留适配器,按需决定是否迁移或废弃 |
|
||||
|
||||
### 3.2 单个适配器迁移步骤(以 Telegram 为例)
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 2.1 | 创建目录结构 | `pkg/platform/adapters/telegram/` 下创建 `__init__.py`, `adapter.py`, `event_converter.py`, `message_converter.py`, `api_impl.py`, `types.py`, `manifest.yaml` |
|
||||
| 2.2 | 迁移消息转换器 | 将 `TelegramMessageConverter` 从 `sources/telegram.py` 搬到 `adapters/telegram/message_converter.py`,逻辑不变 |
|
||||
| 2.3 | 重写事件转换器 | 新的 `TelegramEventConverter` 支持将 Telegram Update 转换为所有通用事件类型(不只是消息),不支持的事件转为 `PlatformSpecificEvent` |
|
||||
| 2.4 | 实现通用 API | 在 `api_impl.py` 中实现 `edit_message`, `delete_message`, `get_group_info` 等 Telegram 支持的通用 API |
|
||||
| 2.5 | 实现透传 API | 在 `adapter.py` 中实现 `call_platform_api`,将 action 映射到 Telegram Bot API 调用 |
|
||||
| 2.6 | 声明能力 | 在 `manifest.yaml` 或适配器类中声明支持的事件和 API 列表 |
|
||||
| 2.7 | 新建 Adapter 主类 | `TelegramAdapter` 继承 `AbstractPlatformAdapter`(新基类),委托各模块实现 |
|
||||
| 2.8 | 更新 manifest.yaml | 更新 `execution.python.path` 指向新位置 |
|
||||
| 2.9 | 验证 | 确保新适配器通过现有消息收发流程的测试 |
|
||||
|
||||
### 3.3 基础设施任务
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 2.A | 创建 `adapters/_base/` | 将 SDK 中新基类的运行时辅助代码放在此处(如事件分发辅助函数) |
|
||||
| 2.B | 更新 ComponentDiscovery | 使 `discover_blueprint` 支持扫描 `adapters/` 子目录中的 YAML |
|
||||
| 2.C | 更新 `templates/components.yaml` | 将 `fromDirs` 从 `pkg/platform/sources/` 改为 `pkg/platform/adapters/`(过渡期两个都扫描) |
|
||||
| 2.D | 保留旧 sources/ | 过渡期不删除旧文件,通过 manifest 的 `deprecated: true` 标记 |
|
||||
|
||||
### 3.4 验收标准
|
||||
|
||||
- [ ] 已迁移的适配器在新目录结构下正常启动和收发消息
|
||||
- [ ] 新事件(如 `message.edited`)在支持的平台上正确触发
|
||||
- [ ] 通用 API(如 `edit_message`)在支持的平台上正确执行
|
||||
- [ ] 未迁移的适配器(仍在 `sources/`)继续正常工作
|
||||
- [ ] ComponentDiscovery 同时扫描新旧目录
|
||||
|
||||
---
|
||||
|
||||
## 4. Phase 3:核心系统
|
||||
|
||||
**目标**:实现 EventBus、EventRouter 和事件处理器框架,将事件从适配器分发到不同的处理器。
|
||||
|
||||
**仓库**:`LangBot`(后端)
|
||||
|
||||
### 4.1 任务清单
|
||||
|
||||
| # | 任务 | 文件/模块 | 说明 |
|
||||
|---|------|----------|------|
|
||||
| 3.1 | 实现 EventBus | `pkg/platform/event_bus.py`(新文件) | 事件总线:接收适配器事件,进行日志记录,分发给 EventRouter |
|
||||
| 3.2 | 实现 EventRouter | `pkg/platform/event_router.py`(新文件) | 事件路由引擎:读取 Bot 的 `event_handlers` 配置,匹配事件类型,分发到对应 Handler |
|
||||
| 3.3 | 实现 PipelineHandler | `pkg/platform/handlers/pipeline_handler.py` | 将 `message.received` 事件转为现有 Query,进入 Pipeline 流水线 |
|
||||
| 3.4 | 实现 AgentHandler | `pkg/platform/handlers/agent_handler.py` | 直接调用 RequestRunner 处理事件,不经过 Pipeline 多 Stage 流程 |
|
||||
| 3.5 | 实现 WebhookHandler | `pkg/platform/handlers/webhook_handler.py` | 将事件 POST 到外部 URL,解析响应执行动作(重构现有 WebhookPusher) |
|
||||
| 3.6 | 实现 PluginHandler | `pkg/platform/handlers/plugin_handler.py` | 将事件分发给插件 EventListener(复用现有 plugin_connector 机制) |
|
||||
| 3.7 | Bot 实体扩展 | `pkg/entity/persistence/bot.py` | 新增 `event_handlers` JSON 字段 |
|
||||
| 3.8 | 数据库迁移 | `pkg/persistence/migrations/` | 新增迁移脚本:添加 `event_handlers` 列,将现有 `use_pipeline_uuid` 数据迁移为 `event_handlers` 格式 |
|
||||
| 3.9 | 重构 RuntimeBot | `pkg/platform/botmgr.py` | 将 `initialize()` 中硬编码的 `on_friend_message`/`on_group_message` 回调替换为通过 EventBus 分发所有事件 |
|
||||
| 3.10 | 重构 MessageAggregator | `pkg/pipeline/aggregator.py` | 从 RuntimeBot 解耦,作为 PipelineHandler 的内部机制(只对 `message.received` 事件生效) |
|
||||
| 3.11 | Agent Handler 中 RequestRunner 解耦 | `pkg/provider/runner.py` + handlers | RequestRunner 需要能独立于 Pipeline Stage 运行,为 Agent Handler 提供轻量调用路径 |
|
||||
| 3.12 | HTTP API 扩展 | `pkg/api/http/controller/` | 新增/更新 Bot API 端点以支持 `event_handlers` 的 CRUD |
|
||||
|
||||
### 4.2 数据迁移策略
|
||||
|
||||
现有 Bot 表有 `use_pipeline_uuid` 字段,需要自动迁移为 `event_handlers`:
|
||||
|
||||
```python
|
||||
# 迁移逻辑伪代码
|
||||
for bot in all_bots:
|
||||
if bot.use_pipeline_uuid:
|
||||
bot.event_handlers = [
|
||||
{
|
||||
"event_type": "message.received",
|
||||
"handler_type": "pipeline",
|
||||
"handler_config": {
|
||||
"pipeline_uuid": bot.use_pipeline_uuid
|
||||
}
|
||||
}
|
||||
]
|
||||
else:
|
||||
bot.event_handlers = []
|
||||
```
|
||||
|
||||
### 4.3 RuntimeBot 重构要点
|
||||
|
||||
当前 `RuntimeBot.initialize()` 硬编码注册两个回调:
|
||||
|
||||
```python
|
||||
# 现有代码 (botmgr.py)
|
||||
self.adapter.register_listener(FriendMessage, on_friend_message)
|
||||
self.adapter.register_listener(GroupMessage, on_group_message)
|
||||
```
|
||||
|
||||
重构后改为注册通用事件回调:
|
||||
|
||||
```python
|
||||
# 新代码
|
||||
async def on_event(event: Event, adapter: AbstractPlatformAdapter):
|
||||
await self.event_bus.emit(
|
||||
bot_uuid=self.bot_entity.uuid,
|
||||
event=event,
|
||||
adapter=adapter,
|
||||
)
|
||||
|
||||
# 注册所有事件类型的统一回调
|
||||
self.adapter.register_listener(Event, on_event)
|
||||
```
|
||||
|
||||
EventBus 接收事件后,调用 EventRouter 按配置分发。
|
||||
|
||||
### 4.4 事件处理器执行流程
|
||||
|
||||
```
|
||||
EventBus.emit(bot_uuid, event, adapter)
|
||||
│
|
||||
▼
|
||||
EventRouter.route(bot_uuid, event)
|
||||
│ 查询 bot.event_handlers 配置
|
||||
│ 匹配 event_type(精确匹配 > 通配符 *)
|
||||
▼
|
||||
匹配到的 Handler(s)
|
||||
│
|
||||
├── PipelineHandler.handle(event, adapter)
|
||||
│ │ 仅支持 message.received
|
||||
│ │ 构造 Query → MessageAggregator → QueryPool → Pipeline
|
||||
│ └── 沿用现有完整流水线机制
|
||||
│
|
||||
├── AgentHandler.handle(event, adapter)
|
||||
│ │ 根据 handler_config 选择 RequestRunner
|
||||
│ │ 直接调用 runner.run() 处理事件
|
||||
│ └── 将结果通过 adapter API 回复
|
||||
│
|
||||
├── WebhookHandler.handle(event, adapter)
|
||||
│ │ 序列化事件为 JSON
|
||||
│ │ POST 到 handler_config.url
|
||||
│ └── 解析响应,执行动作(回复消息、调用 API 等)
|
||||
│
|
||||
└── PluginHandler.handle(event, adapter)
|
||||
│ 通过 plugin_connector 分发给插件
|
||||
└── 插件 EventListener 处理
|
||||
```
|
||||
|
||||
### 4.5 验收标准
|
||||
|
||||
- [ ] `message.received` 事件通过 PipelineHandler 正确进入现有 Pipeline(与旧行为一致)
|
||||
- [ ] 新增事件(如 `group.member_joined`)能通过 PluginHandler 分发给插件
|
||||
- [ ] AgentHandler 能直接调用 RequestRunner(至少 `local-agent`)处理事件并回复
|
||||
- [ ] WebhookHandler 能将事件 POST 到外部 URL
|
||||
- [ ] 数据库迁移正确执行,`use_pipeline_uuid` 数据迁移到 `event_handlers`
|
||||
- [ ] 现有 Bot 在不修改配置的情况下行为不变(自动迁移保证)
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 4:插件 SDK 集成
|
||||
|
||||
**目标**:将新事件和 API 通过插件 SDK 暴露给插件开发者,同时实现兼容层。
|
||||
|
||||
**仓库**:`langbot-plugin-sdk` + `LangBot`
|
||||
|
||||
### 5.1 任务清单
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 4.1 | 新增插件事件包装 | 在 `api/entities/events.py` 中为每个通用事件新增插件级事件类(如 `MessageEditedReceived`, `MemberJoinedReceived`) |
|
||||
| 4.2 | 兼容层实现 | `PersonMessageReceived` / `GroupMessageReceived` 由新的 `MessageReceivedEvent` 自动生成,旧事件作为新事件的 alias |
|
||||
| 4.3 | 新 API 暴露 | 在 `LangBotAPIProxy` 中新增方法:`edit_message`, `delete_message`, `get_group_info`, `get_user_info`, `call_platform_api` 等 |
|
||||
| 4.4 | 通信协议扩展 | 在 `entities/io/actions/enums.py` 中新增 action 枚举(如 `EDIT_MESSAGE`, `DELETE_MESSAGE`, `GET_GROUP_INFO`, `CALL_PLATFORM_API`) |
|
||||
| 4.5 | Runtime Handler 扩展 | 在 PluginConnectionHandler / ControlConnectionHandler 中添加新 action 的处理逻辑 |
|
||||
| 4.6 | EventListener 扩展 | 确保 `@handler()` 装饰器支持注册新事件类型 |
|
||||
| 4.7 | QueryBasedAPI 扩展 | 在 `QueryBasedAPIProxy` 中新增事件上下文相关的 API(如 `get_event_source_adapter`) |
|
||||
|
||||
### 5.2 兼容层详细设计
|
||||
|
||||
```
|
||||
新事件系统 旧事件系统(兼容层)
|
||||
───────────── ─────────────────
|
||||
MessageReceivedEvent ┌→ PersonMessageReceived (chat_type == "private")
|
||||
(chat_type: "private"|"group") ┤
|
||||
└→ GroupMessageReceived (chat_type == "group")
|
||||
```
|
||||
|
||||
**实现方式**:在 RuntimeEventDispatcher 中,当分发 `MessageReceivedEvent` 给插件时,同时生成对应的旧事件类实例。插件可以用新事件类或旧事件类注册 handler,都能收到。
|
||||
|
||||
### 5.3 验收标准
|
||||
|
||||
- [ ] 现有插件(使用旧事件和 API)无需修改即可运行
|
||||
- [ ] 新插件可以使用新事件类型(如 `MemberJoinedReceived`)注册 handler
|
||||
- [ ] 新 API(如 `edit_message`)可通过 `self.edit_message()` 或 `event_context.edit_message()` 调用
|
||||
- [ ] 透传 API `call_platform_api` 可正常调用适配器特有功能
|
||||
- [ ] 所有新 action 的通信协议正确工作(stdio / WebSocket)
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 5:WebUI 编排面板
|
||||
|
||||
**目标**:在 WebUI 的 Bot 管理页面实现事件处理器的可视化编排。
|
||||
|
||||
**仓库**:`LangBot`(前端 `web/`)
|
||||
|
||||
### 6.1 任务清单
|
||||
|
||||
| # | 任务 | 说明 |
|
||||
|---|------|------|
|
||||
| 5.1 | Bot 编辑页面扩展 | 在 Bot 编辑页面新增「事件处理」面板 |
|
||||
| 5.2 | 事件处理器列表组件 | 可视化展示当前 Bot 的 `event_handlers` 列表,支持增删改排序 |
|
||||
| 5.3 | 事件类型选择器 | 下拉选择事件类型(命名空间分组展示),支持通配符 `*` |
|
||||
| 5.4 | Handler 类型选择与配置 | 选择 handler 类型后展示对应的配置表单(Pipeline 选择器、Runner 选择器、Webhook URL 等) |
|
||||
| 5.5 | Pipeline Handler 配置 | 复用现有的 Pipeline 选择 UI(从现有 `use_pipeline_uuid` 选择器迁移) |
|
||||
| 5.6 | Agent Handler 配置 | Runner 选择器(local-agent / dify / n8n / coze 等)+ Runner 参数配置表单 |
|
||||
| 5.7 | Webhook Handler 配置 | URL 输入、认证方式选择、Header 配置 |
|
||||
| 5.8 | Plugin Handler 配置 | 通常无需额外配置,分发给所有匹配的插件 EventListener |
|
||||
| 5.9 | HTTP API 对接 | 前端调用后端 API 保存/读取 `event_handlers` 配置 |
|
||||
| 5.10 | 迁移提示 | 对于从旧版本升级的用户,如果检测到 `use_pipeline_uuid` 已自动迁移,展示提示说明 |
|
||||
|
||||
### 6.2 UI 交互设计概要
|
||||
|
||||
```
|
||||
┌─ Bot 编辑页面 ─────────────────────────────────────┐
|
||||
│ │
|
||||
│ 基本信息 │ 适配器配置 │ ★ 事件处理 │ │
|
||||
│ │
|
||||
│ ┌─ 事件处理器列表 ────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ① message.received → Pipeline: "主流水线" │ │
|
||||
│ │ [编辑] [删除] │ │
|
||||
│ │ │ │
|
||||
│ │ ② group.member_joined → Agent: local-agent │ │
|
||||
│ │ [编辑] [删除] │ │
|
||||
│ │ │ │
|
||||
│ │ ③ * (默认) → Plugin │ │
|
||||
│ │ [编辑] [删除] │ │
|
||||
│ │ │ │
|
||||
│ │ [+ 添加事件处理器] │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [保存] [取消] │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.3 验收标准
|
||||
|
||||
- [ ] 用户可以在 WebUI 上为 Bot 添加/编辑/删除事件处理器
|
||||
- [ ] 四种 Handler 类型均有对应的配置表单
|
||||
- [ ] 配置保存后正确写入数据库 `event_handlers` 字段
|
||||
- [ ] 旧版本升级后,自动迁移的配置在 UI 上正确展示
|
||||
- [ ] Pipeline Handler 的行为与旧的 `use_pipeline_uuid` 完全一致
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 6:文档与示例
|
||||
|
||||
**目标**:更新所有面向开发者的文档和示例。
|
||||
|
||||
**仓库**:`langbot-wiki`, `langbot-plugin-demo`
|
||||
|
||||
### 7.1 任务清单
|
||||
|
||||
| # | 任务 | 仓库 | 说明 |
|
||||
|---|------|------|------|
|
||||
| 6.1 | EBA 架构概览文档 | langbot-wiki | 面向用户的新架构说明 |
|
||||
| 6.2 | 适配器开发指南更新 | langbot-wiki | 如何开发一个新的适配器(新目录结构、新基类、事件转换等) |
|
||||
| 6.3 | 插件开发指南更新 | langbot-wiki | 新事件类型、新 API 的使用说明 |
|
||||
| 6.4 | 插件迁移指南 | langbot-wiki | 现有插件如何迁移到新事件/API(如果需要使用新能力) |
|
||||
| 6.5 | 事件处理器配置指南 | langbot-wiki | WebUI 上如何配置事件处理器 |
|
||||
| 6.6 | 示例插件更新 | langbot-plugin-demo | HelloPlugin 增加新事件监听示例、新 API 调用示例 |
|
||||
| 6.7 | 新示例插件 | langbot-plugin-demo | 新建一个示例展示非消息事件处理(如入群欢迎) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险评估与缓解
|
||||
|
||||
### 8.1 技术风险
|
||||
|
||||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| 适配器迁移中断现有功能 | 高 | 中 | 新旧目录并存,ComponentDiscovery 同时扫描两个目录,逐个适配器迁移验证 |
|
||||
| 事件模型不兼容导致插件崩溃 | 高 | 低 | 兼容层保证旧事件类型继续工作,新增类不修改旧类 |
|
||||
| 数据库迁移失败 | 高 | 低 | 迁移脚本做前置校验,`use_pipeline_uuid` 在过渡期保留不删除 |
|
||||
| RequestRunner 解耦破坏 Pipeline | 高 | 中 | Agent Handler 调用 Runner 的路径独立于 Pipeline,不修改现有 Pipeline Stage 中的 Runner 调用逻辑 |
|
||||
| 性能回退(EventBus 额外开销) | 中 | 低 | EventBus 在进程内同步分发,无额外序列化/网络开销 |
|
||||
| 各平台事件差异大难以统一 | 中 | 中 | 通用事件只抽象最大公约数字段,差异部分保留在 `source_platform_object`;不支持的事件走 `PlatformSpecificEvent` |
|
||||
|
||||
### 8.2 兼容性风险
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 现有插件使用旧事件类 | 兼容层自动将新事件转为旧事件分发,两种事件类都能注册 handler |
|
||||
| 现有插件调用 `reply()` / `send_message()` | 这两个 API 保持不变,只是底层实现可能微调 |
|
||||
| 第三方基于 `AbstractMessagePlatformAdapter` 开发的适配器 | 旧基类保留,新基类继承旧基类,第三方适配器无需立即迁移 |
|
||||
| 用户自定义 Pipeline 配置 | Pipeline 机制完整保留,PipelineHandler 只是入口变了(从 RuntimeBot 硬编码变为 EventRouter 配置) |
|
||||
|
||||
### 8.3 回滚策略
|
||||
|
||||
每个 Phase 独立可回滚:
|
||||
|
||||
- **Phase 1**(SDK 新增类):删除新增文件,回退 SDK 版本号
|
||||
- **Phase 2**(适配器目录):恢复 `components.yaml` 的 `fromDirs` 指向旧目录,旧 sources/ 未删除
|
||||
- **Phase 3**(核心系统):回退数据库迁移,恢复 RuntimeBot 旧的硬编码回调
|
||||
- **Phase 4**(插件集成):回退 SDK 版本,插件使用旧版 SDK
|
||||
- **Phase 5**(WebUI):前端回退,Bot 编辑页面隐藏事件处理面板
|
||||
|
||||
---
|
||||
|
||||
## 9. 里程碑与时间线建议
|
||||
|
||||
| 里程碑 | 阶段 | 预期产出 |
|
||||
|--------|------|----------|
|
||||
| M1 | Phase 1 完成 | SDK 新版本发布,包含新事件/实体/基类定义 |
|
||||
| M2 | Phase 2 首批适配器(Telegram + Discord) | 两个参考实现,验证目录结构和事件/API 体系 |
|
||||
| M3 | Phase 3 核心系统 | EventBus + EventRouter + 四种 Handler 可用 |
|
||||
| M4 | Phase 2 剩余适配器 | 所有活跃适配器迁移完成 |
|
||||
| M5 | Phase 4 插件集成 | 新 SDK 发布,插件可使用新事件和 API |
|
||||
| M6 | Phase 5 WebUI | 事件处理器编排面板上线 |
|
||||
| M7 | Phase 6 文档 | 开发者文档和示例更新完毕 |
|
||||
|
||||
建议 M1-M3 作为第一个大版本发布(如 v5.0),M4-M7 在后续小版本迭代中完成。
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发指引
|
||||
|
||||
### 10.1 分支策略
|
||||
|
||||
建议在主仓库创建 `feature/eba` 长期特性分支,各 Phase 在子分支上开发后合入特性分支:
|
||||
|
||||
```
|
||||
main
|
||||
└── feature/eba
|
||||
├── feature/eba-sdk-entities (Phase 1)
|
||||
├── feature/eba-adapter-telegram (Phase 2)
|
||||
├── feature/eba-adapter-discord (Phase 2)
|
||||
├── feature/eba-core-system (Phase 3)
|
||||
├── feature/eba-plugin-sdk (Phase 4)
|
||||
└── feature/eba-webui (Phase 5)
|
||||
```
|
||||
|
||||
### 10.2 测试策略
|
||||
|
||||
| 层次 | 测试内容 | 工具 |
|
||||
|------|----------|------|
|
||||
| 单元测试 | 事件序列化/反序列化、实体构造、API 调用 mock | pytest |
|
||||
| 集成测试 | EventBus → EventRouter → Handler 全链路 | pytest + asyncio |
|
||||
| 适配器测试 | 各适配器的事件转换、消息转换、API 调用 | pytest + mock SDK |
|
||||
| 端到端测试 | 从模拟平台事件到完整处理流程 | staging 环境 |
|
||||
| 插件兼容性测试 | 旧插件在新系统下的行为 | langbot-plugin-demo |
|
||||
|
||||
### 10.3 代码审查关注点
|
||||
|
||||
- 新增代码是否影响现有行为
|
||||
- 兼容层是否正确映射所有旧事件/API 场景
|
||||
- 数据库迁移是否可逆
|
||||
- 新 API 的错误处理(`NotSupportedError`)是否一致
|
||||
- 事件模型的序列化在 stdio/WebSocket 通信中是否正确
|
||||
37
docs/event-based-agents/adapters/00-index.md
Normal file
37
docs/event-based-agents/adapters/00-index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# EBA Adapter Migration Records
|
||||
|
||||
This directory records adapter-level migration details for the Event-Based Agents architecture. Each adapter document should be kept close to the implementation and must answer four questions:
|
||||
|
||||
1. What changed in the adapter structure.
|
||||
2. Which configuration fields are required.
|
||||
3. Which events and APIs are supported.
|
||||
4. What has been verified end to end.
|
||||
|
||||
## Adapter Documents
|
||||
|
||||
General acceptance checklist: [EBA Adapter Acceptance Checklist](./acceptance-checklist.md)
|
||||
|
||||
Current acceptance report: [EBA Adapter Acceptance Report](./acceptance-report.md)
|
||||
|
||||
| Adapter | Status | Document |
|
||||
|---------|--------|----------|
|
||||
| Telegram | Migrated; partial plugin E2E, real UI inbound image/file verified | [Telegram](./telegram.md) |
|
||||
| Discord | Migrated; partial plugin E2E, media-inbound gaps remain | [Discord](./discord.md) |
|
||||
| OneBot v11 / aiocqhttp | Migrated; Matcha UI plus protocol-level multi-component coverage | [OneBot v11 / aiocqhttp](./aiocqhttp.md) |
|
||||
| DingTalk | Migrated; partial plugin E2E, real UI inbound image/file verified; group gap remains | [DingTalk](./dingtalk.md) |
|
||||
| Lark / Feishu | Migrated; partial live text E2E, media-inbound gap remains | [Lark / Feishu](./lark.md) |
|
||||
| WeCom | Migrated; private text plugin E2E verified, media/group gaps remain | [WeCom](./wecom.md) |
|
||||
| WeComBot | Migrated; private text and outbound/API plugin E2E verified, feedback/group gaps remain | [WeComBot](./wecombot.md) |
|
||||
| Official Account | Migrated; private text plugin E2E verified, proactive outbound not supported | [Official Account](./officialaccount.md) |
|
||||
|
||||
## Documentation Checklist
|
||||
|
||||
When migrating a new adapter, add one document here with:
|
||||
|
||||
- Configuration table matching the adapter manifest.
|
||||
- Supported event list.
|
||||
- Supported common API list.
|
||||
- Supported `call_platform_api` action list.
|
||||
- Known unsupported APIs and the reason.
|
||||
- Live test notes, including platform, channel type, destructive operations, and residual risks.
|
||||
- A clear distinction between real UI inbound media, protocol-level injected inbound media, and bot outbound media.
|
||||
208
docs/event-based-agents/adapters/acceptance-checklist.md
Normal file
208
docs/event-based-agents/adapters/acceptance-checklist.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# EBA Adapter Acceptance Checklist
|
||||
|
||||
This checklist is the architecture-level acceptance standard for every Event-Based Agents platform adapter. It is not platform-specific. Adapter migration is not complete until the adapter has a written result against this checklist.
|
||||
|
||||
## Evidence Levels
|
||||
|
||||
Use these evidence levels consistently in adapter records:
|
||||
|
||||
| Level | Meaning | Can Mark Complete |
|
||||
|-------|---------|-------------------|
|
||||
| `plugin-e2e-ui` | Real SDK plugin running through standalone runtime, LangBot core, the migrated adapter, and a real platform/simulator UI action. | Yes |
|
||||
| `plugin-e2e-protocol` | Real SDK plugin running through standalone runtime, LangBot core, and the migrated adapter from a protocol-boundary event injection, such as a OneBot reverse WebSocket event. | Partial; must not be claimed as UI coverage |
|
||||
| `plugin-e2e-outbound` | Real SDK plugin calls an API and the bot output is visible in the real platform/simulator UI. | Yes for send/API coverage only |
|
||||
| `adapter-live` | Direct adapter probe connected to a real or simulator platform endpoint, bypassing plugin runtime. | No, auxiliary only |
|
||||
| `unit` | Unit/API-shape tests with mocked platform SDK objects or mocked APIs. | No, auxiliary only |
|
||||
| `not-supported` | Platform protocol or SDK has no equivalent capability. Must include reason and source. | Yes, as explicitly unsupported |
|
||||
| `blocked` | Intended capability could not be verified because of credentials, permissions, endpoint gaps, or simulator gaps. | No |
|
||||
|
||||
The primary acceptance path must be `plugin-e2e-ui` for inbound UI-triggered behavior and `plugin-e2e-outbound` for bot send/API behavior. `adapter-live`, `plugin-e2e-protocol`, and `unit` tests are useful, but they must be labelled precisely.
|
||||
|
||||
## Required Architecture Path
|
||||
|
||||
Every adapter must prove this full path:
|
||||
|
||||
```text
|
||||
Real platform / simulator UI
|
||||
-> platform SDK native event
|
||||
-> adapter event converter
|
||||
-> unified EBA event/entity/message types
|
||||
-> LangBot core event dispatch
|
||||
-> standalone SDK runtime
|
||||
-> real test plugin listener
|
||||
-> plugin calls platform APIs through SDK
|
||||
-> LangBot core API dispatch
|
||||
-> adapter API implementation
|
||||
-> real platform / simulator UI
|
||||
```
|
||||
|
||||
The test plugin must record JSONL evidence containing:
|
||||
|
||||
- event class and `event.type`
|
||||
- `bot_uuid` and `adapter_name` as received by the plugin
|
||||
- adapter name
|
||||
- chat type and chat ID
|
||||
- sender/user/group IDs with secrets redacted
|
||||
- message component list for received messages
|
||||
- API action name, input summary, result or error
|
||||
- raw unsupported/blocked reason when an item is skipped
|
||||
|
||||
## Required Message Receive Tests
|
||||
|
||||
For every adapter, inbound message conversion must be tested through `plugin-e2e-ui` for each component the platform can receive. If a protocol-level injection is used, label it `plugin-e2e-protocol`; it proves the adapter/core/plugin path, but it does not prove that the user-facing platform UI can send that component. If the platform UI/simulator cannot create a component, record it as `blocked` with the endpoint limitation.
|
||||
|
||||
| Component | Required Receive Assertion |
|
||||
|-----------|----------------------------|
|
||||
| `Source` | Message ID and timestamp are present and stable enough for reply/get/delete APIs. |
|
||||
| `Plain` | Text is preserved exactly, including spaces and multi-line content. |
|
||||
| `At` | Mentioned user ID is converted to common `At.target`. |
|
||||
| `AtAll` | Broadcast mention is converted to common `AtAll`, if platform supports it. |
|
||||
| `Image` | Image ID, URL, path, or base64 is represented without leaking platform-native segment shape. |
|
||||
| `Voice` | Voice/audio component is represented as `Voice` when the platform exposes it. |
|
||||
| `File` | File name, ID/URL, and size are represented as `File` when available. |
|
||||
| `Quote` | Reply/quote source ID and origin content are represented when the platform exposes it. |
|
||||
| `Face` | Native emoji/sticker/dice/rps-like components are represented as `Face` or documented as platform-specific. |
|
||||
| `Forward` | Merged/forwarded messages are represented as `Forward` when the platform exposes structured content. |
|
||||
| `Unknown` | Unsupported native segments become `Unknown` or `PlatformSpecificEvent` data, not crashes. |
|
||||
| Mixed chain | A message containing multiple component types preserves order. |
|
||||
|
||||
The plugin must subscribe to `MessageReceivedEvent` and assert that `message_chain` contains common `langbot_plugin.api.entities.builtin.platform.message` components, not platform-native SDK objects.
|
||||
|
||||
## Required Message Send Tests
|
||||
|
||||
For every adapter, outbound message conversion must be tested through `plugin-e2e-outbound` by having the plugin call SDK platform APIs and verifying the platform UI/simulator receives the expected message.
|
||||
|
||||
| Component | Required Send Assertion |
|
||||
|-----------|-------------------------|
|
||||
| `Plain` | Text appears exactly on the platform. |
|
||||
| `At` | User mention renders as a mention or platform equivalent. |
|
||||
| `AtAll` | Broadcast mention renders or is explicitly unsupported. |
|
||||
| `Image` | URL, path, or base64 image sends and renders/downloads correctly. |
|
||||
| `Voice` | Voice/audio sends when supported. |
|
||||
| `File` | File sends with name and content/link when supported. |
|
||||
| `Quote` | Quoted reply points to the original message when supported. |
|
||||
| `Face` | Native emoji/sticker/dice/rps sends or is explicitly unsupported. |
|
||||
| `Forward` | Forward/merged-forward sends when supported; otherwise fallback behavior is documented. |
|
||||
| Mixed chain | A mixed chain preserves component order as closely as the platform allows. |
|
||||
|
||||
If a platform supports a component only in one direction, the adapter record must say so explicitly.
|
||||
|
||||
## Required Event Tests
|
||||
|
||||
The plugin must subscribe to every event declared in `manifest.yaml -> spec.supported_events` and record one of `plugin-e2e-ui`, `plugin-e2e-protocol`, `not-supported`, or `blocked`.
|
||||
|
||||
| Event | Required Assertion |
|
||||
|-------|--------------------|
|
||||
| `message.received` | Real message reaches plugin as `MessageReceivedEvent`. |
|
||||
| `message.edited` | Edited message reaches plugin with message ID and new content, if declared. |
|
||||
| `message.deleted` | Deleted/recalled message reaches plugin with message ID and operator when available, if declared. |
|
||||
| `message.reaction` | Reaction add/remove reaches plugin with message ID, user, reaction, and direction, if declared. |
|
||||
| `feedback.received` | Feedback payload reaches plugin with feedback type and message/session IDs, if declared. |
|
||||
| `group.member_joined` | Join event reaches plugin with group and member. |
|
||||
| `group.member_left` | Leave/kick event reaches plugin with group, member, and kick flag. |
|
||||
| `group.member_banned` | Mute/ban event reaches plugin with group, member, operator, and duration. |
|
||||
| `group.info_updated` | Group metadata update reaches plugin with changed fields, if declared. |
|
||||
| `friend.request_received` | Friend request reaches plugin with request ID and message. |
|
||||
| `friend.added` | Friend-added event reaches plugin. |
|
||||
| `friend.removed` | Friend-removed event reaches plugin, if declared. |
|
||||
| `bot.invited_to_group` | Bot invite/join request reaches plugin with group and inviter/request ID. |
|
||||
| `bot.removed_from_group` | Bot removal reaches plugin with group and operator when available. |
|
||||
| `bot.muted` | Bot mute reaches plugin with duration. |
|
||||
| `bot.unmuted` | Bot unmute reaches plugin. |
|
||||
| `platform.specific` | At least one unmapped native event is delivered as structured platform-specific data, if declared. |
|
||||
|
||||
Do not declare an event in the manifest unless there is an implementation path and an acceptance entry.
|
||||
|
||||
## Required Common API Tests
|
||||
|
||||
The plugin must call every common API declared in `manifest.yaml -> spec.supported_apis.required` and `optional`. Each call must be recorded with input summary and result.
|
||||
|
||||
| API | Required Assertion |
|
||||
|-----|--------------------|
|
||||
| `send_message` | Plugin sends to private and group/channel targets where supported. |
|
||||
| `reply_message` | Plugin replies to the triggering message, with quoted mode tested when supported. |
|
||||
| `edit_message` | Plugin edits a bot-sent message, if declared. |
|
||||
| `delete_message` | Plugin deletes/recalls a bot-sent message, if declared and permissions allow. |
|
||||
| `forward_message` | Plugin forwards or emulates forwarding a real message, if declared. |
|
||||
| `get_message` | Plugin retrieves a real message and receives common `MessageReceivedEvent` shape. |
|
||||
| `get_group_info` | Plugin receives `UserGroup` with ID/name/count where available. |
|
||||
| `get_group_list` | Plugin receives joined groups/channels list where supported. |
|
||||
| `get_group_member_list` | Plugin receives list of `UserGroupMember` where supported. |
|
||||
| `get_group_member_info` | Plugin receives one member with role/display name where available. |
|
||||
| `set_group_name` | Plugin changes and restores a disposable group name, if declared. |
|
||||
| `mute_member` | Plugin mutes a disposable target, if declared. |
|
||||
| `unmute_member` | Plugin unmutes the same target, if declared. |
|
||||
| `kick_member` | Plugin kicks a disposable target only in destructive test mode, if declared. |
|
||||
| `leave_group` | Plugin leaves only in destructive test mode and only at the end, if declared. |
|
||||
| `get_user_info` | Plugin receives common `User` shape. |
|
||||
| `get_friend_list` | Plugin receives friend/contact list where supported. |
|
||||
| `approve_friend_request` | Plugin accepts/rejects a disposable friend request, if declared. |
|
||||
| `approve_group_invite` | Plugin accepts/rejects a disposable group invite, if declared. |
|
||||
| `upload_file` | Plugin uploads a real small file, if declared. |
|
||||
| `get_file_url` | Plugin resolves a real file ID to a URL, if declared. |
|
||||
| `call_platform_api` | Plugin calls every declared platform-specific action with safe parameters. |
|
||||
|
||||
Destructive APIs must be opt-in and documented with the exact target used.
|
||||
|
||||
The SDK must expose a plugin-side platform API escape hatch for adapter-specific actions. The acceptance plugin should call it from the same EBA event handler that received the real platform event, so the evidence proves both directions of the path:
|
||||
|
||||
```text
|
||||
plugin -> SDK call_platform_api -> LangBot core -> adapter call_platform_api -> platform SDK/API
|
||||
```
|
||||
|
||||
The result must be serialized into JSON-safe values before it is returned to the plugin runtime.
|
||||
|
||||
## Platform-Specific API Tests
|
||||
|
||||
Every action listed in `manifest.yaml -> spec.platform_specific_apis` must have one acceptance entry:
|
||||
|
||||
- `plugin-e2e-ui` or `plugin-e2e-outbound`: called by the plugin against the live/simulator endpoint.
|
||||
- `plugin-e2e-protocol`: called by the plugin after a protocol-boundary injected event; useful for endpoint-specific simulators but must be labelled.
|
||||
- `not-supported`: removed from manifest or explained if the platform SDK exposes it but this adapter intentionally does not.
|
||||
- `blocked`: endpoint did not implement it, permissions missing, or safe fixture unavailable.
|
||||
|
||||
Do not leave a platform-specific API in the manifest without a corresponding test record.
|
||||
|
||||
## Required Compatibility Tests
|
||||
|
||||
Each migrated adapter must also prove:
|
||||
|
||||
- Manifest supported events match `adapter.get_supported_events()`.
|
||||
- Manifest supported APIs match `adapter.get_supported_apis()`.
|
||||
- Manifest platform-specific actions match `PLATFORM_API_MAP`.
|
||||
- Legacy `FriendMessage` / `GroupMessage` listeners still work when the core registers them.
|
||||
- EBA listener dispatch prefers the most specific event class, then `EBAEvent`, then base `Event`.
|
||||
- Self-message filtering prevents bot echo loops without dropping edit/delete/moderation events needed for API tests.
|
||||
- `source_platform_object` is present for reply/debug but not required by plugins for common behavior.
|
||||
|
||||
## Required Documentation Per Adapter
|
||||
|
||||
Each adapter document must include:
|
||||
|
||||
- adapter directory and manifest name
|
||||
- config table
|
||||
- supported event table with evidence level per event
|
||||
- supported common API table with evidence level per API
|
||||
- platform-specific API table with evidence level per action
|
||||
- receive component table with evidence level per component
|
||||
- send component table with evidence level per component
|
||||
- exact test date
|
||||
- exact platform endpoint or simulator used
|
||||
- standalone runtime command
|
||||
- plugin path/name used for testing
|
||||
- evidence JSONL path
|
||||
- destructive operations performed or explicitly skipped
|
||||
- blocked items and reasons
|
||||
|
||||
## Acceptance Rule
|
||||
|
||||
An adapter can be marked migrated only when:
|
||||
|
||||
1. All declared events have `plugin-e2e-ui`, justified `plugin-e2e-protocol`, or `not-supported` evidence.
|
||||
2. All declared APIs have `plugin-e2e-outbound` or `not-supported` evidence.
|
||||
3. All platform-supported receive components have `plugin-e2e-ui` evidence; protocol-only receive coverage keeps the status partial.
|
||||
4. All platform-supported send components have `plugin-e2e-outbound` evidence.
|
||||
5. Unit tests cover conversion and API-shape boundaries.
|
||||
6. The adapter document lists every blocked or skipped item honestly.
|
||||
|
||||
If any declared capability is only covered by `adapter-live` or `unit`, the adapter status must remain partial.
|
||||
164
docs/event-based-agents/adapters/acceptance-report.md
Normal file
164
docs/event-based-agents/adapters/acceptance-report.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# EBA Adapter Acceptance Report
|
||||
|
||||
Date: May 10, 2026
|
||||
|
||||
Scope:
|
||||
|
||||
- `telegram-eba`
|
||||
- `discord-eba`
|
||||
- `aiocqhttp-eba`
|
||||
- `dingtalk-eba`
|
||||
- `lark-eba`
|
||||
- `wecom-eba`
|
||||
- `wecombot-eba`
|
||||
- `wecomcs-eba`
|
||||
- `officialaccount-eba`
|
||||
|
||||
This report follows `acceptance-checklist.md`. Evidence levels are intentionally strict:
|
||||
|
||||
- `plugin-e2e-ui`: real platform or simulator UI event reached LangBot, standalone runtime, and `EBAEventProbe`.
|
||||
- `plugin-e2e-protocol`: real adapter endpoint event reached LangBot, standalone runtime, and `EBAEventProbe`, but the event was injected at the platform protocol boundary rather than sent through the UI.
|
||||
- `plugin-e2e-outbound`: the plugin called SDK APIs and the resulting bot message was visible on the platform.
|
||||
- `unit`: mocked converter/API coverage only.
|
||||
- `blocked`: not completed, either because the platform/simulator/client could not trigger it or because a safe disposable fixture was unavailable.
|
||||
- `not-supported`: the platform has no equivalent capability.
|
||||
|
||||
## Summary
|
||||
|
||||
| Adapter | Status | Honest acceptance summary |
|
||||
|---------|--------|---------------------------|
|
||||
| Telegram | Partial EBA acceptance | Real Telegram UI covered private text, group mention text, bot invite, inbound private image/file, outbound component sweep, safe SDK APIs, and safe Telegram platform APIs. Real UI inbound voice/quote was not completed in the latest plugin run. |
|
||||
| Discord | Partial EBA acceptance | Real Discord UI covered group text, outbound image/file/quote/mention components, safe SDK APIs, and safe Discord platform APIs. Real UI inbound attachment/image/file/reply/mention was not completed. A later UI retry was blocked because the Discord client kept the send button disabled. |
|
||||
| OneBot v11 / aiocqhttp | Partial EBA acceptance | Matcha UI covered real group text and outbound supported components/APIs. Multi-component inbound `Source/Plain/At/Face/Image/Voice/File/Quote` was verified through the real OneBot reverse WebSocket adapter endpoint, but not through Matcha UI upload/send. Matcha blocks file-send and merged-forward APIs. |
|
||||
| DingTalk | Partial EBA acceptance | Real DingTalk UI covered private text, emoji-as-text inbound, private inbound image/file, outbound image/file/quote/mention fallback components, safe SDK APIs, and safe DingTalk platform APIs. Real UI inbound voice/quote and group trigger were not completed. |
|
||||
| Lark / Feishu | Partial EBA acceptance | EBA adapter structure, self-built/store app config, WebSocket/Webhook mode handling, converters, common APIs, platform APIs, and unit tests are in place. One real LangBot organization WebSocket private text event reached `EBAEventProbe`; outbound component sweep was visible in Feishu. Latest real UI image/file sends did not reach local plugin evidence, so media receive remains blocked. |
|
||||
| WeCom | Partial EBA acceptance | Regular WeCom application-message adapter is split into the EBA directory with manifest, converters, API mixin, platform API map, and unit tests. Private text reached `EBAEventProbe` through standalone runtime and the real WeCom client; safe plugin APIs passed. Real inbound media and broader event coverage remain pending. |
|
||||
| WeComBot | Partial EBA acceptance | WeCom AI Bot is split into the EBA directory with WebSocket long connection mode and optional webhook mode, EBA message/feedback/platform-specific conversion, cache-backed common APIs, platform API map, unit tests, and a direct live probe. Private text, outbound component sweep, safe common APIs, and all declared WeComBot platform APIs reached `EBAEventProbe`; group, real inbound media, and feedback callback evidence remain pending. |
|
||||
| WeCom Customer Service | Partial EBA acceptance | WeCom Customer Service is split into the EBA directory with manifest, converters, API mixin, platform API map, unit tests, docs, and a direct live probe scaffold. Real WeChat customer-side UI text reached `EBAEventProbe`; plugin outbound text/image and safe cache-backed common APIs passed. Inbound media and platform-specific API live coverage remain pending; later fallback text sends were blocked by WeCom `95001 send msg count limit`. |
|
||||
| Official Account | Partial EBA acceptance | WeChat Official Account is split into the EBA directory with manifest, converters, cache-backed safe APIs, platform API map, unit tests, and a direct live probe scaffold. Real WeChat Official Account UI private text reached `EBAEventProbe`; safe cache-backed common APIs and declared platform APIs passed. Proactive outbound `send_message` is not supported because replies must be tied to inbound webhook windows; inbound image/voice live UI evidence remains pending. |
|
||||
|
||||
Telegram and DingTalk now have real user-side UI image/file upload evidence in plugin JSONL. Discord and aiocqhttp do not yet have real UI inbound image/file evidence.
|
||||
|
||||
## Evidence Files
|
||||
|
||||
| Adapter | Endpoint | Evidence |
|
||||
|---------|----------|----------|
|
||||
| Telegram private | Telegram Lite, `@rockchinq_bot` private chat | `data/temp/telegram-plugin-e2e-rerun.jsonl` |
|
||||
| Telegram private media | Telegram Lite, `@rockchinq_bot` private chat | `data/temp/telegram-plugin-e2e-media-ui.jsonl` |
|
||||
| Telegram group | Telegram Lite, `Rock'sBotGroup` | `data/temp/telegram-plugin-e2e-group.jsonl` |
|
||||
| Discord | Discord client, LangBot server, `#debugging` | `data/temp/discord-plugin-e2e-20260510-final.jsonl` |
|
||||
| aiocqhttp UI | local Matcha, group `test group` | `data/temp/aiocqhttp-plugin-e2e-20260510-multiformat.jsonl` |
|
||||
| aiocqhttp protocol | OneBot reverse WebSocket endpoint `127.0.0.1:2280/ws` | `data/temp/aiocqhttp-plugin-e2e-20260510-multiformat.jsonl` |
|
||||
| DingTalk | DingTalk Mac, `LangBot Team` org private chat | `data/temp/dingtalk-plugin-e2e-20260510-rerun.jsonl` |
|
||||
| DingTalk private media | DingTalk Mac, `LangBot Team` org private chat | `data/temp/dingtalk-plugin-e2e-media-ui.jsonl` |
|
||||
| Lark / Feishu unit | local mocked Feishu SDK/client paths | `tests/unit_tests/platform/test_lark_eba_adapter.py` |
|
||||
| Lark / Feishu partial live | Feishu Mac, LangBot organization `LangBotDev` private chat | `data/temp/lark-plugin-e2e-ws.jsonl` |
|
||||
| WeCom Customer Service | WeChat customer-side UI, `客服消息 -> 浪波智能客服` on `dev.rockchin.top` | `/home/wgc/LangBotxg/LangBotEbaTest/data/temp/wecomcs_eba_plugin_probe.jsonl` |
|
||||
| Official Account | WeChat desktop client, subscribed Official Account on `dev.rockchin.top` | `/home/wgc/LangBotxg/LangBotEbaTest/data/temp/officialaccount_eba_plugin_probe.jsonl` |
|
||||
|
||||
All plugin runs used SDK standalone runtime ports `5400/5401`, LangBot `--standalone-runtime`, and the real plugin at `langbot-plugin-demo/EBAEventProbe`.
|
||||
|
||||
## Unified Shape Verification
|
||||
|
||||
All four adapters deliver common SDK entities to plugins before LangBot core/plugin logic handles the event.
|
||||
|
||||
| Requirement | Telegram | Discord | aiocqhttp | DingTalk | Lark / Feishu |
|
||||
|-------------|----------|---------|-----------|----------|---------------|
|
||||
| `bot_uuid` filled | plugin-e2e | plugin-e2e | plugin-e2e | plugin-e2e | live plugin-e2e pending |
|
||||
| `adapter_name` filled | `telegram` | `discord` | `aiocqhttp` | `dingtalk` | `lark-eba` in current unit/code; older live text evidence recorded `lark` before the naming fix |
|
||||
| common `MessageChain` delivered | `Plain`, group `At + Plain`, private `Image`, private `File` | `Source + Plain` | UI `Source + Plain`; protocol `Source + Plain + At + Face + Image + Voice + File + Quote + Plain` | `Source + Plain`, private `Source + Image`, private `Source + File` | live private `Source + Plain`; unit `Source + Plain + At/Image/File`; latest live image/file blocked |
|
||||
| common user/group entities | plugin-e2e | plugin-e2e | plugin-e2e | plugin-e2e private user; group not completed | live private user; unit private/group |
|
||||
| raw native object isolation | raw data stays in `source_platform_object` | raw data stays in `source_platform_object` | raw data stays in `source_platform_object` | raw data stays in `source_platform_object` | raw data stays in `source_platform_object` |
|
||||
|
||||
## Message Receive Components
|
||||
|
||||
| Component | Telegram | Discord | aiocqhttp | DingTalk | Lark / Feishu |
|
||||
|-----------|----------|---------|-----------|----------|---------------|
|
||||
| `Source` | design gap: event has message id but chain omits `Source` | plugin-e2e-ui | plugin-e2e-ui/protocol | plugin-e2e-ui | plugin-e2e-ui private text |
|
||||
| `Plain` | plugin-e2e-ui private/group | plugin-e2e-ui | plugin-e2e-ui/protocol | plugin-e2e-ui | plugin-e2e-ui private text |
|
||||
| `At` | plugin-e2e-ui group mention | unit; real UI mention not completed in latest run | plugin-e2e-protocol; unit | unit; group trigger not completed | unit; group trigger not completed |
|
||||
| `AtAll` | not-supported | unit only | unit only | unit/send fallback only | unit only |
|
||||
| `Image` | plugin-e2e-ui private | converter/unit; real UI attachment not completed | plugin-e2e-protocol, not Matcha UI | plugin-e2e-ui private | unit; real UI image sent but not observed in plugin evidence |
|
||||
| `Voice` | converter/unit; real UI inbound not completed | not-supported as native voice; audio is attachment/file | plugin-e2e-protocol, not Matcha UI | converter/unit; real UI inbound not completed | unit; real UI inbound not completed |
|
||||
| `File` | plugin-e2e-ui private | converter/unit; real UI attachment not completed | plugin-e2e-protocol, not Matcha UI | plugin-e2e-ui private | unit; real UI file sent but not observed in plugin evidence |
|
||||
| `Quote` | converter/unit; real UI reply not completed | unit; real UI reply not completed | plugin-e2e-protocol | converter/unit; real UI quote not completed | unit/API-backed quote lookup; real UI quote not completed |
|
||||
| `Face` | not-supported as common `Face` | not-supported as common `Face` | plugin-e2e-protocol | UI emoji becomes `Plain` (`[smile]` text), not `Face` | not-supported as common `Face` |
|
||||
| `Forward` | not-supported inbound | not-supported inbound | unit; Matcha forward UI/action blocked | not-supported inbound | not-supported inbound |
|
||||
| Mixed chain | group `At + Plain`; media tested as separate messages | not completed inbound | plugin-e2e-protocol | media tested as separate messages; mixed inbound not completed | unit only |
|
||||
|
||||
## Message Send Components
|
||||
|
||||
| Component | Telegram | Discord | aiocqhttp | DingTalk | Lark / Feishu |
|
||||
|-----------|----------|---------|-----------|----------|---------------|
|
||||
| `Plain` | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound |
|
||||
| `At` | plugin-e2e-outbound equivalent | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound fallback/equivalent | plugin-e2e-outbound |
|
||||
| `AtAll` | plugin-e2e-outbound fallback | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound fallback | unit; group live not completed |
|
||||
| `Image` | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound |
|
||||
| `Voice` | not-supported in current send converter | not-supported as native voice | converter path; not completed against Matcha UI | fallback as file/text depending DingTalk media support | converter path; live not completed |
|
||||
| `File` | plugin-e2e-outbound | plugin-e2e-outbound | blocked by Matcha endpoint error | plugin-e2e-outbound | plugin-e2e-outbound |
|
||||
| `Quote` | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound fallback | plugin-e2e-outbound fallback |
|
||||
| `Face` | not-supported | not-supported | plugin-e2e-outbound attempted in mixed chain | fallback text | not-supported |
|
||||
| `Forward` | flattened fallback | flattened fallback | blocked by Matcha unsupported action | flattened fallback | plugin-e2e-outbound flattened fallback |
|
||||
| Mixed chain | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound except blocked file/forward | plugin-e2e-outbound | plugin-e2e-outbound |
|
||||
|
||||
## Event Acceptance
|
||||
|
||||
| Event category | Telegram | Discord | aiocqhttp | DingTalk |
|
||||
|----------------|----------|---------|-----------|----------|
|
||||
| `message.received` | plugin-e2e-ui | plugin-e2e-ui | plugin-e2e-ui and plugin-e2e-protocol | plugin-e2e-ui private |
|
||||
| `message.edited` | implemented/unit, not plugin-e2e-ui | historical/direct only, not latest plugin-e2e | unit | not declared |
|
||||
| `message.deleted` | implemented/unit, not plugin-e2e-ui | historical/direct only, not latest plugin-e2e | unit | not declared |
|
||||
| `message.reaction` | implemented/unit, not plugin-e2e-ui | historical/direct only, not latest plugin-e2e | not-supported in standard OneBot message path | not declared |
|
||||
| member join/left/ban | implemented/unit or blocked without disposable users | blocked without disposable users | unit; Matcha fixture unavailable | not declared |
|
||||
| bot invited/removed | invite plugin-e2e-ui for Telegram; removal blocked | invite historical/plugin-series; removal blocked | unit; Matcha fixture unavailable | not declared |
|
||||
| requests/friend events | not applicable | not applicable | unit; Matcha fixture unavailable | not declared |
|
||||
| `platform.specific` | implemented; not latest plugin-e2e | not latest plugin-e2e | adapter lifecycle observed; plugin focus was message path | declared for fallback; not reproduced in UI run |
|
||||
|
||||
## Common API Acceptance
|
||||
|
||||
| API area | Telegram | Discord | aiocqhttp | DingTalk |
|
||||
|----------|----------|---------|-----------|----------|
|
||||
| send/reply | plugin-e2e-outbound | plugin-e2e-outbound | plugin-e2e-outbound, with Matcha file/forward gaps | plugin-e2e-outbound |
|
||||
| edit/delete | historical/direct or unit; destructive/current UI not repeated | historical/direct; destructive/current UI not repeated | unit/destructive blocked | not declared or blocked |
|
||||
| message lookup | not-supported | not-supported | plugin-e2e | inbound cache-backed where available; limited live coverage |
|
||||
| group info/member info | plugin-e2e safe subset | plugin-e2e safe subset | plugin-e2e safe subset | private path only; group not completed |
|
||||
| user/friend info | plugin-e2e where platform allows | plugin-e2e where platform allows | plugin-e2e | plugin-e2e private user |
|
||||
| moderation/leave | blocked without disposable safe targets | blocked without disposable safe targets | blocked without disposable safe targets | blocked/not declared |
|
||||
| `get_file_url` | implemented; latest inbound `File` carried downloadable file data in plugin evidence | URL passthrough for attachments; inbound attachment not completed | not portable/endpoint-dependent | implemented through DingTalk media API; latest inbound `File` carried a platform file URL |
|
||||
| `call_platform_api` | plugin-e2e safe actions | plugin-e2e safe actions | plugin-e2e safe actions, Matcha gaps documented | plugin-e2e safe `check_access_token` |
|
||||
|
||||
## Platform-Specific API Acceptance
|
||||
|
||||
| Adapter | plugin-e2e verified | Blocked or not reproduced |
|
||||
|---------|---------------------|---------------------------|
|
||||
| Telegram | safe chat/admin/member count/chat-action actions | mutating actions and callback-only actions were not repeated |
|
||||
| Discord | safe channel/guild/role/typing actions | mutating pin/reaction/invite actions were not repeated in the latest plugin run; inbound attachment paths not completed |
|
||||
| aiocqhttp | safe OneBot actions such as status/version/can-send checks | `get_group_honor_info` unsupported by Matcha; admin/card/title/ban/record/file/forward require better endpoint fixtures |
|
||||
| DingTalk | `check_access_token`; real inbound file produced a file URL in the common `File` component | separate media-download replay APIs and group actions need a working follow-up fixture |
|
||||
|
||||
## SDK API Acceptance
|
||||
|
||||
`EBAEventProbe` exercised the standalone runtime path for:
|
||||
|
||||
- bot discovery and bot info lookup
|
||||
- send message
|
||||
- component sweep where enabled
|
||||
- platform API sweep where enabled
|
||||
- plugin storage
|
||||
- workspace storage
|
||||
- plugin/command/tool/knowledge-base list APIs
|
||||
|
||||
The probe logs set `ok=true` when the sweep completed with only expected unsupported/blocked items. Individual call details are stored in the JSONL evidence files.
|
||||
|
||||
## Residual Risks And Required Follow-Up
|
||||
|
||||
- Discord still requires real UI inbound image/file upload evidence before it can be called media-complete.
|
||||
- aiocqhttp has rich inbound component evidence only at the OneBot reverse WebSocket boundary; Matcha UI did not provide image/file upload coverage.
|
||||
- DingTalk group trigger remains unclosed; current evidence is private chat only.
|
||||
- Lark / Feishu requires a clean follow-up live pass: the latest LangBot organization WebSocket run connected, but UI-sent text/image/file after the loop-scheduling fix did not append plugin events.
|
||||
- Discord UI retry on May 10, 2026 was blocked by the client keeping the send button disabled even after text was entered.
|
||||
- Destructive moderation and leave APIs are intentionally blocked until disposable users/groups are available.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The EBA conversion path is implemented and partially proven for the migrated adapters. Telegram and DingTalk now have real UI private-chat image/file inbound evidence. Discord, aiocqhttp, and Lark / Feishu still have explicit UI-level media gaps, so the overall adapter set remains partial acceptance rather than production-complete media acceptance.
|
||||
162
docs/event-based-agents/adapters/aiocqhttp.md
Normal file
162
docs/event-based-agents/adapters/aiocqhttp.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# OneBot v11 / aiocqhttp EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
OneBot v11 has been migrated to the EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/aiocqhttp/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
├── types.py
|
||||
└── onebot.svg
|
||||
```
|
||||
|
||||
The EBA adapter is registered as `aiocqhttp-eba`. The legacy adapter remains at `src/langbot/pkg/platform/sources/aiocqhttp.py`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `host` | Yes | `0.0.0.0` | Host for the reverse WebSocket server that the OneBot endpoint connects to. |
|
||||
| `port` | Yes | `2280` | Reverse WebSocket listen port. |
|
||||
| `access-token` | No | `""` | OneBot access token, if the endpoint is configured to use one. |
|
||||
|
||||
## Events
|
||||
|
||||
The adapter declares these EBA events:
|
||||
|
||||
- `message.received`
|
||||
- `message.deleted`
|
||||
- `group.member_joined`
|
||||
- `group.member_left`
|
||||
- `group.member_banned`
|
||||
- `friend.request_received`
|
||||
- `friend.added`
|
||||
- `bot.invited_to_group`
|
||||
- `bot.removed_from_group`
|
||||
- `bot.muted`
|
||||
- `bot.unmuted`
|
||||
- `platform.specific`
|
||||
|
||||
`platform.specific` is used for OneBot notice/request/meta events that do not yet have a common EBA event type, such as group admin changes, group file uploads, pokes, honor changes, and group join requests from non-bot users.
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|--------|-------|
|
||||
| `send_message` | Supported | Supports private and group text, mentions, images, voice, files, faces, and flattened forwards. Group merged forwards are sent through OneBot forward APIs when possible. |
|
||||
| `reply_message` | Supported | Uses the original OneBot event and can prepend a reply segment. |
|
||||
| `edit_message` | Not supported | OneBot v11 has no standard message edit action. |
|
||||
| `delete_message` | Supported | Uses `delete_msg`; permission depends on endpoint and group role. |
|
||||
| `forward_message` | Supported | Emulates forward by fetching the source message with `get_msg` and sending its content to the target chat. |
|
||||
| `get_message` | Supported | Uses `get_msg` and converts the response into `MessageReceivedEvent`. |
|
||||
| `get_group_info` | Supported | Uses `get_group_info`. |
|
||||
| `get_group_list` | Supported | Uses `get_group_list`. |
|
||||
| `get_group_member_list` | Supported | Uses `get_group_member_list`. |
|
||||
| `get_group_member_info` | Supported | Uses `get_group_member_info`. |
|
||||
| `set_group_name` | Supported | Uses `set_group_name`; may be unsupported by mock endpoints. |
|
||||
| `get_user_info` | Supported | Uses `get_stranger_info`. |
|
||||
| `get_friend_list` | Supported | Uses `get_friend_list`. |
|
||||
| `approve_friend_request` | Supported | Uses `set_friend_add_request`. |
|
||||
| `approve_group_invite` | Supported | Uses `set_group_add_request` with `sub_type=invite`. |
|
||||
| `upload_file` | Not supported | OneBot v11 has endpoint-specific file upload extensions but no portable standalone upload action. |
|
||||
| `get_file_url` | Not supported | OneBot v11 file URL resolution is endpoint-specific. Use `call_platform_api("get_image")`, `get_record`, or endpoint extensions when available. |
|
||||
| `mute_member` | Supported | Uses `set_group_ban`. |
|
||||
| `unmute_member` | Supported | Uses `set_group_ban` with duration `0`. |
|
||||
| `kick_member` | Supported | Destructive; test only with disposable members. |
|
||||
| `leave_group` | Supported | Destructive; should run last in live tests. |
|
||||
| `call_platform_api` | Supported | See below. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
`call_platform_api(action, params)` supports:
|
||||
|
||||
- `get_login_info`
|
||||
- `get_status`
|
||||
- `get_version_info`
|
||||
- `get_group_honor_info`
|
||||
- `set_group_card`
|
||||
- `set_group_special_title`
|
||||
- `set_group_admin`
|
||||
- `set_group_whole_ban`
|
||||
- `send_group_forward_msg`
|
||||
- `get_forward_msg`
|
||||
- `get_record`
|
||||
- `get_image`
|
||||
- `can_send_image`
|
||||
- `can_send_record`
|
||||
|
||||
## Message Conversion Notes
|
||||
|
||||
Incoming OneBot segments are converted into common `MessageChain` components before LangBot core/plugin dispatch:
|
||||
|
||||
- `text` -> `Plain`
|
||||
- `at` -> `At` / `AtAll`
|
||||
- `image` -> `Image` or `Face` for OneBot emoji-package images
|
||||
- `record` -> `Voice`
|
||||
- `file` -> `File`
|
||||
- `reply` -> `Quote`
|
||||
- `face`, `rps`, `dice` -> `Face`
|
||||
- unsupported segments -> `Unknown`
|
||||
|
||||
Outgoing `MessageChain` components are converted back into `aiocqhttp.Message` segments. Base64 media strings are normalized to OneBot `base64://...` format.
|
||||
|
||||
## Live Test Record
|
||||
|
||||
The direct live probe is:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/Users/qinjunyan/code/projects/langbot/langbot-plugin-sdk/src \
|
||||
uv run python tests/e2e/live_aiocqhttp_eba_probe.py --host 127.0.0.1 --port 2280
|
||||
```
|
||||
|
||||
It starts the reverse WebSocket adapter directly, records observed EBA events to `data/temp/aiocqhttp_eba_live_probe.jsonl`, waits for a real Matcha or OneBot message, then tries reply/send/get/delete/group/user/platform API calls as far as the endpoint supports them.
|
||||
|
||||
Verified on May 10, 2026 with local Matcha connected to `ws://127.0.0.1:2280/ws`:
|
||||
|
||||
- Real inbound group message converted to `MessageReceivedEvent`.
|
||||
- Real lifecycle connection converted to `PlatformSpecificEvent`.
|
||||
- Real reply API succeeded and rendered a quoted bot reply in Matcha.
|
||||
- Real proactive send API succeeded and rendered a bot group message in Matcha.
|
||||
- Real outgoing component sweep succeeded for text, `At`, `AtAll`, `Face`, and base64 `Image`.
|
||||
- Real `get_message`, `get_group_info`, `get_login_info`, `get_status`, `get_version_info`, `can_send_image`, and `can_send_record` calls succeeded against Matcha.
|
||||
- Unit conversion and API-shape tests passed for `Plain`, `At`, `AtAll`, `Image`, `Voice`, `File`, `Quote`, `Face`, `rps`, `dice`, `Forward`, `Unknown`, private/group message events, delete notices, group join/leave/ban notices, bot mute notices, friend requests, group invites, friend added notices, dispatch specificity, send, reply, delete, forward, get message, group APIs, user APIs, request approval APIs, moderation APIs, leave group, unsupported file APIs, and all declared `call_platform_api` actions.
|
||||
|
||||
Skipped or residual live-test items:
|
||||
|
||||
- `edit_message`: not implemented because OneBot v11 has no standard edit action.
|
||||
- `upload_file` and `get_file_url`: not implemented as common APIs because portable OneBot v11 file upload/download URL semantics are endpoint-specific.
|
||||
- `kick_member` and `leave_group`: destructive; run only with explicit `--destructive` and disposable Matcha/OneBot state.
|
||||
- `group.info_updated`, message reactions, and message edits are not declared because OneBot v11 does not provide standard equivalents for them.
|
||||
- Matcha returned `ActionFailed` for outgoing `File` segment rendering and did not support merged-forward actions in this run. The adapter keeps the conversion/API implementations because they are valid OneBot/NapCat-style capabilities, but the Matcha live probe records them as skipped.
|
||||
- Matcha returned an empty `get_group_member_list` for the test group, so `get_group_member_info`, mute/unmute, kick, and leave were covered by unit/API-shape tests only in this run.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Verified on May 10, 2026 with `EBAEventProbe`, SDK standalone runtime, LangBot `--standalone-runtime`, local Matcha, and group `测试群`.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Plugin JSONL: `data/temp/aiocqhttp-plugin-e2e-20260510-multiformat.jsonl`
|
||||
|
||||
Observed and verified:
|
||||
|
||||
- A real Matcha group message reached the plugin as `MessageReceived` with `bot_uuid=eba-aiocqhttp-matcha`, `adapter_name=aiocqhttp`, common `Source`/`Plain` message components, common sender, and common group identifiers.
|
||||
- A protocol-level OneBot reverse WebSocket event reached the plugin as `MessageReceived` with a mixed common chain: `Source`, `Plain`, `At`, `Face`, `Image`, `Voice`, `File`, `Quote`, and trailing `Plain`. This proves the real adapter + LangBot + standalone runtime + plugin path for mixed inbound OneBot payloads, but it was not sent through Matcha UI.
|
||||
- SDK API calls succeeded: `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin storage, workspace storage, `list_plugins_manifest`, `list_commands`, `list_tools`, and `list_knowledge_bases`.
|
||||
- Outbound component sweep succeeded for plain text plus `At`/`Face`, `AtAll`, base64 `Image`, and quoted reply.
|
||||
- Common APIs succeeded through the plugin path: `get_message`, `get_user_info`, `get_friend_list`, `get_group_info`, `get_group_list`, `get_group_member_list`, and `get_group_member_info`.
|
||||
- Safe OneBot platform APIs succeeded through `call_platform_api`: `get_login_info`, `get_status`, `get_version_info`, `can_send_image`, and `can_send_record`.
|
||||
|
||||
Documented Matcha limits in this E2E run:
|
||||
|
||||
- Matcha UI did not provide a completed image/file upload/send path for inbound media. The rich inbound media evidence is `plugin-e2e-protocol`, not UI-level media upload evidence.
|
||||
- Outbound `File` failed in Matcha even after the adapter emitted an official `file` segment shape.
|
||||
- Outbound `Forward` failed because Matcha returned unsupported action for merged-forward.
|
||||
- `get_group_honor_info` failed because Matcha returned unsupported action.
|
||||
- Destructive/admin APIs such as mute, unmute, kick, leave, group rename, card/title/admin/whole-ban changes, and request approvals were not run without disposable fixtures.
|
||||
114
docs/event-based-agents/adapters/dingtalk.md
Normal file
114
docs/event-based-agents/adapters/dingtalk.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# DingTalk EBA Adapter Migration Record
|
||||
|
||||
Status: migrated with partial plugin E2E evidence.
|
||||
|
||||
Adapter directory: `src/langbot/pkg/platform/adapters/dingtalk/`
|
||||
|
||||
## What Changed
|
||||
|
||||
The DingTalk adapter now has an Event-Based Agents adapter package with:
|
||||
|
||||
- `manifest.yaml` for adapter metadata, configuration, events, common APIs, and platform-specific APIs.
|
||||
- `adapter.py` for DingTalk client startup, native callback handling, legacy compatibility, and EBA dispatch.
|
||||
- `event_converter.py` for native DingTalk events to common EBA events.
|
||||
- `message_converter.py` for DingTalk message payloads to/from common `MessageChain` components.
|
||||
- `api_impl.py` for common EBA API implementations.
|
||||
- `platform_api.py` for DingTalk-specific `call_platform_api` actions.
|
||||
|
||||
The legacy DingTalk HTTP client now returns successful JSON response bodies from proactive send methods and raises with response details on non-200 responses.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Notes |
|
||||
|-------|----------|-------|
|
||||
| `client-id` | yes | DingTalk robot/client identifier. |
|
||||
| `client-secret` | yes | DingTalk client secret. |
|
||||
| `robot-code` | yes | Robot code used for send APIs. |
|
||||
| `robot-name` | no | Used for bot mention/self filtering and display. |
|
||||
| `encrypt-key` | no | DingTalk callback encryption key when configured. |
|
||||
| `verification-token` | no | DingTalk callback verification token when configured. |
|
||||
|
||||
## Supported Events
|
||||
|
||||
| Event | Support | Evidence |
|
||||
|-------|---------|----------|
|
||||
| `message.received` | implemented | `plugin-e2e-ui` private text and emoji-as-text. |
|
||||
| `platform.specific` | implemented | Not reproduced in the latest UI run. |
|
||||
|
||||
## Receive Components
|
||||
|
||||
| Component | Support | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| `Source` | supported | `plugin-e2e-ui` private message. |
|
||||
| `Plain` | supported | `plugin-e2e-ui` private text. DingTalk emoji currently arrives as plain text such as `[smile]`. |
|
||||
| `At` | converter path | Group trigger was not completed in the latest run. |
|
||||
| `AtAll` | fallback/send-side only | Not completed inbound. |
|
||||
| `Image` | supported | Real DingTalk Mac private-chat image upload reached the plugin as common `Image`. |
|
||||
| `Voice` | converter path | Real UI inbound voice was not completed. |
|
||||
| `File` | supported | Real DingTalk Mac private-chat file upload reached the plugin as common `File`. |
|
||||
| `Quote` | converter path | Real UI inbound quote was not completed. |
|
||||
| `Face` | not native common mapping | DingTalk emoji was observed as `Plain`, not `Face`. |
|
||||
| `Forward` | not-supported inbound | DingTalk does not expose a portable structured forward event in this adapter. |
|
||||
|
||||
## Send Components
|
||||
|
||||
| Component | Support | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| `Plain` | supported | `plugin-e2e-outbound`. |
|
||||
| `At` | supported or text fallback | `plugin-e2e-outbound`. |
|
||||
| `AtAll` | fallback | `plugin-e2e-outbound`. |
|
||||
| `Image` | supported | `plugin-e2e-outbound`. |
|
||||
| `File` | supported | `plugin-e2e-outbound`. |
|
||||
| `Quote` | fallback | `plugin-e2e-outbound`. |
|
||||
| `Face` | fallback | `plugin-e2e-outbound` as text fallback. |
|
||||
| `Forward` | flattened fallback | `plugin-e2e-outbound`. |
|
||||
| `Voice` | fallback/endpoint-dependent | Not separately verified as a native DingTalk voice send. |
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Support | Notes |
|
||||
|-----|---------|-------|
|
||||
| `send_message` | supported | Verified through `EBAEventProbe`. |
|
||||
| `reply_message` | supported | Verified through quoted/fallback send path. |
|
||||
| `get_message` | cache-backed | Requires the message to have been observed by this adapter process. |
|
||||
| `get_group_info` | cache-backed/API-backed where available | Group path not completed in latest UI run. |
|
||||
| `get_group_list` | supported where DingTalk API allows | Limited live coverage. |
|
||||
| `get_group_member_info` | supported where DingTalk API allows | Limited live coverage. |
|
||||
| `get_user_info` | supported | Private sender path verified. |
|
||||
| `get_friend_list` | limited | DingTalk does not expose a portable friend-list equivalent. |
|
||||
| `get_file_url` | supported with media/file identifiers | Real inbound file yielded a platform file URL in the converted `File` component. |
|
||||
| `call_platform_api` | supported | Safe action `check_access_token` verified. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
| Action | Support | Evidence |
|
||||
|--------|---------|----------|
|
||||
| `check_access_token` | supported | `plugin-e2e`. |
|
||||
| `refresh_access_token` | supported | Implemented; not separately reproduced in the latest plugin run. |
|
||||
| `get_file_url` | supported | Real inbound file yielded a platform file URL in the converted `File` component. |
|
||||
| `get_audio_base64` | supported | Needs real inbound audio/media ID. |
|
||||
| `download_image_base64` | supported | Real inbound image reached the plugin as `Image`; separate image-download API replay was not completed. |
|
||||
|
||||
## End-to-End Evidence
|
||||
|
||||
Evidence files:
|
||||
|
||||
- Text/API/component JSONL: `data/temp/dingtalk-plugin-e2e-20260510-rerun.jsonl`
|
||||
- Real UI inbound media JSONL: `data/temp/dingtalk-plugin-e2e-media-ui.jsonl`
|
||||
|
||||
Verified:
|
||||
|
||||
- DingTalk Mac private chat in the `LangBot Team` organization produced `MessageReceived` through LangBot standalone runtime and `EBAEventProbe`.
|
||||
- The common chain was `Source + Plain` for normal text.
|
||||
- DingTalk emoji was received as `Source + Plain`, not common `Face`.
|
||||
- Real DingTalk Mac private-chat image upload was received as `Source + Image`.
|
||||
- Real DingTalk Mac private-chat file upload was received as `Source + File`.
|
||||
- The plugin sent outbound text, mention/fallback, image, quote/fallback, file, and forward/fallback messages visible in DingTalk.
|
||||
- The plugin called safe SDK and DingTalk platform APIs.
|
||||
|
||||
Not completed:
|
||||
|
||||
- Real UI inbound voice.
|
||||
- Real UI inbound quote.
|
||||
- Group trigger with a real robot mention.
|
||||
- Destructive or organization-mutating APIs.
|
||||
147
docs/event-based-agents/adapters/discord.md
Normal file
147
docs/event-based-agents/adapters/discord.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Discord EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
Discord has been migrated from the legacy source adapter:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/sources/discord.py
|
||||
src/langbot/pkg/platform/sources/discord.yaml
|
||||
```
|
||||
|
||||
EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/discord/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
├── types.py
|
||||
└── voice.py
|
||||
```
|
||||
|
||||
The adapter is registered as `discord-eba`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `client_id` | Yes | `""` | Discord application client ID. |
|
||||
| `token` | Yes | `""` | Discord bot token. |
|
||||
|
||||
The bot needs gateway permissions and intents for the target test server. Message Content intent is required for message bodies, Server Members intent is required for member APIs/events, and reaction events require the Reactions intent and channel permissions.
|
||||
|
||||
## Events
|
||||
|
||||
Discord declares these EBA events:
|
||||
|
||||
- `message.received`
|
||||
- `message.edited`
|
||||
- `message.deleted`
|
||||
- `message.reaction`
|
||||
- `group.member_joined`
|
||||
- `group.member_left`
|
||||
- `group.member_banned`
|
||||
- `bot.invited_to_group`
|
||||
- `bot.removed_from_group`
|
||||
- `platform.specific`
|
||||
|
||||
Discord-specific events that do not map cleanly to common events should be surfaced as `platform.specific`.
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|-----------------|-------|
|
||||
| `send_message` | Supported | Supports text, image, file, and mixed message chains through Discord messages and attachments. |
|
||||
| `reply_message` | Supported | Uses Discord message references when replying to a received EBA message event. |
|
||||
| `edit_message` | Supported | Bot can edit its own messages. File edits are implemented by clearing old attachments and sending replacement files when needed. |
|
||||
| `delete_message` | Supported | Requires message management permissions for non-bot messages. |
|
||||
| `forward_message` | Emulated | Discord has no native forward API; the adapter copies content and attachments. |
|
||||
| `get_group_info` | Supported | Maps Discord guild metadata to EBA group info. |
|
||||
| `get_group_member_list` | Supported | Requires member cache or the Server Members intent/fetch permission. |
|
||||
| `get_group_member_info` | Supported | Maps Discord roles/permissions into EBA member roles. |
|
||||
| `get_user_info` | Supported | Uses Discord user fetch/cache. |
|
||||
| `upload_file` | Not supported | Discord uploads files as message attachments; standalone upload raises `NotSupportedError`. |
|
||||
| `get_file_url` | Supported | Discord attachment URLs are already downloadable URLs, so the adapter returns the input URL. |
|
||||
| `mute_member` | Supported where possible | Uses Discord timeout API and requires guild moderation permission. |
|
||||
| `unmute_member` | Supported where possible | Clears timeout and requires guild moderation permission. |
|
||||
| `kick_member` | Supported | Destructive; test only with a disposable account/bot. |
|
||||
| `leave_group` | Supported | Bot leaves a guild; destructive and should run last. |
|
||||
| `call_platform_api` | Supported | Discord-specific actions live here. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
`call_platform_api(action, params)` supports:
|
||||
|
||||
- `get_channel`
|
||||
- `get_guild`
|
||||
- `get_guild_channels`
|
||||
- `get_guild_roles`
|
||||
- `create_invite`
|
||||
- `pin_message`
|
||||
- `unpin_message`
|
||||
- `add_reaction`
|
||||
- `remove_reaction`
|
||||
- `typing`
|
||||
|
||||
Voice helpers are intentionally kept Discord-specific:
|
||||
|
||||
- `join_voice_channel`
|
||||
- `leave_voice_channel`
|
||||
- `get_voice_connection_status`
|
||||
- `list_active_voice_connections`
|
||||
- `get_voice_channel_info`
|
||||
|
||||
## Live Test Record
|
||||
|
||||
The live probe is:
|
||||
|
||||
```bash
|
||||
uv run python tests/e2e/live_discord_eba_probe.py --help
|
||||
```
|
||||
|
||||
Verified on May 7, 2026 with a newly created Discord application/bot named `LangBot EBA Test 0507`, the LangBot Discord server, and the `#🐞-debugging` channel:
|
||||
|
||||
- SDK standalone runtime started with WebSocket control/debug ports, and the `EBAEventProbe` plugin connected through `lbp run`.
|
||||
- Plugin runtime received real Discord events through LangBot: `BotInvitedToGroup`, `MessageReceived`, `MessageReactionReceived` add/remove, `MessageEdited`, and `MessageDeleted`.
|
||||
- Plugin runtime API calls succeeded through the standalone runtime: `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin storage APIs, workspace storage APIs, `list_plugins_manifest`, `list_commands`, `list_tools`, and `list_knowledge_bases`.
|
||||
- Direct live adapter probe observed `message.received`, `message.edited`, `message.deleted`, and `bot.removed_from_group`.
|
||||
- Message APIs verified: send, reply, edit, delete, forward, text/image/file mixed message chains.
|
||||
- User and guild APIs verified: `get_user_info`, `get_group_info`, `get_group_member_list`, `get_group_member_info`.
|
||||
- Platform-specific APIs verified: `get_channel`, `get_guild`, `get_guild_channels`, `get_guild_roles`, `create_invite`, `typing`, `pin_message`, `unpin_message`, `add_reaction`, `remove_reaction`.
|
||||
- Unsupported API behavior verified: `upload_file` raises `NotSupportedError`.
|
||||
- Destructive API verified at the end: `leave_group`, which emitted `bot.removed_from_group`.
|
||||
|
||||
Not verified in the shared LangBot server live run: `mute_member`, `unmute_member`, and `kick_member`, because the run did not use a disposable target member. They are implemented through Discord timeout/kick APIs and should only be exercised against a disposable account or bot.
|
||||
|
||||
The test fixed one real test-fixture issue: `EBAEventProbe` previously assumed `get_bots()` returned UUID strings. The current standalone runtime returns bot dictionaries, so the probe now selects an enabled bot dictionary and passes its `uuid` to `get_bot_info` and `send_message`. The probe also now subscribes to `MessageDeleted`.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Verified again on May 10, 2026 with SDK standalone runtime, LangBot `--standalone-runtime`, Discord web client, the LangBot server, and `#🐞-debugging`.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Main plugin JSONL: `data/temp/discord-plugin-e2e-20260510-final.jsonl`
|
||||
- LangBot runtime log: `data/temp/discord-langbot-e2e-20260510-rerun.log`
|
||||
|
||||
Observed and verified:
|
||||
|
||||
- A newly invited Discord bot connected to the LangBot server and received a real web-client message in `#🐞-debugging`.
|
||||
- `MessageReceived` reached the plugin with `bot_uuid=eba-discord-live`, `adapter_name=discord`, common `Source`/`Plain` message components, common `User`, and common `UserGroup` for the guild.
|
||||
- SDK API calls succeeded: `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin storage, workspace storage, `list_plugins_manifest`, `list_commands`, `list_tools`, and `list_knowledge_bases`.
|
||||
- Outbound component sweep succeeded: plain text plus user mention, `AtAll`/`@everyone`, base64 image, quoted reply, file attachment, and flattened forward fallback.
|
||||
- Common APIs succeeded: `get_user_info`, `get_group_info`, `get_group_member_list`, and `get_group_member_info`.
|
||||
- Discord platform APIs succeeded through `call_platform_api`: `get_channel`, `typing`, `get_guild`, `get_guild_channels`, and `get_guild_roles`.
|
||||
|
||||
Documented limits in this E2E run:
|
||||
|
||||
- Real Discord UI inbound attachment/image/file, reply/quote, and fresh mention-chain messages were not completed in the plugin E2E evidence. Outbound image/file attachments from the bot do not prove inbound attachment conversion.
|
||||
- A later May 10 UI retry could write text into the Discord message box, but the client kept the send button disabled and did not send the message, so it produced no new plugin evidence.
|
||||
- `get_message`, `get_friend_list`, and `get_group_list` are not supported by this Discord adapter.
|
||||
- Destructive moderation and guild-leave APIs were not repeated against the shared LangBot server.
|
||||
- Native Discord voice is not represented as common `Voice`; audio-like payloads are treated as file attachments.
|
||||
- `create_invite`, pin/unpin, and reaction mutation were covered by prior direct live probes but were not repeated by the final plugin run to avoid extra shared-server side effects.
|
||||
135
docs/event-based-agents/adapters/lark.md
Normal file
135
docs/event-based-agents/adapters/lark.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Lark / Feishu EBA Adapter Migration Record
|
||||
|
||||
Status: migrated with unit coverage and partial live plugin E2E. WebSocket text reached the standalone runtime once in the LangBot organization test app, but the latest real UI image/file inbound attempts did not reach the local adapter log, so media receive is not release-complete yet.
|
||||
|
||||
Adapter directory: `src/langbot/pkg/platform/adapters/lark/`
|
||||
|
||||
## What Changed
|
||||
|
||||
The Lark/Feishu adapter now has an Event-Based Agents adapter package with:
|
||||
|
||||
- `manifest.yaml` for adapter metadata, configuration, events, common APIs, platform-specific APIs, app type, and communication mode.
|
||||
- `adapter.py` for self-built/store app token handling, WebSocket long connection startup, Webhook callback handling, card feedback, streaming-card replies, and EBA dispatch.
|
||||
- `event_converter.py` for native Feishu events to common EBA events.
|
||||
- `message_converter.py` for Feishu text/post/image/file/audio payloads to/from common `MessageChain` components.
|
||||
- `api_impl.py` for common EBA API implementations.
|
||||
- `platform_api.py` for Feishu-specific `call_platform_api` actions.
|
||||
|
||||
The legacy `lark` adapter remains available while the EBA adapter is registered separately as `lark-eba`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Notes |
|
||||
|-------|----------|-------|
|
||||
| `app_id` | yes | Feishu/Lark application App ID. |
|
||||
| `app_secret` | yes | Feishu/Lark application App Secret. |
|
||||
| `bot_name` | yes | Must match the bot name so group mentions can be recognized. |
|
||||
| `enable-webhook` | yes | `false` uses WebSocket long connection; `true` uses Request URL/Webhook callbacks. |
|
||||
| `webhook_url` | no | Generated callback URL for Webhook mode. |
|
||||
| `encrypt-key` | no | Webhook decrypt key when event encryption is enabled. |
|
||||
| `enable-stream-reply` | yes | Enables streaming replies through an updating Feishu card. |
|
||||
| `app_type` | no | `self` for self-built apps; `isv` for store apps. |
|
||||
| `bot_added_welcome` | no | Optional group welcome message sent after bot-added events. |
|
||||
|
||||
## Application And Communication Modes
|
||||
|
||||
| Mode | Support | Implementation |
|
||||
|------|---------|----------------|
|
||||
| Self-built application | implemented | Uses standard app credentials and tenant token behavior from the Feishu SDK client. |
|
||||
| Store application | implemented | Builds an ISV client, requests app tickets, and resolves app/tenant access tokens with per-tenant caching. |
|
||||
| WebSocket long connection | implemented | Registers `im.message.receive_v1` and card-action callbacks through `lark_oapi.ws.Client`. |
|
||||
| Webhook Request URL | implemented | Handles URL verification, encrypted payloads, message events, app-ticket events, bot-added events, and card-action feedback. |
|
||||
|
||||
## Supported Events
|
||||
|
||||
| Event | Support | Evidence |
|
||||
|-------|---------|----------|
|
||||
| `message.received` | implemented | Unit coverage for private and group native events to common EBA events. |
|
||||
| `bot.invited_to_group` | implemented | Webhook bot-added event maps to common bot invite event and optional welcome send. |
|
||||
| `platform.specific` | implemented | Unknown callback events are preserved as `platform.specific`. |
|
||||
| `FeedbackEvent` | compatibility event | Card button feedback is still dispatched through the existing SDK `FeedbackEvent` type. |
|
||||
|
||||
## Receive Components
|
||||
|
||||
| Component | Support | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| `Source` | supported | Unit coverage; live private text evidence. |
|
||||
| `Plain` | supported | Text and post payloads convert to common text; live private text evidence. |
|
||||
| `At` | supported | Feishu mentions map to common `At` with user ID and display name. |
|
||||
| `AtAll` | supported | `user_id=all` maps to common `AtAll`. |
|
||||
| `Image` | supported | Image payloads download through message resource API and map to common `Image`; real UI image send attempted, but not observed in local plugin evidence yet. |
|
||||
| `Voice` | supported | Audio payloads download through message resource API and map to common `Voice`. |
|
||||
| `File` | supported | File payloads download through message resource API and map to common `File`; real UI file send attempted, but not observed in local plugin evidence yet. |
|
||||
| `Quote` | supported | Parent/thread reply lookup maps quoted content into common `Quote`. |
|
||||
| `Face` | not native common mapping | Feishu emoji/stickers are not exposed as a portable common `Face` component here. |
|
||||
| `Forward` | not-supported inbound | Feishu does not expose a portable structured forward event in this adapter. |
|
||||
|
||||
## Send Components
|
||||
|
||||
| Component | Support | Evidence |
|
||||
|-----------|---------|----------|
|
||||
| `Plain` | supported | Unit coverage; sends Feishu `text`. |
|
||||
| `At` | supported | Unit coverage; sends Feishu `post` at element. |
|
||||
| `AtAll` | supported | Unit coverage; sends Feishu `post` at-all element. |
|
||||
| `Image` | supported | Uploads image resource and sends Feishu `image`. |
|
||||
| `Voice` | supported | Uploads OPUS/audio resource and sends Feishu `audio`. |
|
||||
| `File` | supported | Uploads file resource and sends Feishu `file`. |
|
||||
| `Quote` | supported/fallback | Sends quote marker plus origin content. |
|
||||
| `Face` | not-supported | No portable send mapping. |
|
||||
| `Forward` | flattened fallback | Flattens forward nodes into text/media messages. |
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Support | Notes |
|
||||
|-----|---------|-------|
|
||||
| `send_message` | supported | Supports private/open_id and group/chat_id targets; live plugin outbound component sweep produced visible Feishu messages. |
|
||||
| `reply_message` | supported | Replies to the source Feishu message; fixed to recover the native Feishu message ID from legacy-wrapped source events. |
|
||||
| `get_message` | cache-backed/API-backed | Returns cached inbound event where possible and converts uncached Feishu message API items into common `MessageReceivedEvent`. |
|
||||
| `get_group_info` | supported | Uses cached group or Feishu chat metadata. |
|
||||
| `get_group_member_info` | limited | Uses cached user data when available. |
|
||||
| `get_user_info` | limited | Uses cached user data when available. |
|
||||
| `get_file_url` | limited | Returns `file://` paths from downloaded inbound resources; remote Feishu resource download uses platform-specific API params. |
|
||||
| `call_platform_api` | supported | See below. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
| Action | Support | Evidence |
|
||||
|--------|---------|----------|
|
||||
| `check_tenant_access_token` | supported | Unit coverage. |
|
||||
| `refresh_app_access_token` | supported | Store-app token path implemented. |
|
||||
| `refresh_tenant_access_token` | supported | Store-app tenant token path implemented. |
|
||||
| `get_chat` | supported | Feishu chat metadata API wrapper. |
|
||||
| `get_message` | supported | Feishu message API wrapper with JSON-safe return values for plugin calls. |
|
||||
| `get_message_resource` | supported | Feishu message resource download wrapper. |
|
||||
|
||||
## End-to-End Evidence
|
||||
|
||||
Current code-level evidence:
|
||||
|
||||
- `tests/unit_tests/platform/test_lark_eba_adapter.py`
|
||||
- `PYTHONPATH=../langbot-plugin-sdk/src uv run pytest tests/unit_tests/platform/test_lark_eba_adapter.py -q`
|
||||
|
||||
Live evidence collected on May 11, 2026:
|
||||
|
||||
- Standalone runtime: `uv run lbp rt --ws-control-port 5400 --ws-debug-port 5401 --skip-deps-check`
|
||||
- LangBot: `uv run main.py --standalone-runtime --debug`
|
||||
- Plugin: `LangBot__EBAEventProbe`
|
||||
- Feishu org/app: LangBot organization, `LangBotDev` private chat.
|
||||
- Observed plugin JSONL: one private `MessageReceived` event with `Source + Plain`; plugin API probe then exercised bot discovery, bot info, `send_message`, outbound component sweep, storage/list APIs, and safe platform API calls.
|
||||
- Real UI sends attempted after the fixes: private text, local file, and image/video image upload. These appeared in the Feishu client but did not append new `EBAEventProbe` records in the local JSONL during this run.
|
||||
- Fixes from live testing: reply path now extracts the native Feishu `message_id` from legacy-wrapped source events; WebSocket callbacks are scheduled onto the adapter event loop instead of assuming the SDK callback has a running asyncio loop; platform API results are converted to JSON-safe values.
|
||||
|
||||
Live E2E items still required before marking release-complete:
|
||||
|
||||
- WebSocket self-built app in LangBot organization: repeat private text after callback-loop fix, plus private image/file/audio and group mention message received by `EBAEventProbe`.
|
||||
- Webhook self-built app in LangBot organization: URL verification plus text/image/file message received by `EBAEventProbe`.
|
||||
- Store app token path: at least token acquisition/tenant-token safe API through `call_platform_api`; full message E2E if a LangBot organization store-app fixture is available.
|
||||
- Outbound component sweep: text, mention, at-all, image, file, voice where Feishu accepts the fixture, quote/fallback, and forward/fallback.
|
||||
- Safe platform API sweep: token check, chat metadata, message lookup, and message resource download using real inbound IDs.
|
||||
|
||||
## Known Limits
|
||||
|
||||
- Store-app live E2E requires a real ISV app ticket/tenant installation fixture.
|
||||
- Current LangBot organization WebSocket run connected successfully but did not deliver the latest UI-sent image/file attempts to local plugin evidence; this blocks release-complete media acceptance.
|
||||
- Feishu native emoji/sticker semantics are not represented as common `Face`.
|
||||
- Destructive org or chat mutations are not declared in this adapter.
|
||||
101
docs/event-based-agents/adapters/officialaccount.md
Normal file
101
docs/event-based-agents/adapters/officialaccount.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# OfficialAccount EBA Adapter
|
||||
|
||||
Adapter directory: `src/langbot/pkg/platform/adapters/officialaccount/`
|
||||
|
||||
Manifest name: `officialaccount-eba`
|
||||
|
||||
Status: partial migration. Unit/API-shape coverage is present, and private text `plugin-e2e-ui` plus safe API evidence has been verified against the `dev.rockchin.top` Official Account fixture. Proactive outbound `send_message` remains not supported by this adapter because WeChat Official Account replies must be tied to inbound webhook windows.
|
||||
|
||||
## Config
|
||||
|
||||
| Field | Required | Notes |
|
||||
| --- | --- | --- |
|
||||
| `webhook_url` | no | Generated by LangBot and copied into the Official Account callback settings. |
|
||||
| `token` | yes | WeChat callback token. |
|
||||
| `EncodingAESKey` | yes | WeChat message encryption key. |
|
||||
| `AppID` | yes | Official Account app ID. |
|
||||
| `AppSecret` | yes | Official Account app secret. |
|
||||
| `Mode` | yes | `drop` waits for an in-callback reply; `passive` returns the loading text first and queues the answer for the user's next message. |
|
||||
| `LoadingMessage` | no | Only used by `passive` mode. |
|
||||
| `api_base_url` | no | Optional API base URL for proxy deployments. |
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Evidence | Notes |
|
||||
| --- | --- | --- |
|
||||
| `message.received` | plugin-e2e-ui, unit | Text UI message verified through WeChat Official Account on `dev.rockchin.top`; image and voice webhook payloads are covered by unit tests. |
|
||||
| `platform.specific` | unit | Subscribe/menu/etc. native events are emitted as structured `PlatformSpecificEvent`. |
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Evidence | Notes |
|
||||
| --- | --- | --- |
|
||||
| `reply_message` | unit | Queues/passively returns text through the inbound webhook source event. |
|
||||
| `get_message` | plugin-e2e-ui, unit | Cached inbound message retrieved by `EBAEventProbe` platform API sweep. |
|
||||
| `get_user_info` | plugin-e2e-ui, unit | Cached inbound sender retrieved by `EBAEventProbe` platform API sweep. |
|
||||
| `get_friend_list` | plugin-e2e-ui, unit | Cached inbound sender list retrieved by `EBAEventProbe` platform API sweep. |
|
||||
| `call_platform_api` | plugin-e2e-ui, unit | Safe diagnostic actions verified through `get_mode` and `get_cached_response_status`. |
|
||||
| `send_message` | not-supported | Official Account customer-service proactive messaging is not implemented by the existing SDK adapter; only webhook reply is supported here. |
|
||||
|
||||
## Platform APIs
|
||||
|
||||
| Action | Evidence | Notes |
|
||||
| --- | --- | --- |
|
||||
| `get_mode` | plugin-e2e-ui, unit | Returned `{"mode": "drop", "longer_response": false}` in live probe. |
|
||||
| `get_cached_response_status` | plugin-e2e-ui, unit | Returned `{"pending": false}` in live probe. |
|
||||
|
||||
## Components
|
||||
|
||||
| Receive Component | Evidence | Notes |
|
||||
| --- | --- | --- |
|
||||
| `Source` | plugin-e2e-ui, unit | Uses `MsgId` and `CreateTime`; live UI text message included `Source`. |
|
||||
| `Plain` | plugin-e2e-ui, unit | Live UI text message mapped to `Plain`. |
|
||||
| `Image` | unit | `PicUrl` and `MediaId` map to common `Image`. |
|
||||
| `Voice` | unit | `MediaId` maps to common `Voice`. |
|
||||
| `Unknown` | unit | Unsupported message/event types do not crash. |
|
||||
| `At`, `AtAll`, `File`, `Quote`, `Face`, `Forward`, mixed chain | not-supported | WeChat Official Account inbound webhook payloads used by the current SDK do not expose these as common structured components. |
|
||||
|
||||
| Send Component | Evidence | Notes |
|
||||
| --- | --- | --- |
|
||||
| `Plain` | unit | Sent as webhook reply text. |
|
||||
| `Image`, `Voice`, `File`, `Quote`, `At`, `AtAll`, `Face`, `Forward`, mixed chain | not-supported | Existing SDK reply path is text XML only; non-text components degrade to readable placeholders in tests and are not declared as supported outbound components. |
|
||||
|
||||
## Verification Record
|
||||
|
||||
Test date: 2026-05-28
|
||||
|
||||
Endpoint/simulator: `dev.rockchin.top` with WeChat desktop client and a real subscribed Official Account conversation. The running EBA test stack used SDK standalone runtime ports `5400/5401`, LangBot from `/home/wgc/LangBotxg/LangBotEbaTest`, and `EBAEventProbe`.
|
||||
|
||||
Verified UI message: `EBA officialaccount single probe 2026-05-28 16:53`
|
||||
|
||||
Observed event/API evidence:
|
||||
|
||||
- `MessageReceived`: `bot_uuid=d7c46880-a9f8-431a-9172-5d3e0d663dbc`, `adapter_name=officialaccount-eba`, `chat_type=private`, `chat_id=ovH9L7OW6hNpWZWvp_NMmypVh26w`, `message_chain=[Source, Plain]`.
|
||||
- Common safe APIs through probe platform sweep: `get_message`, `get_user_info`, `get_friend_list`.
|
||||
- Platform APIs through `call_platform_api`: `get_mode`, `get_cached_response_status`.
|
||||
- `send_message` and outbound component sweep returned explicit `NotSupportedError: send_message:official_account_requires_inbound_webhook_reply`, as expected for this adapter.
|
||||
|
||||
Standalone runtime command:
|
||||
|
||||
```bash
|
||||
cd langbot-plugin-sdk
|
||||
uv run python -m langbot_plugin.cli.__init__ rt --debug-only --ws-control-port 5400 --ws-debug-port 5401 --skip-deps-check
|
||||
```
|
||||
|
||||
Probe plugin: `data/plugins/LangBot__EBAEventProbe` when live credentials are available.
|
||||
|
||||
Adapter live probe:
|
||||
|
||||
```bash
|
||||
uv run python -m py_compile tests/e2e/live_officialaccount_eba_probe.py
|
||||
OFFICIALACCOUNT_TOKEN=... OFFICIALACCOUNT_ENCODING_AES_KEY=... OFFICIALACCOUNT_APP_SECRET=... OFFICIALACCOUNT_APP_ID=... uv run python tests/e2e/live_officialaccount_eba_probe.py
|
||||
```
|
||||
|
||||
Evidence JSONL path: `/home/wgc/LangBotxg/LangBotEbaTest/data/temp/officialaccount_eba_plugin_probe.jsonl` for plugin E2E, or `data/temp/officialaccount_eba_probe.jsonl` for direct adapter live probe.
|
||||
|
||||
Destructive operations: none.
|
||||
|
||||
Blocked items:
|
||||
|
||||
- `plugin-e2e-outbound`: proactive `send_message` is not supported for this adapter; Official Account responses must be produced through the inbound webhook reply window.
|
||||
- Inbound image and voice live UI evidence remains pending; webhook conversion is covered by unit tests.
|
||||
139
docs/event-based-agents/adapters/telegram.md
Normal file
139
docs/event-based-agents/adapters/telegram.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Telegram EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
Telegram has been migrated to the EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/telegram/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
└── types.py
|
||||
```
|
||||
|
||||
The adapter is registered as `telegram-eba`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `token` | Yes | `""` | Telegram Bot API token from BotFather. |
|
||||
| `markdown_card` | No | `true` | Whether to render Markdown card style replies. |
|
||||
| `enable-stream-reply` | Yes | `false` | Whether to use Telegram streaming reply mode. |
|
||||
|
||||
## Events
|
||||
|
||||
Telegram declares these EBA events:
|
||||
|
||||
- `message.received`
|
||||
- `message.edited`
|
||||
- `message.reaction`
|
||||
- `group.member_joined`
|
||||
- `group.member_left`
|
||||
- `group.member_banned`
|
||||
- `bot.invited_to_group`
|
||||
- `bot.removed_from_group`
|
||||
- `bot.muted`
|
||||
- `bot.unmuted`
|
||||
- `platform.specific`
|
||||
|
||||
`platform.specific` is currently used for Telegram-only callback and chat-member update payloads that do not yet have a more specific common event type.
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|--------|-------|
|
||||
| `send_message` | Supported | Supports text, image, file, and mixed message chains. |
|
||||
| `reply_message` | Supported | Supports quoted replies through the original message event. |
|
||||
| `edit_message` | Supported | Uses Telegram message editing APIs. |
|
||||
| `delete_message` | Supported | Deletes messages where bot permissions allow it. |
|
||||
| `forward_message` | Supported | Forwards a message between Telegram chats. |
|
||||
| `get_group_info` | Supported | Uses Telegram chat metadata. |
|
||||
| `get_group_member_list` | Supported | Telegram only exposes administrators through the Bot API; this returns the available member set. |
|
||||
| `get_group_member_info` | Supported | Maps Telegram member status to EBA member roles. |
|
||||
| `get_user_info` | Supported | Uses Telegram `get_chat` for user chat metadata. |
|
||||
| `upload_file` | Not supported | Telegram has no standalone upload endpoint; files are uploaded as part of messages. The adapter raises `NotSupportedError`. |
|
||||
| `get_file_url` | Supported | Returns the Bot API file URL. Test output redacts the bot token. |
|
||||
| `mute_member` | Supported | Requires a supergroup and bot moderation permission. |
|
||||
| `unmute_member` | Supported | Uses current `telegram.ChatPermissions` fields. |
|
||||
| `kick_member` | Supported | Destructive; should only be run against disposable users/bots in tests. |
|
||||
| `leave_group` | Supported | Destructive; should run at the end of a live test. |
|
||||
| `call_platform_api` | Supported | See below. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
`call_platform_api(action, params)` supports:
|
||||
|
||||
- `pin_message`
|
||||
- `unpin_message`
|
||||
- `unpin_all_messages`
|
||||
- `get_chat_administrators`
|
||||
- `set_chat_title`
|
||||
- `set_chat_description`
|
||||
- `get_chat_member_count`
|
||||
- `send_chat_action`
|
||||
- `create_chat_invite_link`
|
||||
- `answer_callback_query`
|
||||
|
||||
## Live Test Record
|
||||
|
||||
The live probe is:
|
||||
|
||||
```bash
|
||||
uv run python tests/e2e/live_telegram_eba_probe.py --help
|
||||
```
|
||||
|
||||
It supports private chat tests, group/supergroup tests, moderation tests, destructive tests, and a callback-only mode.
|
||||
|
||||
Verified on May 7, 2026:
|
||||
|
||||
- Private chat message APIs: send, reply, edit, delete, forward.
|
||||
- Private chat media APIs: image/file sending and `get_file_url`.
|
||||
- User API: `get_user_info`.
|
||||
- Supergroup APIs: group info, member list, member info, administrators, member count, invite link.
|
||||
- Supergroup mutation APIs: pin, unpin, unpin all, set title, restore title, set description, restore description.
|
||||
- Moderation APIs: mute and unmute against a non-owner target bot.
|
||||
- Destructive APIs: kick a disposable target bot, then make the test bot leave the test group.
|
||||
- Event conversion observed for `message.received`, `group.member_banned`, `group.member_left`, `bot.removed_from_group`, and Telegram-specific chat-member updates.
|
||||
|
||||
The test fixed one real compatibility issue: `unmute_member` previously used Telegram's removed `can_send_media_messages` permission field. It now uses the split media permission fields required by current `python-telegram-bot`.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Verified on May 10, 2026 with `EBAEventProbe`, SDK standalone runtime, Telegram Lite, `@rockchinq_bot`, and `Rock'sBotGroup`.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Private chat JSONL: `data/temp/telegram-plugin-e2e-rerun.jsonl`
|
||||
- Group chat JSONL: `data/temp/telegram-plugin-e2e-group.jsonl`
|
||||
- Private media JSONL: `data/temp/telegram-plugin-e2e-media-ui.jsonl`
|
||||
|
||||
Observed and verified:
|
||||
|
||||
- `MessageReceived` reached the plugin with `bot_uuid=eba-telegram-live`, `adapter_name=telegram`, common sender/chat fields, and common `MessageChain` content.
|
||||
- `BotInvitedToGroup` reached the plugin after adding the bot to `Rock'sBotGroup`.
|
||||
- SDK API calls succeeded: `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin storage, workspace storage, `list_plugins_manifest`, `list_commands`, `list_tools`, and `list_knowledge_bases`.
|
||||
- Outbound component sweep succeeded in private and group chats: plain text, mention text/equivalent, base64 image, quoted reply, file/document, and flattened forward fallback. Group mode also covered `AtAll` fallback behavior.
|
||||
- Real Telegram Lite private-chat inbound media was verified through the plugin path: a sent document arrived as common `File`, and a sent photo arrived as common `Image`.
|
||||
- Telegram platform API sweep succeeded for safe group actions: `get_chat_administrators`, `get_chat_member_count`, and `send_chat_action`.
|
||||
- Common group/user APIs succeeded in group mode: `get_user_info`, `get_group_info`, `get_group_member_list`, and `get_group_member_info`.
|
||||
|
||||
Documented limits in this E2E run:
|
||||
|
||||
- Real Telegram UI inbound voice, sticker/emoji-as-common-component, and reply/quote messages were not completed in the plugin E2E evidence.
|
||||
- `get_message`, `get_friend_list`, and `get_group_list` are not supported by this Telegram adapter.
|
||||
- Mutating/destructive Telegram-specific actions such as pin/unpin, title/description changes, invite-link creation, moderation, kick, and leave were not repeated in the plugin run. They remain opt-in live-probe cases.
|
||||
- Telegram does not expose a portable common `Face` component for native sticker/emoji semantics in the current adapter.
|
||||
|
||||
## Notes for Future Adapters
|
||||
|
||||
Telegram is the reference implementation for:
|
||||
|
||||
- Keeping platform-specific actions behind `call_platform_api`.
|
||||
- Treating unsupported common APIs as explicit `NotSupportedError`.
|
||||
- Marking destructive live test operations behind CLI flags.
|
||||
- Redacting access tokens from live probe output.
|
||||
130
docs/event-based-agents/adapters/wecom.md
Normal file
130
docs/event-based-agents/adapters/wecom.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# WeCom EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
WeCom application messages now have an EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/wecom/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
└── types.py
|
||||
```
|
||||
|
||||
The adapter is registered as `wecom-eba`.
|
||||
|
||||
This record covers the regular WeCom application-message adapter. WeCom AI Bot (`wecombot-eba`) uses a different protocol flow and is documented separately in `wecombot.md`. WeCom Customer Service (`wecomcs`) remains a separate follow-up migration.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `webhook_url` | No | `""` | Unified webhook URL copied into the WeCom application callback settings. |
|
||||
| `corpid` | Yes | `""` | WeCom corporate ID. |
|
||||
| `secret` | Yes | `""` | WeCom application secret. |
|
||||
| `token` | Yes | `""` | WeCom callback token. |
|
||||
| `EncodingAESKey` | Yes | `""` | WeCom callback encryption key. |
|
||||
| `contacts_secret` | No | `""` | Contacts secret for contact-list based helper APIs. |
|
||||
| `api_base_url` | No | `https://qyapi.weixin.qq.com/cgi-bin` | WeCom API base URL, overrideable for proxy/private-network deployments. |
|
||||
|
||||
## Events
|
||||
|
||||
WeCom declares these EBA events:
|
||||
|
||||
- `message.received`
|
||||
- `platform.specific`
|
||||
|
||||
`message.received` currently covers text and image application callbacks. Other WeCom callback types are surfaced as `platform.specific` so plugins can inspect the raw structured payload without crashing the common message path.
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|--------|-------|
|
||||
| `send_message` | Supported | Private/person target only. `target_id` must be `user_id|agent_id`. Supports text, image, voice, file, flattened forward, and quote fallback. |
|
||||
| `reply_message` | Supported | Replies to the original WeCom sender and application agent from `source_platform_object`. |
|
||||
| `get_message` | Supported from cache | Returns cached inbound `MessageReceivedEvent` by message ID. |
|
||||
| `get_user_info` | Supported | Uses cached event users first, then WeCom `user/get`. |
|
||||
| `get_friend_list` | Partial | Returns users seen by this adapter instance. Full contacts listing is not declared as common coverage. |
|
||||
| `call_platform_api` | Supported | See below. |
|
||||
| `edit_message` | Not supported | WeCom application messages do not expose a general edit endpoint for sent messages. |
|
||||
| `delete_message` | Not supported | WeCom application messages do not expose a general delete endpoint for sent messages. |
|
||||
| `get_group_info` / member APIs | Not supported | Regular WeCom application callbacks handled here are private user messages, not group-chat bot messages. |
|
||||
| `upload_file` / `get_file_url` | Not supported as common APIs | WeCom media upload is used internally while sending image/voice/file components; no portable standalone common file URL is exposed. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
`call_platform_api(action, params)` supports:
|
||||
|
||||
- `check_access_token`
|
||||
- `refresh_access_token`
|
||||
- `get_user_info`
|
||||
- `send_to_all`
|
||||
|
||||
`send_to_all` requires a configured `contacts_secret` with suitable contact visibility and should be treated as a broad-send operation in live testing.
|
||||
|
||||
## Unit Verification
|
||||
|
||||
Covered by:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit_tests/platform/test_wecom_eba_adapter.py
|
||||
```
|
||||
|
||||
The unit tests cover:
|
||||
|
||||
- Manifest events/APIs/platform actions match adapter declarations.
|
||||
- Outbound component conversion for text, image, voice, file, quote fallback, and byte-safe text splitting.
|
||||
- Text callback conversion to `MessageReceivedEvent`.
|
||||
- Legacy `FriendMessage` compatibility.
|
||||
- EBA listener dispatch and inbound message/user cache.
|
||||
- `send_message`, `reply_message`, and safe platform API dispatch against a mocked WeCom client.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Verified on May 27, 2026 with `EBAEventProbe`, SDK standalone runtime, LangBot core, and a real WeCom desktop client against the server test environment.
|
||||
|
||||
```bash
|
||||
cd langbot-plugin-sdk
|
||||
uv run python -m langbot_plugin.cli.__init__ rt --debug-only --ws-control-port 5400 --ws-debug-port 5401 --skip-deps-check
|
||||
|
||||
cd LangBot
|
||||
uv run main.py --standalone-runtime
|
||||
|
||||
cd data/plugins/LangBot__EBAEventProbe
|
||||
EBA_PROBE_API=1 EBA_PROBE_COMPONENT_SWEEP=1 EBA_PROBE_PLATFORM_API=1 \
|
||||
uv --project /absolute/path/to/langbot-plugin-sdk run python -m langbot_plugin.cli.__init__ run
|
||||
```
|
||||
|
||||
Evidence:
|
||||
|
||||
- JSONL: `data/temp/wecom_eba_plugin_probe.jsonl`
|
||||
- Bot: `wecom-eba`
|
||||
- Client: real WeCom desktop client
|
||||
- Environment: `dev.rockchin.top` test server
|
||||
|
||||
Observed and verified:
|
||||
|
||||
- A real private WeCom user message reached the plugin as `MessageReceived` with `adapter_name=wecom-eba`, common sender/chat fields, and `Source + Plain`.
|
||||
- SDK API calls succeeded through the standalone runtime, including `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin/workspace storage, and manifest/list APIs.
|
||||
- Safe adapter API checks succeeded through the plugin path for cached message/user data and declared safe platform API actions.
|
||||
|
||||
Still required for stricter acceptance:
|
||||
|
||||
- Send a private image and confirm common `Image` reaches the plugin.
|
||||
- Have the plugin call `send_message` and `reply_message` for text and one media component, then verify the WeCom client receives the bot output.
|
||||
- Exercise `send_to_all` only with a disposable visible-contact scope.
|
||||
- Trigger one non-text/image callback, if available, and confirm it becomes `PlatformSpecificEventReceived`.
|
||||
|
||||
## Current Acceptance
|
||||
|
||||
Current status is **partial EBA acceptance**.
|
||||
|
||||
Blocked items:
|
||||
|
||||
- Real inbound image/voice/file evidence was not completed in this run.
|
||||
- Inbound voice/file callback parsing is not present in the legacy `WecomClient.get_message()` path, so the EBA adapter does not claim those receive components yet.
|
||||
- Group/member/moderation APIs do not apply to this regular WeCom application-message adapter.
|
||||
148
docs/event-based-agents/adapters/wecombot.md
Normal file
148
docs/event-based-agents/adapters/wecombot.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# WeComBot EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
WeCom AI Bot now has an EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/wecombot/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
└── types.py
|
||||
```
|
||||
|
||||
The adapter is registered as `wecombot-eba`.
|
||||
|
||||
This is separate from regular WeCom internal applications (`wecom-eba`). WeComBot supports WebSocket long connection mode, which does not require a webhook URL. Webhook mode remains available when `enable-webhook=true`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `BotId` | Yes for WebSocket mode | `""` | WeCom AI Bot ID. |
|
||||
| `robot_name` | Yes | `""` | Bot display name used to strip bot mentions from incoming group text. |
|
||||
| `enable-webhook` | Yes | `false` | `false` uses WebSocket long connection mode; `true` uses webhook callback mode. |
|
||||
| `webhook_url` | No | `""` | Unified webhook URL, only needed when webhook mode is enabled. |
|
||||
| `Secret` | Yes for WebSocket mode | `""` | WeCom AI Bot secret for long connection mode. |
|
||||
| `Corpid` | Yes for webhook mode | `""` | WeCom corporate ID for webhook callback mode. |
|
||||
| `Token` | Yes for webhook mode | `""` | WeCom callback token. |
|
||||
| `EncodingAESKey` | Yes for webhook mode; optional for WebSocket media decrypt | `""` | Message encryption/decryption key. |
|
||||
| `enable-stream-reply` | No | `true` | Enables WeComBot streaming replies. |
|
||||
|
||||
## Events
|
||||
|
||||
WeComBot declares these EBA events:
|
||||
|
||||
- `message.received`
|
||||
- `feedback.received`
|
||||
- `platform.specific`
|
||||
|
||||
`message.received` covers private and group messages from the WeComBot SDK. `feedback.received` covers WeComBot like/dislike feedback callbacks. Native SDK events without a common EBA equivalent are emitted as `platform.specific`.
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|--------|-------|
|
||||
| `send_message` | Supported in WebSocket mode | Sends proactive markdown/text to a person or group chat ID. Webhook mode raises `NotSupportedError` because the platform callback flow has no proactive send path here. |
|
||||
| `reply_message` | Supported | Replies through native `req_id` in WebSocket mode or stream finalization/cache in webhook mode. |
|
||||
| `get_message` | Supported from cache | Returns cached inbound `MessageReceivedEvent` by message ID. |
|
||||
| `get_user_info` | Supported from cache | WeComBot events carry user info; no full user lookup endpoint is declared. |
|
||||
| `get_friend_list` | Partial | Returns users observed by this adapter instance. |
|
||||
| `get_group_info` | Supported from cache | Returns groups observed from inbound group messages. |
|
||||
| `get_group_member_info` | Supported from cache | Returns observed sender/group-member pairs. |
|
||||
| `get_group_member_list` | Partial | Returns observed members for the cached group only. |
|
||||
| `call_platform_api` | Supported | See below. |
|
||||
| `edit_message` / `delete_message` / `forward_message` | Not supported | WeComBot does not expose portable common APIs for these operations in the current SDK wrapper. |
|
||||
| `upload_file` / `get_file_url` | Not supported as common APIs | Media is represented inside messages; no portable standalone file upload/URL API is declared. |
|
||||
| moderation / leave APIs | Not supported | WeComBot does not expose equivalent common moderation operations through this adapter. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
`call_platform_api(action, params)` supports:
|
||||
|
||||
- `is_websocket_mode`
|
||||
- `get_stream_session_status`
|
||||
- `send_markdown`
|
||||
|
||||
`send_markdown` is only available in WebSocket mode.
|
||||
|
||||
## Unit Verification
|
||||
|
||||
Covered by:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/Users/wangqiang/code/python/langbot-plugin-sdk/src uv run pytest tests/unit_tests/platform/test_wecombot_eba_adapter.py
|
||||
```
|
||||
|
||||
The unit tests cover:
|
||||
|
||||
- Manifest events/APIs/platform actions match adapter declarations.
|
||||
- Outbound common components flatten to WeComBot markdown/text.
|
||||
- Private and group native events become `MessageReceivedEvent`.
|
||||
- Inbound image, file, voice, and quote components map to common `MessageChain`.
|
||||
- Legacy `FriendMessage`/`GroupMessage` compatibility.
|
||||
- EBA listener dispatch, message/user/group/member cache, reply, send, streaming chunk, feedback, and platform API calls.
|
||||
|
||||
## Live Probe
|
||||
|
||||
The direct adapter probe is:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/absolute/path/to/langbot-plugin-sdk/src uv run python tests/e2e/live_wecombot_eba_probe.py --help
|
||||
```
|
||||
|
||||
Default mode is WebSocket long connection and requires:
|
||||
|
||||
- `WECOMBOT_BOT_ID`
|
||||
- `WECOMBOT_SECRET`
|
||||
- `WECOMBOT_ROBOT_NAME`
|
||||
- optional `WECOMBOT_ENCODING_AES_KEY`
|
||||
|
||||
Webhook mode uses `--webhook` and requires:
|
||||
|
||||
- `WECOMBOT_TOKEN`
|
||||
- `WECOMBOT_ENCODING_AES_KEY`
|
||||
- `WECOMBOT_CORPID`
|
||||
|
||||
The probe writes JSONL evidence to `data/temp/wecombot_eba_live_probe.jsonl`, waits for a real WeComBot message, records common EBA event fields and message components, then runs safe cached/common/platform API checks.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Verified on May 27, 2026 with `EBAEventProbe`, SDK standalone runtime, LangBot core, and the real WeCom desktop client in a WeCom AI Bot private chat.
|
||||
|
||||
Evidence:
|
||||
|
||||
- JSONL: `data/temp/wecombot_eba_plugin_probe.jsonl`
|
||||
- Bot UUID: `9f5d4125-7b6d-4c98-8ca2-111111111111`
|
||||
- Adapter: `wecombot-eba`
|
||||
- Client: real WeCom desktop client, private `LangBot` BOT chat
|
||||
- Mode: WebSocket long connection (`enable-webhook=false`)
|
||||
|
||||
Observed and verified:
|
||||
|
||||
- A real user-side message reached the plugin as `MessageReceived` with `adapter_name=wecombot-eba`, common sender/chat fields, and `Source + Plain`.
|
||||
- SDK API calls succeeded through the standalone runtime: `get_langbot_version`, `get_bots`, `get_bot_info`, `send_message`, plugin/workspace storage, manifest/list APIs, and safe cached common platform APIs.
|
||||
- Outbound component sweep was visible in the WeCom client and returned `errcode=0`: plain/mention/face fallback, base64 image marker, quote fallback, file marker, and flattened forward fallback.
|
||||
- Declared WeComBot platform APIs succeeded through `plugin.call_platform_api`: `is_websocket_mode`, `get_stream_session_status`, and `send_markdown`.
|
||||
- The `send_markdown` platform API produced visible bot output in the WeCom client.
|
||||
|
||||
Not completed:
|
||||
|
||||
- Clicking the visible WeCom AI feedback button did not produce a `FeedbackReceived` JSONL entry in this run, so `feedback.received` remains unverified at plugin E2E level.
|
||||
- Group chat inbound and group cache/member coverage still need a real group-side trigger.
|
||||
- Real inbound image/file/voice from the WeCom client was not exercised.
|
||||
|
||||
## Current Acceptance
|
||||
|
||||
Current status is **partial EBA acceptance**.
|
||||
|
||||
Blocked or limited items:
|
||||
|
||||
- `feedback.received` is implemented and unit-covered, but real plugin E2E feedback evidence was not observed from the desktop client click.
|
||||
- Outbound image/voice/file are flattened as textual markers because the WeComBot SDK reply/proactive path used here is markdown/text oriented.
|
||||
- Group member APIs are cache-backed and only know members observed in received messages.
|
||||
- Destructive or moderation APIs are not declared because the current WeComBot protocol surface does not provide safe common equivalents.
|
||||
161
docs/event-based-agents/adapters/wecomcs.md
Normal file
161
docs/event-based-agents/adapters/wecomcs.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# WeCom Customer Service EBA Adapter
|
||||
|
||||
## Status
|
||||
|
||||
WeCom Customer Service now has an EBA adapter directory:
|
||||
|
||||
```text
|
||||
src/langbot/pkg/platform/adapters/wecomcs/
|
||||
├── adapter.py
|
||||
├── api_impl.py
|
||||
├── event_converter.py
|
||||
├── manifest.yaml
|
||||
├── message_converter.py
|
||||
├── platform_api.py
|
||||
└── types.py
|
||||
```
|
||||
|
||||
The adapter is registered as `wecomcs-eba`. It is separate from regular WeCom application messages (`wecom-eba`) and WeCom AI Bot (`wecombot-eba`).
|
||||
|
||||
## Configuration
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `webhook_url` | No | `""` | Unified webhook URL copied into the WeCom Customer Service callback settings. |
|
||||
| `corpid` | Yes | `""` | WeCom corporate ID. |
|
||||
| `secret` | Yes | `""` | Customer Service secret used for access tokens. |
|
||||
| `token` | Yes | `""` | Customer Service callback token. |
|
||||
| `EncodingAESKey` | Yes | `""` | Customer Service callback encryption key. |
|
||||
| `api_base_url` | No | `https://qyapi.weixin.qq.com/cgi-bin` | WeCom API base URL, overrideable for proxy/private-network deployments. |
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| `message.received` | Plugin E2E UI covered for text | Text, image, file, and voice payloads convert to common EBA message components in unit tests. Real WeChat customer-side UI text reached `EBAEventProbe` on May 27, 2026. |
|
||||
| `platform.specific` | Unit covered | Non-message or unknown Customer Service payloads become structured `PlatformSpecificEvent` records. |
|
||||
|
||||
## Common APIs
|
||||
|
||||
| API | Status | Notes |
|
||||
|-----|--------|-------|
|
||||
| `send_message` | Plugin E2E outbound covered | Private/person target only. `target_id` must be `external_userid|open_kfid`. Text and image are implemented; voice/file are explicitly unsupported. |
|
||||
| `reply_message` | Plugin E2E partial | Replies through Customer Service `kf/send_msg` using the original `source_platform_object`. The pipeline reply path reached the send API, but the dev account later hit WeCom `95001 send msg count limit`. |
|
||||
| `get_message` | Plugin E2E covered from cache | Returns cached inbound `MessageReceivedEvent` by message ID. |
|
||||
| `get_user_info` | Plugin E2E covered | Uses cached event users first, then Customer Service `customer/batchget`. |
|
||||
| `get_friend_list` | Plugin E2E covered, partial | Returns customer users seen by this adapter instance. |
|
||||
| `call_platform_api` | Unit covered | See platform-specific APIs below. |
|
||||
| `edit_message` / `delete_message` | Not supported | WeCom Customer Service does not expose a general edit/delete endpoint for bot-sent messages in this adapter. |
|
||||
| Group/member/moderation APIs | Not supported | Customer Service conversations handled here are private customer sessions, not group chats. |
|
||||
| `upload_file` / `get_file_url` | Not supported | Media upload is used internally for outbound image; no portable file URL common API is exposed. |
|
||||
|
||||
## Platform-Specific APIs
|
||||
|
||||
| Action | Status | Notes |
|
||||
|--------|--------|-------|
|
||||
| `check_access_token` | Unit covered | Checks whether the current access token is present. |
|
||||
| `refresh_access_token` | Unit covered | Refreshes the Customer Service access token. |
|
||||
| `get_customer_info` | Unit covered | Calls Customer Service customer lookup by `external_userid`. |
|
||||
|
||||
## Message Components
|
||||
|
||||
Receive:
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| `Source` | Unit covered | Uses Customer Service `msgid` and `send_time`. |
|
||||
| `Plain` | Unit covered | Text payload content is preserved. |
|
||||
| `Image` | Unit covered | Uses the base64 data URL produced by the existing SDK image download path. |
|
||||
| `Voice` | Unit covered | Maps exposed voice media ID to common `Voice.voice_id`; live UI evidence pending. |
|
||||
| `File` | Unit covered | Maps exposed file media ID/name/size to common `File`; live UI evidence pending. |
|
||||
| `Quote`, `At`, `AtAll`, `Face`, `Forward` | Not supported inbound | The current Customer Service SDK event model does not expose these as structured inbound fields. |
|
||||
| `Unknown` | Unit covered | Unsupported message types become `Unknown` in message conversion or `platform.specific` at event level. |
|
||||
|
||||
Send:
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| `Plain` | Plugin E2E outbound covered | Sends through `kf/send_msg` text. |
|
||||
| `Image` | Plugin E2E outbound covered | Uploads media as WeCom image media and sends through `kf/send_msg` image. |
|
||||
| `Quote`, `At`, `AtAll`, `Forward` | Unit covered fallback, live partially blocked | Flattened to text where possible. In the May 27 sweep, later text sends hit WeCom `95001 send msg count limit` after the successful text/image sends. |
|
||||
| `Voice`, `File`, `Face` | Not supported | The adapter raises `NotSupportedError`; no tested Customer Service send path is implemented. |
|
||||
|
||||
## Unit Verification
|
||||
|
||||
Covered by:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/Users/wangqiang/code/python/langbot-plugin-sdk/src uv run pytest tests/unit_tests/platform/test_wecomcs_eba_adapter.py
|
||||
```
|
||||
|
||||
Result on May 27, 2026: `10 passed`.
|
||||
|
||||
The local `PYTHONPATH` is required in this workspace because the installed SDK package in the LangBot venv does not contain the newer `langbot_plugin.api.entities.builtin.platform.errors` module; the existing EBA adapter tests need the same SDK override.
|
||||
|
||||
## Live Probe
|
||||
|
||||
Auxiliary direct adapter probe:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/path/to/langbot-plugin-sdk/src uv run python -m py_compile tests/e2e/live_wecomcs_eba_probe.py
|
||||
|
||||
WECOMCS_CORPID=... \
|
||||
WECOMCS_SECRET=... \
|
||||
WECOMCS_TOKEN=... \
|
||||
WECOMCS_ENCODING_AES_KEY=... \
|
||||
PYTHONPATH=/path/to/langbot-plugin-sdk/src \
|
||||
uv run python tests/e2e/live_wecomcs_eba_probe.py \
|
||||
--path /wecomcs/callback \
|
||||
--log data/temp/wecomcs_eba_live_probe.jsonl
|
||||
```
|
||||
|
||||
This probe is diagnostic only. Final EBA acceptance still requires the standalone SDK runtime plus `EBAEventProbe` plugin path.
|
||||
|
||||
## Standalone Runtime Plugin E2E Record
|
||||
|
||||
Completed partial plugin E2E on May 27, 2026 against `dev.rockchin.top` and the WeChat customer-side UI entry `微信 -> 客服消息 -> 浪波智能客服`.
|
||||
|
||||
Evidence:
|
||||
|
||||
- Server JSONL: `/home/wgc/LangBotxg/LangBotEbaTest/data/temp/wecomcs_eba_plugin_probe.jsonl`
|
||||
- Trigger text: `EBA wecomcs dedupe probe 2026-05-27`
|
||||
- `bot_uuid`: `cc810d2c-91f3-4f92-8f27-e1bf9f7b6cb4`
|
||||
- `adapter_name`: `wecomcs-eba`
|
||||
- Observed common event: `MessageReceived`, `event.type=message.received`
|
||||
- Observed message chain: `Source + Plain`
|
||||
- Observed chat: `chat_type=private`, `chat_id=external_userid|open_kfid`
|
||||
- Observed sender: customer `User` with nickname/avatar from Customer Service lookup
|
||||
- Plugin API probe: `send_message`, `get_message`, `get_user_info`, `get_friend_list`, plugin/workspace storage, and manifest/list APIs succeeded
|
||||
- Component sweep: outbound `Plain` and `Image` succeeded; `Face` and `File` returned explicit `NotSupportedError`; later quote/forward fallback sends were blocked by WeCom `95001 send msg count limit`
|
||||
|
||||
Command shape used:
|
||||
|
||||
```bash
|
||||
cd langbot-plugin-sdk
|
||||
uv run python -m langbot_plugin.cli.__init__ rt --debug-only --ws-control-port 5400 --ws-debug-port 5401 --skip-deps-check
|
||||
|
||||
cd LangBot
|
||||
PYTHONPATH=/absolute/path/to/langbot-plugin-sdk/src uv run main.py --standalone-runtime
|
||||
|
||||
cd data/plugins/LangBot__EBAEventProbe
|
||||
DEBUG_RUNTIME_WS_URL=ws://127.0.0.1:5401/plugin/ws \
|
||||
EBA_PROBE_LOG=/absolute/path/to/LangBot/data/temp/wecomcs_eba_plugin_probe.jsonl \
|
||||
EBA_PROBE_API=1 \
|
||||
EBA_PROBE_COMPONENT_SWEEP=1 \
|
||||
EBA_PROBE_PLATFORM_API=1 \
|
||||
uv --project /absolute/path/to/langbot-plugin-sdk run python -m langbot_plugin.cli.__init__ run
|
||||
```
|
||||
|
||||
Required real UI trigger: send a Customer Service message from the WeCom/WeChat customer-side UI to the configured `dev.rockchin.top` Customer Service account.
|
||||
|
||||
## Current Acceptance
|
||||
|
||||
Current status is **partial EBA acceptance**.
|
||||
|
||||
Blocked or pending items:
|
||||
|
||||
- Inbound UI media (`Image`, `Voice`, `File`) was not sent from the real WeChat customer UI during this run, so receive-side media remains unit-covered only.
|
||||
- Pipeline auto-reply reached `kf/send_msg`, but the test account hit WeCom `95001 send msg count limit` after successful plugin outbound text/image sends. This is recorded as an account/platform rate-limit block, not a conversion or API-shape failure.
|
||||
- The current `EBAEventProbe` run did not call the adapter-specific `call_platform_api` actions (`check_access_token`, `refresh_access_token`, `get_customer_info`); the platform API map remains unit-covered.
|
||||
- Inbound voice/file depends on whether the real Customer Service callback plus `sync_msg` endpoint returns those fields in the shape the local SDK models.
|
||||
- Group, member, edit, delete, moderation, and standalone file URL APIs are intentionally not declared because this Customer Service protocol path does not provide tested common equivalents.
|
||||
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,53 +0,0 @@
|
||||
from v1 import client
|
||||
|
||||
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,246 +0,0 @@
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Callable
|
||||
import dingtalk_stream
|
||||
from .EchoHandler import EchoTextHandler
|
||||
from .dingtalkevent import DingTalkEvent
|
||||
import httpx
|
||||
import traceback
|
||||
|
||||
|
||||
class DingTalkClient:
|
||||
def __init__(
|
||||
self,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
robot_name: str,
|
||||
robot_code: str,
|
||||
markdown_card: bool,
|
||||
):
|
||||
"""初始化 WebSocket 连接并自动启动"""
|
||||
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
||||
self.client = dingtalk_stream.DingTalkStreamClient(self.credential)
|
||||
self.key = client_id
|
||||
self.secret = client_secret
|
||||
# 在 DingTalkClient 中传入自己作为参数,避免循环导入
|
||||
self.EchoTextHandler = EchoTextHandler(self)
|
||||
self.client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
self.access_token = ''
|
||||
self.robot_name = robot_name
|
||||
self.robot_code = robot_code
|
||||
self.access_token_expiry_time = ''
|
||||
self.markdown_card = markdown_card
|
||||
|
||||
async def get_access_token(self):
|
||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'appKey': self.key, 'appSecret': self.secret}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, json=data, headers=headers)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
self.access_token = response_data.get('accessToken')
|
||||
expires_in = int(response_data.get('expireIn', 7200))
|
||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
||||
except Exception as e:
|
||||
raise Exception(e)
|
||||
|
||||
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
|
||||
|
||||
async def check_access_token(self):
|
||||
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 download_image(self, download_code: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get('downloadUrl')
|
||||
else:
|
||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||
|
||||
if download_url:
|
||||
return await self.download_url_to_base64(download_url)
|
||||
|
||||
async def download_url_to_base64(self, download_url):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(download_url)
|
||||
|
||||
if response.status_code == 200:
|
||||
file_bytes = response.content
|
||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
||||
return base64_str
|
||||
else:
|
||||
raise Exception('获取文件失败')
|
||||
|
||||
async def get_audio_url(self, download_code: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get('downloadUrl')
|
||||
if download_url:
|
||||
return await self.download_url_to_base64(download_url)
|
||||
else:
|
||||
raise Exception('获取音频失败')
|
||||
else:
|
||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||
|
||||
async def update_incoming_message(self, message):
|
||||
"""异步更新 DingTalkClient 中的 incoming_message"""
|
||||
message_data = await self.get_message(message)
|
||||
if message_data:
|
||||
event = DingTalkEvent.from_payload(message_data)
|
||||
if event:
|
||||
await self._handle_message(event)
|
||||
|
||||
async def send_message(self, content: str, incoming_message,at:bool):
|
||||
if self.markdown_card:
|
||||
if at:
|
||||
self.EchoTextHandler.reply_markdown(
|
||||
title='@'+incoming_message.sender_nick+' '+content,
|
||||
text='@'+incoming_message.sender_nick+' '+content,
|
||||
incoming_message=incoming_message,
|
||||
)
|
||||
else:
|
||||
self.EchoTextHandler.reply_markdown(
|
||||
title=content,
|
||||
text=content,
|
||||
incoming_message=incoming_message,
|
||||
)
|
||||
else:
|
||||
self.EchoTextHandler.reply_text(content, incoming_message)
|
||||
|
||||
async def get_incoming_message(self):
|
||||
"""获取收到的消息"""
|
||||
return await self.EchoTextHandler.get_incoming_message()
|
||||
|
||||
def on_message(self, msg_type: str):
|
||||
def decorator(func: Callable[[DingTalkEvent], 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: DingTalkEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
msg_type = event.conversation
|
||||
if msg_type in self._message_handlers:
|
||||
for handler in self._message_handlers[msg_type]:
|
||||
await handler(event)
|
||||
|
||||
async def get_message(self, incoming_message: dingtalk_stream.chatbot.ChatbotMessage):
|
||||
try:
|
||||
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
|
||||
message_data = {
|
||||
'IncomingMessage': incoming_message,
|
||||
}
|
||||
if str(incoming_message.conversation_type) == '1':
|
||||
message_data['conversation_type'] = 'FriendMessage'
|
||||
elif str(incoming_message.conversation_type) == '2':
|
||||
message_data['conversation_type'] = 'GroupMessage'
|
||||
|
||||
if incoming_message.message_type == 'richText':
|
||||
data = incoming_message.rich_text_content.to_dict()
|
||||
for item in data['richText']:
|
||||
if 'text' in item:
|
||||
message_data['Content'] = item['text']
|
||||
if incoming_message.get_image_list()[0]:
|
||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
||||
message_data['Type'] = 'text'
|
||||
|
||||
elif incoming_message.message_type == 'text':
|
||||
message_data['Content'] = incoming_message.get_text_list()[0]
|
||||
|
||||
message_data['Type'] = 'text'
|
||||
elif incoming_message.message_type == 'picture':
|
||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
||||
|
||||
message_data['Type'] = 'image'
|
||||
elif incoming_message.message_type == 'audio':
|
||||
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
|
||||
|
||||
message_data['Type'] = 'audio'
|
||||
|
||||
copy_message_data = message_data.copy()
|
||||
del copy_message_data['IncomingMessage']
|
||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
return message_data
|
||||
|
||||
async def send_proactive_message_to_one(self, target_id: str, content: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'
|
||||
|
||||
headers = {
|
||||
'x-acs-dingtalk-access-token': self.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
data = {
|
||||
'robotCode': self.robot_code,
|
||||
'userIds': [target_id],
|
||||
'msgKey': 'sampleText',
|
||||
'msgParam': json.dumps({'content': content}),
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(url, headers=headers, json=data)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'
|
||||
|
||||
headers = {
|
||||
'x-acs-dingtalk-access-token': self.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
data = {
|
||||
'robotCode': self.robot_code,
|
||||
'openConversationId': target_id,
|
||||
'msgKey': 'sampleText',
|
||||
'msgParam': json.dumps({'content': content}),
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.post(url, headers=headers, json=data)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
async def start(self):
|
||||
"""启动 WebSocket 连接,监听消息"""
|
||||
await self.client.start()
|
||||
@@ -1,256 +0,0 @@
|
||||
import time
|
||||
from quart import request
|
||||
import httpx
|
||||
from quart import Quart
|
||||
from typing import Callable, Dict, Any
|
||||
from pkg.platform.types import 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):
|
||||
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
|
||||
|
||||
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:
|
||||
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:
|
||||
traceback.print_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)
|
||||
if response.status_code == 200:
|
||||
return
|
||||
else:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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 +0,0 @@
|
||||
from .client import WeChatPadClient
|
||||
@@ -1,123 +0,0 @@
|
||||
|
||||
from libs.wechatpad_api.util.http_util import async_request, post_json
|
||||
|
||||
|
||||
class MessageApi:
|
||||
def __init__(self, base_url, token):
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
|
||||
def post_text(self, to_wxid, content, ats: list= []):
|
||||
'''
|
||||
|
||||
Args:
|
||||
app_id: 微信id
|
||||
to_wxid: 发送方的微信id
|
||||
content: 内容
|
||||
ats: at
|
||||
|
||||
Returns:
|
||||
|
||||
'''
|
||||
url = self.base_url + "/message/SendTextMessage"
|
||||
"""发送文字消息"""
|
||||
json_data = {
|
||||
"MsgItem": [
|
||||
{
|
||||
"AtWxIDList": ats,
|
||||
"ImageContent": "",
|
||||
"MsgType": 0,
|
||||
"TextContent": content,
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
]
|
||||
}
|
||||
return post_json(base_url=url, token=self.token, data=json_data)
|
||||
|
||||
|
||||
|
||||
|
||||
def post_image(self, to_wxid, img_url, ats: list= []):
|
||||
"""发送图片消息"""
|
||||
# 这里好像可以尝试发送多个暂时未测试
|
||||
json_data = {
|
||||
"MsgItem": [
|
||||
{
|
||||
"AtWxIDList": ats,
|
||||
"ImageContent": img_url,
|
||||
"MsgType": 0,
|
||||
"TextContent": '',
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
]
|
||||
}
|
||||
url = self.base_url + "/message/SendImageMessage"
|
||||
return post_json(base_url=url, token=self.token, data=json_data)
|
||||
|
||||
def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration):
|
||||
"""发送语音消息"""
|
||||
json_data = {
|
||||
"ToUserName": to_wxid,
|
||||
"VoiceData": voice_data,
|
||||
"VoiceFormat": voice_forma,
|
||||
"VoiceSecond": voice_duration
|
||||
}
|
||||
url = self.base_url + "/message/SendVoice"
|
||||
return post_json(base_url=url, token=self.token, data=json_data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag):
|
||||
"""发送名片消息"""
|
||||
param = {
|
||||
"CardAlias": alias,
|
||||
"CardFlag": flag,
|
||||
"CardNickName": nick_name,
|
||||
"CardWxId": name_card_wxid,
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
url = f"{self.base_url}/message/ShareCardMessage"
|
||||
return post_json(base_url=url, token=self.token, data=param)
|
||||
|
||||
def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0):
|
||||
"""发送emoji消息"""
|
||||
json_data = {
|
||||
"EmojiList": [
|
||||
{
|
||||
"EmojiMd5": emoji_md5,
|
||||
"EmojiSize": emoji_size,
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
]
|
||||
}
|
||||
url = f"{self.base_url}/message/SendEmojiMessage"
|
||||
return post_json(base_url=url, token=self.token, data=json_data)
|
||||
|
||||
def post_app_msg(self, to_wxid,xml_data, contenttype:int=0):
|
||||
"""发送appmsg消息"""
|
||||
json_data = {
|
||||
"AppList": [
|
||||
{
|
||||
"ContentType": contenttype,
|
||||
"ContentXML": xml_data,
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
]
|
||||
}
|
||||
url = f"{self.base_url}/message/SendAppMessage"
|
||||
return post_json(base_url=url, token=self.token, data=json_data)
|
||||
|
||||
|
||||
|
||||
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
|
||||
"""撤回消息"""
|
||||
param = {
|
||||
"ClientMsgId": msg_id,
|
||||
"CreateTime": create_time,
|
||||
"NewMsgId": new_msg_id,
|
||||
"ToUserName": to_wxid
|
||||
}
|
||||
url = f"{self.base_url}/message/RevokeMsg"
|
||||
return post_json(base_url=url, token=self.token, data=param)
|
||||
@@ -1,92 +0,0 @@
|
||||
import requests
|
||||
|
||||
def post_json(base_url, token, data=None):
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
url = base_url + f'?key={token}'
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=data, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
raise RuntimeError(response.text)
|
||||
except Exception as e:
|
||||
print(f"http请求失败, url={url}, exception={e}")
|
||||
raise RuntimeError(str(e))
|
||||
|
||||
def get_json(base_url, token):
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
|
||||
url = base_url + f'?key={token}'
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=60)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
raise RuntimeError(response.text)
|
||||
except Exception as e:
|
||||
print(f"http请求失败, url={url}, exception={e}")
|
||||
raise RuntimeError(str(e))
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
|
||||
async def async_request(
|
||||
base_url: str,
|
||||
token_key: str,
|
||||
method: str = 'POST',
|
||||
params: dict = None,
|
||||
# headers: dict = None,
|
||||
data: dict = None,
|
||||
json: dict = None
|
||||
):
|
||||
"""
|
||||
通用异步请求函数
|
||||
|
||||
:param base_url: 请求URL
|
||||
:param token_key: 请求token
|
||||
:param method: HTTP方法 (GET, POST, PUT, DELETE等)
|
||||
:param params: URL查询参数
|
||||
# :param headers: 请求头
|
||||
:param data: 表单数据
|
||||
:param json: JSON数据
|
||||
:return: 响应文本
|
||||
"""
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
url = f"{base_url}?key={token_key}"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
data=data,
|
||||
json=json
|
||||
) as response:
|
||||
response.raise_for_status() # 如果状态码不是200,抛出异常
|
||||
result = await response.json()
|
||||
# print(result)
|
||||
return result
|
||||
# if result.get('Code') == 200:
|
||||
#
|
||||
# return await result
|
||||
# else:
|
||||
# raise RuntimeError("请求失败",response.text)
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
from quart import request
|
||||
from .WXBizMsgCrypt3 import WXBizMsgCrypt
|
||||
import base64
|
||||
import binascii
|
||||
import httpx
|
||||
from quart import Quart
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Callable, Dict, Any
|
||||
from .wecomevent import WecomEvent
|
||||
from pkg.platform.types import message as platform_message
|
||||
import aiofiles
|
||||
|
||||
|
||||
class WecomClient:
|
||||
def __init__(
|
||||
self,
|
||||
corpid: str,
|
||||
secret: str,
|
||||
token: str,
|
||||
EncodingAESKey: str,
|
||||
contacts_secret: str,
|
||||
):
|
||||
self.corpid = corpid
|
||||
self.secret = secret
|
||||
self.access_token_for_contacts = ''
|
||||
self.token = token
|
||||
self.aes = EncodingAESKey
|
||||
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
|
||||
self.access_token = ''
|
||||
self.secret_for_contacts = contacts_secret
|
||||
self.app = Quart(__name__)
|
||||
self.app.add_url_rule(
|
||||
'/callback/command',
|
||||
'handle_callback',
|
||||
self.handle_callback_request,
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
|
||||
# access——token操作
|
||||
async def check_access_token(self):
|
||||
return bool(self.access_token and self.access_token.strip())
|
||||
|
||||
async def check_access_token_for_contacts(self):
|
||||
return bool(self.access_token_for_contacts and self.access_token_for_contacts.strip())
|
||||
|
||||
async def get_access_token(self, secret):
|
||||
url = f'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corpid}&corpsecret={secret}'
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url)
|
||||
data = response.json()
|
||||
if 'access_token' in data:
|
||||
return data['access_token']
|
||||
else:
|
||||
raise Exception(f'未获取access token: {data}')
|
||||
|
||||
async def get_users(self):
|
||||
if not self.check_access_token_for_contacts():
|
||||
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
|
||||
|
||||
url = self.base_url + '/user/list_id?access_token=' + self.access_token_for_contacts
|
||||
async with httpx.AsyncClient() as client:
|
||||
params = {
|
||||
'cursor': '',
|
||||
'limit': 10000,
|
||||
}
|
||||
response = await client.post(url, json=params)
|
||||
data = response.json()
|
||||
if data['errcode'] == 0:
|
||||
dept_users = data['dept_user']
|
||||
userid = []
|
||||
for user in dept_users:
|
||||
userid.append(user['userid'])
|
||||
return userid
|
||||
else:
|
||||
raise Exception('未获取用户')
|
||||
|
||||
async def send_to_all(self, content: str, agent_id: int):
|
||||
if not self.check_access_token_for_contacts():
|
||||
self.access_token_for_contacts = await self.get_access_token(self.secret_for_contacts)
|
||||
|
||||
url = self.base_url + '/message/send?access_token=' + self.access_token_for_contacts
|
||||
user_ids = await self.get_users()
|
||||
user_ids_string = '|'.join(user_ids)
|
||||
async with httpx.AsyncClient() as client:
|
||||
params = {
|
||||
'touser': user_ids_string,
|
||||
'msgtype': 'text',
|
||||
'agentid': agent_id,
|
||||
'text': {
|
||||
'content': content,
|
||||
},
|
||||
'safe': 0,
|
||||
'enable_id_trans': 0,
|
||||
'enable_duplicate_check': 0,
|
||||
'duplicate_check_interval': 1800,
|
||||
}
|
||||
response = await client.post(url, json=params)
|
||||
data = response.json()
|
||||
if data['errcode'] != 0:
|
||||
raise Exception('Failed to send message: ' + str(data))
|
||||
|
||||
async def send_image(self, user_id: str, agent_id: int, media_id: str):
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
url = self.base_url + '/media/upload?access_token=' + self.access_token
|
||||
async with httpx.AsyncClient() as client:
|
||||
params = {
|
||||
'touser': user_id,
|
||||
'toparty': '',
|
||||
'totag': '',
|
||||
'agentid': agent_id,
|
||||
'msgtype': 'image',
|
||||
'image': {
|
||||
'media_id': media_id,
|
||||
},
|
||||
'safe': 0,
|
||||
'enable_id_trans': 0,
|
||||
'enable_duplicate_check': 0,
|
||||
'duplicate_check_interval': 1800,
|
||||
}
|
||||
try:
|
||||
response = await client.post(url, json=params)
|
||||
data = response.json()
|
||||
except Exception as e:
|
||||
raise Exception('Failed to send image: ' + str(e))
|
||||
|
||||
# 企业微信错误码40014和42001,代表accesstoken问题
|
||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_image(user_id, agent_id, media_id)
|
||||
|
||||
if data['errcode'] != 0:
|
||||
raise Exception('Failed to send image: ' + str(data))
|
||||
|
||||
async def send_private_msg(self, user_id: str, agent_id: int, content: str):
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
|
||||
url = self.base_url + '/message/send?access_token=' + self.access_token
|
||||
async with httpx.AsyncClient() as client:
|
||||
params = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'text',
|
||||
'agentid': agent_id,
|
||||
'text': {
|
||||
'content': content,
|
||||
},
|
||||
'safe': 0,
|
||||
'enable_id_trans': 0,
|
||||
'enable_duplicate_check': 0,
|
||||
'duplicate_check_interval': 1800,
|
||||
}
|
||||
response = await client.post(url, json=params)
|
||||
data = response.json()
|
||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
return await self.send_private_msg(user_id, agent_id, content)
|
||||
if data['errcode'] != 0:
|
||||
raise Exception('Failed to send message: ' + str(data))
|
||||
|
||||
async def handle_callback_request(self):
|
||||
"""
|
||||
处理回调请求,包括 GET 验证和 POST 消息接收。
|
||||
"""
|
||||
try:
|
||||
msg_signature = request.args.get('msg_signature')
|
||||
timestamp = request.args.get('timestamp')
|
||||
nonce = request.args.get('nonce')
|
||||
|
||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
|
||||
if request.method == 'GET':
|
||||
echostr = request.args.get('echostr')
|
||||
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||
if ret != 0:
|
||||
raise Exception(f'验证失败,错误码: {ret}')
|
||||
return reply_echo_str
|
||||
|
||||
elif request.method == 'POST':
|
||||
encrypt_msg = await request.data
|
||||
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
|
||||
if ret != 0:
|
||||
raise Exception(f'消息解密失败,错误码: {ret}')
|
||||
|
||||
# 解析消息并处理
|
||||
message_data = await self.get_message(xml_msg)
|
||||
if message_data:
|
||||
event = WecomEvent.from_payload(message_data) # 转换为 WecomEvent 对象
|
||||
if event:
|
||||
await self._handle_message(event)
|
||||
|
||||
return 'success'
|
||||
except Exception as e:
|
||||
return f'Error processing request: {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[[WecomEvent], 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: WecomEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
msg_type = event.type
|
||||
if msg_type in self._message_handlers:
|
||||
for handler in self._message_handlers[msg_type]:
|
||||
await handler(event)
|
||||
|
||||
async def get_message(self, xml_msg: str) -> Dict[str, Any]:
|
||||
"""
|
||||
解析微信返回的 XML 消息并转换为字典。
|
||||
"""
|
||||
root = ET.fromstring(xml_msg)
|
||||
message_data = {
|
||||
'ToUserName': root.find('ToUserName').text,
|
||||
'FromUserName': root.find('FromUserName').text,
|
||||
'CreateTime': int(root.find('CreateTime').text),
|
||||
'MsgType': root.find('MsgType').text,
|
||||
'Content': root.find('Content').text if root.find('Content') is not None else None,
|
||||
'MsgId': int(root.find('MsgId').text) if root.find('MsgId') is not None else None,
|
||||
'AgentID': int(root.find('AgentID').text) if root.find('AgentID') is not None else None,
|
||||
}
|
||||
if message_data['MsgType'] == 'image':
|
||||
message_data['MediaId'] = root.find('MediaId').text if root.find('MediaId') is not None else None
|
||||
message_data['PicUrl'] = root.find('PicUrl').text if root.find('PicUrl') is not None else None
|
||||
|
||||
return message_data
|
||||
|
||||
@staticmethod
|
||||
async def get_image_type(image_bytes: bytes) -> str:
|
||||
"""
|
||||
通过图片的magic numbers判断图片类型
|
||||
"""
|
||||
magic_numbers = {
|
||||
b'\xff\xd8\xff': 'jpg',
|
||||
b'\x89\x50\x4e\x47': 'png',
|
||||
b'\x47\x49\x46': 'gif',
|
||||
b'\x42\x4d': 'bmp',
|
||||
b'\x00\x00\x01\x00': 'ico',
|
||||
}
|
||||
|
||||
for magic, ext in magic_numbers.items():
|
||||
if image_bytes.startswith(magic):
|
||||
return ext
|
||||
return 'jpg' # 默认返回jpg
|
||||
|
||||
async def upload_to_work(self, image: platform_message.Image):
|
||||
"""
|
||||
获取 media_id
|
||||
"""
|
||||
if not await self.check_access_token():
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
|
||||
url = self.base_url + '/media/upload?access_token=' + self.access_token + '&type=file'
|
||||
file_bytes = None
|
||||
file_name = 'uploaded_file.txt'
|
||||
|
||||
# 获取文件的二进制数据
|
||||
if image.path:
|
||||
async with aiofiles.open(image.path, 'rb') as f:
|
||||
file_bytes = await f.read()
|
||||
file_name = image.path.split('/')[-1]
|
||||
elif image.url:
|
||||
file_bytes = await self.download_image_to_bytes(image.url)
|
||||
file_name = image.url.split('/')[-1]
|
||||
elif image.base64:
|
||||
try:
|
||||
base64_data = image.base64
|
||||
if ',' in base64_data:
|
||||
base64_data = base64_data.split(',', 1)[1]
|
||||
padding = 4 - (len(base64_data) % 4) if len(base64_data) % 4 else 0
|
||||
padded_base64 = base64_data + '=' * padding
|
||||
file_bytes = base64.b64decode(padded_base64)
|
||||
except binascii.Error as e:
|
||||
raise ValueError(f'Invalid base64 string: {str(e)}')
|
||||
else:
|
||||
raise ValueError('image对象出错')
|
||||
|
||||
# 设置 multipart/form-data 格式的文件
|
||||
boundary = '-------------------------acebdf13572468'
|
||||
headers = {'Content-Type': f'multipart/form-data; boundary={boundary}'}
|
||||
body = (
|
||||
(
|
||||
f'--{boundary}\r\n'
|
||||
f'Content-Disposition: form-data; name="media"; filename="{file_name}"; filelength={len(file_bytes)}\r\n'
|
||||
f'Content-Type: application/octet-stream\r\n\r\n'
|
||||
).encode('utf-8')
|
||||
+ file_bytes
|
||||
+ f'\r\n--{boundary}--\r\n'.encode('utf-8')
|
||||
)
|
||||
|
||||
# 上传文件
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, content=body)
|
||||
data = response.json()
|
||||
if data['errcode'] == 40014 or data['errcode'] == 42001:
|
||||
self.access_token = await self.get_access_token(self.secret)
|
||||
media_id = await self.upload_to_work(image)
|
||||
if data.get('errcode', 0) != 0:
|
||||
raise Exception('failed to upload file')
|
||||
|
||||
media_id = data.get('media_id')
|
||||
return media_id
|
||||
|
||||
async def download_image_to_bytes(self, url: str) -> bytes:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
# 进行media_id的获取
|
||||
async def get_media_id(self, image: platform_message.Image):
|
||||
media_id = await self.upload_to_work(image=image)
|
||||
return media_id
|
||||
100
main.py
100
main.py
@@ -1,99 +1,3 @@
|
||||
import asyncio
|
||||
# LangBot 终端启动入口
|
||||
# 在此层级解决依赖项检查。
|
||||
# LangBot/main.py
|
||||
import langbot.__main__
|
||||
|
||||
asciiart = r"""
|
||||
_ ___ _
|
||||
| | __ _ _ _ __ _| _ ) ___| |_
|
||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
||||
|____\__,_|_||_\__, |___/\___/\__|
|
||||
|___/
|
||||
|
||||
⭐️ Open Source 开源地址: https://github.com/RockChinQ/LangBot
|
||||
📖 Documentation 文档地址: https://docs.langbot.app
|
||||
"""
|
||||
|
||||
|
||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
||||
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)
|
||||
|
||||
# check plugin deps
|
||||
await deps.precheck_plugin_deps()
|
||||
|
||||
# 检查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))
|
||||
langbot.__main__.main()
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
import enum
|
||||
import quart
|
||||
import traceback
|
||||
from quart.typing import RouteCallable
|
||||
|
||||
from ....core import app
|
||||
|
||||
|
||||
preregistered_groups: list[type[RouterGroup]] = []
|
||||
"""RouterGroup 的预注册列表"""
|
||||
|
||||
|
||||
def group_class(name: str, path: str) -> None:
|
||||
"""注册一个 RouterGroup"""
|
||||
|
||||
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
|
||||
cls.name = name
|
||||
cls.path = path
|
||||
preregistered_groups.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class AuthType(enum.Enum):
|
||||
"""认证类型"""
|
||||
|
||||
NONE = 'none'
|
||||
USER_TOKEN = 'user-token'
|
||||
|
||||
|
||||
class RouterGroup(abc.ABC):
|
||||
name: str
|
||||
|
||||
path: str
|
||||
|
||||
ap: app.Application
|
||||
|
||||
quart_app: quart.Quart
|
||||
|
||||
def __init__(self, ap: app.Application, quart_app: quart.Quart) -> None:
|
||||
self.ap = ap
|
||||
self.quart_app = quart_app
|
||||
|
||||
@abc.abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
pass
|
||||
|
||||
def route(
|
||||
self,
|
||||
rule: str,
|
||||
auth_type: AuthType = AuthType.USER_TOKEN,
|
||||
**options: typing.Any,
|
||||
) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator
|
||||
"""注册一个路由"""
|
||||
|
||||
def decorator(f: RouteCallable) -> RouteCallable:
|
||||
nonlocal rule
|
||||
rule = self.path + rule
|
||||
|
||||
async def handler_error(*args, **kwargs):
|
||||
if auth_type == AuthType.USER_TOKEN:
|
||||
# 从Authorization头中获取token
|
||||
token = quart.request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
|
||||
if not token:
|
||||
return self.http_status(401, -1, '未提供有效的用户令牌')
|
||||
|
||||
try:
|
||||
user_email = await self.ap.user_service.verify_jwt_token(token)
|
||||
|
||||
# check if this account exists
|
||||
user = await self.ap.user_service.get_user_by_email(user_email)
|
||||
if not user:
|
||||
return self.http_status(401, -1, '用户不存在')
|
||||
|
||||
# 检查f是否接受user_email参数
|
||||
if 'user_email' in f.__code__.co_varnames:
|
||||
kwargs['user_email'] = user_email
|
||||
except Exception as e:
|
||||
return self.http_status(401, -1, str(e))
|
||||
|
||||
try:
|
||||
return await f(*args, **kwargs)
|
||||
except Exception: # 自动 500
|
||||
traceback.print_exc()
|
||||
# return self.http_status(500, -2, str(e))
|
||||
return self.http_status(500, -2, 'internal server error')
|
||||
|
||||
new_f = handler_error
|
||||
new_f.__name__ = (self.name + rule).replace('/', '__')
|
||||
new_f.__doc__ = f.__doc__
|
||||
|
||||
self.quart_app.route(rule, **options)(new_f)
|
||||
return f
|
||||
|
||||
return decorator
|
||||
|
||||
def success(self, data: typing.Any = None) -> quart.Response:
|
||||
"""返回一个 200 响应"""
|
||||
return quart.jsonify(
|
||||
{
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': data,
|
||||
}
|
||||
)
|
||||
|
||||
def fail(self, code: int, msg: str) -> quart.Response:
|
||||
"""返回一个异常响应"""
|
||||
|
||||
return quart.jsonify(
|
||||
{
|
||||
'code': code,
|
||||
'msg': msg,
|
||||
}
|
||||
)
|
||||
|
||||
def http_status(self, status: int, code: int, msg: str) -> quart.Response:
|
||||
"""返回一个指定状态码的响应"""
|
||||
return self.fail(code, msg), status
|
||||
@@ -1,44 +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':
|
||||
return self.success(data={'pipelines': await self.ap.pipeline_service.get_pipelines()})
|
||||
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,31 +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()
|
||||
@@ -1,109 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import quart
|
||||
|
||||
from .....core import taskmgr
|
||||
from .. import group
|
||||
|
||||
|
||||
@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 = self.ap.plugin_mgr.plugins()
|
||||
|
||||
plugins_data = [plugin.model_dump() for plugin in plugins]
|
||||
|
||||
return self.success(data={'plugins': plugins_data})
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/toggle',
|
||||
methods=['PUT'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
)
|
||||
async def _(author: str, plugin_name: str) -> str:
|
||||
data = await quart.request.json
|
||||
target_enabled = data.get('target_enabled')
|
||||
await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled)
|
||||
return self.success()
|
||||
|
||||
@self.route(
|
||||
'/<author>/<plugin_name>/update',
|
||||
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_mgr.update_plugin(plugin_name, task_context=ctx),
|
||||
kind='plugin-operation',
|
||||
name=f'plugin-update-{plugin_name}',
|
||||
label=f'更新插件 {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 = self.ap.plugin_mgr.get_plugin(author, plugin_name)
|
||||
if plugin is None:
|
||||
return self.http_status(404, -1, 'plugin not found')
|
||||
return self.success(data={'plugin': plugin.model_dump()})
|
||||
elif quart.request.method == 'DELETE':
|
||||
ctx = taskmgr.TaskContext.new()
|
||||
wrapper = self.ap.task_mgr.create_user_task(
|
||||
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx),
|
||||
kind='plugin-operation',
|
||||
name=f'plugin-remove-{plugin_name}',
|
||||
label=f'删除插件 {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 = self.ap.plugin_mgr.get_plugin(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_mgr.set_plugin_config(plugin, data)
|
||||
|
||||
return self.success(data={})
|
||||
|
||||
@self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _() -> str:
|
||||
data = await quart.request.json
|
||||
await self.ap.plugin_mgr.reorder_plugins(data.get('plugins'))
|
||||
return self.success()
|
||||
|
||||
@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'安装插件 ...{short_source_str}',
|
||||
context=ctx,
|
||||
)
|
||||
|
||||
return self.success(data={'task_id': wrapper.id})
|
||||
@@ -1,46 +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.model_service.get_llm_models()})
|
||||
elif quart.request.method == 'POST':
|
||||
json_data = await quart.request.json
|
||||
|
||||
model_uuid = await self.ap.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.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.model_service.update_llm_model(model_uuid, json_data)
|
||||
|
||||
return self.success()
|
||||
elif quart.request.method == 'DELETE':
|
||||
await self.ap.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.model_service.test_llm_model(model_uuid, json_data)
|
||||
|
||||
return self.success()
|
||||
@@ -1,56 +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()),
|
||||
}
|
||||
)
|
||||
|
||||
@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('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
||||
async def _() -> str:
|
||||
json_data = await quart.request.json
|
||||
|
||||
scope = json_data.get('scope')
|
||||
|
||||
await self.ap.reload(scope=scope)
|
||||
return self.success()
|
||||
|
||||
@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}))
|
||||
@@ -1,42 +0,0 @@
|
||||
import quart
|
||||
import argon2
|
||||
|
||||
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, '系统已初始化')
|
||||
|
||||
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, '用户名或密码错误')
|
||||
|
||||
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})
|
||||
@@ -1,100 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import sqlalchemy
|
||||
|
||||
from ....core import app
|
||||
from ....entity.persistence import bot as persistence_bot
|
||||
from ....entity.persistence import pipeline as persistence_pipeline
|
||||
|
||||
|
||||
class BotService:
|
||||
"""机器人服务"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application) -> None:
|
||||
self.ap = ap
|
||||
|
||||
async def get_bots(self) -> list[dict]:
|
||||
"""获取所有机器人"""
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
|
||||
|
||||
bots = result.all()
|
||||
|
||||
return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) for bot in bots]
|
||||
|
||||
async def get_bot(self, bot_uuid: str) -> dict | None:
|
||||
"""获取机器人"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||
)
|
||||
|
||||
bot = result.first()
|
||||
|
||||
if bot is None:
|
||||
return None
|
||||
|
||||
return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot)
|
||||
|
||||
async def create_bot(self, bot_data: dict) -> str:
|
||||
"""创建机器人"""
|
||||
# TODO: 检查配置信息格式
|
||||
bot_data['uuid'] = str(uuid.uuid4())
|
||||
|
||||
# checkout the default pipeline
|
||||
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:
|
||||
bot_data['use_pipeline_uuid'] = pipeline.uuid
|
||||
bot_data['use_pipeline_name'] = pipeline.name
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(bot_data))
|
||||
|
||||
bot = await self.get_bot(bot_data['uuid'])
|
||||
|
||||
await self.ap.platform_mgr.load_bot(bot)
|
||||
|
||||
return bot_data['uuid']
|
||||
|
||||
async def update_bot(self, bot_uuid: str, bot_data: dict) -> None:
|
||||
"""更新机器人"""
|
||||
if 'uuid' in bot_data:
|
||||
del bot_data['uuid']
|
||||
|
||||
# set use_pipeline_name
|
||||
if 'use_pipeline_uuid' in bot_data:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
persistence_pipeline.LegacyPipeline.uuid == bot_data['use_pipeline_uuid']
|
||||
)
|
||||
)
|
||||
pipeline = result.first()
|
||||
if pipeline is not None:
|
||||
bot_data['use_pipeline_name'] = pipeline.name
|
||||
else:
|
||||
raise Exception('Pipeline not found')
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_bot.Bot).values(bot_data).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||
)
|
||||
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
||||
|
||||
# select from db
|
||||
bot = await self.get_bot(bot_uuid)
|
||||
|
||||
runtime_bot = await self.ap.platform_mgr.load_bot(bot)
|
||||
|
||||
if runtime_bot.enable:
|
||||
await runtime_bot.run()
|
||||
|
||||
async def delete_bot(self, bot_uuid: str) -> None:
|
||||
"""删除机器人"""
|
||||
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
||||
)
|
||||
@@ -1,105 +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 ....provider import entities as llm_entities
|
||||
|
||||
|
||||
class ModelsService:
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application) -> None:
|
||||
self.ap = ap
|
||||
|
||||
async def get_llm_models(self) -> list[dict]:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
||||
|
||||
models = result.all()
|
||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) 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=[llm_entities.Message(role='user', content='Hello, world!')],
|
||||
funcs=[],
|
||||
extra_args={},
|
||||
)
|
||||
@@ -1,121 +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) -> list[dict]:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
||||
|
||||
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)
|
||||
|
||||
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,75 +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']
|
||||
@@ -1,114 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from ..core import app, entities as core_entities
|
||||
from . import entities, operator, errors
|
||||
from ..utils import importutil
|
||||
|
||||
# 引入所有算子以便注册
|
||||
from . import operators
|
||||
|
||||
importutil.import_modules_in_pkg(operators)
|
||||
|
||||
|
||||
class CommandManager:
|
||||
"""命令管理器"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
cmd_list: list[operator.CommandOperator]
|
||||
"""
|
||||
运行时命令列表,扁平存储,各个对象包含对应的子节点引用
|
||||
"""
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
async def initialize(self):
|
||||
# 设置各个类的路径
|
||||
def set_path(cls: operator.CommandOperator, ancestors: list[str]):
|
||||
cls.path = '.'.join(ancestors + [cls.name])
|
||||
for op in operator.preregistered_operators:
|
||||
if op.parent_class == cls:
|
||||
set_path(op, ancestors + [cls.name])
|
||||
|
||||
for cls in operator.preregistered_operators:
|
||||
if cls.parent_class is None:
|
||||
set_path(cls, [])
|
||||
|
||||
# 应用命令权限配置
|
||||
for cls in operator.preregistered_operators:
|
||||
if cls.path in self.ap.instance_config.data['command']['privilege']:
|
||||
cls.lowest_privilege = self.ap.instance_config.data['command']['privilege'][cls.path]
|
||||
|
||||
# 实例化所有类
|
||||
self.cmd_list = [cls(self.ap) for cls in operator.preregistered_operators]
|
||||
|
||||
# 设置所有类的子节点
|
||||
for cmd in self.cmd_list:
|
||||
cmd.children = [child for child in self.cmd_list if child.parent_class == cmd.__class__]
|
||||
|
||||
# 初始化所有类
|
||||
for cmd in self.cmd_list:
|
||||
await cmd.initialize()
|
||||
|
||||
async def _execute(
|
||||
self,
|
||||
context: entities.ExecuteContext,
|
||||
operator_list: list[operator.CommandOperator],
|
||||
operator: operator.CommandOperator = None,
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
"""执行命令"""
|
||||
|
||||
found = False
|
||||
if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名
|
||||
for oper in operator_list:
|
||||
if (context.crt_params[0] == oper.name or context.crt_params[0] in oper.alias) and (
|
||||
oper.parent_class is None or oper.parent_class == operator.__class__
|
||||
):
|
||||
found = True
|
||||
|
||||
context.crt_command = context.crt_params[0]
|
||||
context.crt_params = context.crt_params[1:]
|
||||
|
||||
async for ret in self._execute(context, oper.children, oper):
|
||||
yield ret
|
||||
break
|
||||
|
||||
if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错
|
||||
if operator is None:
|
||||
yield entities.CommandReturn(error=errors.CommandNotFoundError(context.crt_params[0]))
|
||||
else:
|
||||
if operator.lowest_privilege > context.privilege:
|
||||
yield entities.CommandReturn(error=errors.CommandPrivilegeError(operator.name))
|
||||
else:
|
||||
async for ret in operator.execute(context):
|
||||
yield ret
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
command_text: str,
|
||||
query: core_entities.Query,
|
||||
session: core_entities.Session,
|
||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
"""执行命令"""
|
||||
|
||||
privilege = 1
|
||||
|
||||
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
|
||||
privilege = 2
|
||||
|
||||
ctx = entities.ExecuteContext(
|
||||
query=query,
|
||||
session=session,
|
||||
command_text=command_text,
|
||||
command='',
|
||||
crt_command='',
|
||||
params=command_text.split(' '),
|
||||
crt_params=command_text.split(' '),
|
||||
privilege=privilege,
|
||||
)
|
||||
|
||||
async for ret in self._execute(ctx, self.cmd_list):
|
||||
yield ret
|
||||
@@ -1,74 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
import pydantic.v1 as pydantic
|
||||
|
||||
from ..core import entities as core_entities
|
||||
from . import errors
|
||||
from ..platform.types import message as platform_message
|
||||
|
||||
|
||||
class CommandReturn(pydantic.BaseModel):
|
||||
"""命令返回值"""
|
||||
|
||||
text: typing.Optional[str] = None
|
||||
"""文本
|
||||
"""
|
||||
|
||||
image: typing.Optional[platform_message.Image] = None
|
||||
"""弃用"""
|
||||
|
||||
image_url: typing.Optional[str] = None
|
||||
"""图片链接
|
||||
"""
|
||||
|
||||
error: typing.Optional[errors.CommandError] = None
|
||||
"""错误
|
||||
"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class ExecuteContext(pydantic.BaseModel):
|
||||
"""单次命令执行上下文"""
|
||||
|
||||
query: core_entities.Query
|
||||
"""本次消息的请求对象"""
|
||||
|
||||
session: core_entities.Session
|
||||
"""本次消息所属的会话对象"""
|
||||
|
||||
command_text: str
|
||||
"""命令完整文本"""
|
||||
|
||||
command: str
|
||||
"""命令名称"""
|
||||
|
||||
crt_command: str
|
||||
"""当前命令
|
||||
|
||||
多级命令中crt_command为当前命令,command为根命令。
|
||||
例如:!plugin on Webwlkr
|
||||
处理到plugin时,command为plugin,crt_command为plugin
|
||||
处理到on时,command为plugin,crt_command为on
|
||||
"""
|
||||
|
||||
params: list[str]
|
||||
"""命令参数
|
||||
|
||||
整个命令以空格分割后的参数列表
|
||||
"""
|
||||
|
||||
crt_params: list[str]
|
||||
"""当前命令参数
|
||||
|
||||
多级命令中crt_params为当前命令参数,params为根命令参数。
|
||||
例如:!plugin on Webwlkr
|
||||
处理到plugin时,params为['on', 'Webwlkr'],crt_params为['on', 'Webwlkr']
|
||||
处理到on时,params为['on', 'Webwlkr'],crt_params为['Webwlkr']
|
||||
"""
|
||||
|
||||
privilege: int
|
||||
"""发起人权限"""
|
||||
@@ -1,26 +0,0 @@
|
||||
class CommandError(Exception):
|
||||
def __init__(self, message: str = None):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class CommandNotFoundError(CommandError):
|
||||
def __init__(self, message: str = None):
|
||||
super().__init__('未知命令: ' + message)
|
||||
|
||||
|
||||
class CommandPrivilegeError(CommandError):
|
||||
def __init__(self, message: str = None):
|
||||
super().__init__('权限不足: ' + message)
|
||||
|
||||
|
||||
class ParamNotEnoughError(CommandError):
|
||||
def __init__(self, message: str = None):
|
||||
super().__init__('参数不足: ' + message)
|
||||
|
||||
|
||||
class CommandOperationError(CommandError):
|
||||
def __init__(self, message: str = None):
|
||||
super().__init__('操作失败: ' + message)
|
||||
@@ -1,41 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>')
|
||||
class CmdOperator(operator.CommandOperator):
|
||||
"""命令列表"""
|
||||
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
"""执行"""
|
||||
if len(context.crt_params) == 0:
|
||||
reply_str = '当前所有命令: \n\n'
|
||||
|
||||
for cmd in self.ap.cmd_mgr.cmd_list:
|
||||
if cmd.parent_class is None:
|
||||
reply_str += f'{cmd.name}: {cmd.help}\n'
|
||||
|
||||
reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助'
|
||||
|
||||
yield entities.CommandReturn(text=reply_str.strip())
|
||||
|
||||
else:
|
||||
cmd_name = context.crt_params[0]
|
||||
|
||||
cmd = None
|
||||
|
||||
for _cmd in self.ap.cmd_mgr.cmd_list:
|
||||
if (cmd_name == _cmd.name or cmd_name in _cmd.alias) and (_cmd.parent_class is None):
|
||||
cmd = _cmd
|
||||
break
|
||||
|
||||
if cmd is None:
|
||||
yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name))
|
||||
else:
|
||||
reply_str = f'{cmd.name}: {cmd.help}\n\n'
|
||||
reply_str += f'使用方法: \n{cmd.usage}'
|
||||
|
||||
yield entities.CommandReturn(text=reply_str.strip())
|
||||
@@ -1,43 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all')
|
||||
class DelOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if context.session.conversations:
|
||||
delete_index = 0
|
||||
if len(context.crt_params) > 0:
|
||||
try:
|
||||
delete_index = int(context.crt_params[0])
|
||||
except Exception:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数'))
|
||||
return
|
||||
|
||||
if delete_index < 0 or delete_index >= len(context.session.conversations):
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围'))
|
||||
return
|
||||
|
||||
# 倒序
|
||||
to_delete_index = len(context.session.conversations) - 1 - delete_index
|
||||
|
||||
if context.session.conversations[to_delete_index] == context.session.using_conversation:
|
||||
context.session.using_conversation = None
|
||||
|
||||
del context.session.conversations[to_delete_index]
|
||||
|
||||
yield entities.CommandReturn(text=f'已删除对话: {delete_index}')
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
||||
|
||||
|
||||
@operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator)
|
||||
class DelAllOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
context.session.conversations = []
|
||||
context.session.using_conversation = None
|
||||
|
||||
yield entities.CommandReturn(text='已删除所有对话')
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from .. import operator, entities
|
||||
|
||||
|
||||
@operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func')
|
||||
class FuncOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> AsyncGenerator[entities.CommandReturn, None]:
|
||||
reply_str = '当前已启用的内容函数: \n\n'
|
||||
|
||||
index = 1
|
||||
|
||||
all_functions = await self.ap.tool_mgr.get_all_functions(
|
||||
plugin_enabled=True,
|
||||
)
|
||||
|
||||
for func in all_functions:
|
||||
reply_str += '{}. {}:\n{}\n\n'.format(
|
||||
index,
|
||||
func.name,
|
||||
func.description,
|
||||
)
|
||||
index += 1
|
||||
|
||||
yield entities.CommandReturn(text=reply_str)
|
||||
@@ -1,15 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities
|
||||
|
||||
|
||||
@operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>')
|
||||
class HelpOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接:https://langbot.app'
|
||||
|
||||
help += '\n发送命令 !cmd 可查看命令列表'
|
||||
|
||||
yield entities.CommandReturn(text=help)
|
||||
@@ -1,28 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='last', help='切换到前一个对话', usage='!last')
|
||||
class LastOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if context.session.conversations:
|
||||
# 找到当前会话的上一个会话
|
||||
for index in range(len(context.session.conversations) - 1, -1, -1):
|
||||
if context.session.conversations[index] == context.session.using_conversation:
|
||||
if index == 0:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了'))
|
||||
return
|
||||
else:
|
||||
context.session.using_conversation = context.session.conversations[index - 1]
|
||||
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
yield entities.CommandReturn(
|
||||
text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}'
|
||||
)
|
||||
return
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
||||
@@ -1,48 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>')
|
||||
class ListOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
page = 0
|
||||
|
||||
if len(context.crt_params) > 0:
|
||||
try:
|
||||
page = int(context.crt_params[0] - 1)
|
||||
except Exception:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数'))
|
||||
return
|
||||
|
||||
record_per_page = 10
|
||||
|
||||
content = ''
|
||||
|
||||
index = 0
|
||||
|
||||
using_conv_index = 0
|
||||
|
||||
for conv in context.session.conversations[::-1]:
|
||||
time_str = conv.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if conv == context.session.using_conversation:
|
||||
using_conv_index = index
|
||||
|
||||
if index >= page * record_per_page and index < (page + 1) * record_per_page:
|
||||
content += (
|
||||
f'{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else "无内容"}\n'
|
||||
)
|
||||
index += 1
|
||||
|
||||
if content == '':
|
||||
content = '无'
|
||||
else:
|
||||
if context.session.using_conversation is None:
|
||||
content += '\n当前处于新会话'
|
||||
else:
|
||||
content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}'
|
||||
|
||||
yield entities.CommandReturn(text=f'第 {page + 1} 页 (时间倒序):\n{content}')
|
||||
@@ -1,27 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='next', help='切换到后一个对话', usage='!next')
|
||||
class NextOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if context.session.conversations:
|
||||
# 找到当前会话的下一个会话
|
||||
for index in range(len(context.session.conversations)):
|
||||
if context.session.conversations[index] == context.session.using_conversation:
|
||||
if index == len(context.session.conversations) - 1:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了'))
|
||||
return
|
||||
else:
|
||||
context.session.using_conversation = context.session.conversations[index + 1]
|
||||
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
yield entities.CommandReturn(
|
||||
text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}'
|
||||
)
|
||||
return
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
||||
@@ -1,156 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
import traceback
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(
|
||||
name='plugin',
|
||||
help='插件操作',
|
||||
usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>',
|
||||
)
|
||||
class PluginOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
plugin_list = self.ap.plugin_mgr.plugins()
|
||||
reply_str = '所有插件({}):\n'.format(len(plugin_list))
|
||||
idx = 0
|
||||
for plugin in plugin_list:
|
||||
reply_str += '\n#{} {} {}\n{}\nv{}\n作者: {}\n'.format(
|
||||
(idx + 1),
|
||||
plugin.plugin_name,
|
||||
'[已禁用]' if not plugin.enabled else '',
|
||||
plugin.plugin_description,
|
||||
plugin.plugin_version,
|
||||
plugin.plugin_author,
|
||||
)
|
||||
|
||||
idx += 1
|
||||
|
||||
yield entities.CommandReturn(text=reply_str)
|
||||
|
||||
|
||||
@operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator)
|
||||
class PluginGetOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址'))
|
||||
else:
|
||||
repo = context.crt_params[0]
|
||||
|
||||
yield entities.CommandReturn(text='正在安装插件...')
|
||||
|
||||
try:
|
||||
await self.ap.plugin_mgr.install_plugin(repo)
|
||||
yield entities.CommandReturn(text='插件安装成功,请重启程序以加载插件')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件安装失败: ' + str(e)))
|
||||
|
||||
|
||||
@operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator)
|
||||
class PluginUpdateOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
||||
else:
|
||||
plugin_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
|
||||
|
||||
if plugin_container is not None:
|
||||
yield entities.CommandReturn(text='正在更新插件...')
|
||||
await self.ap.plugin_mgr.update_plugin(plugin_name)
|
||||
yield entities.CommandReturn(text='插件更新成功,请重启程序以加载插件')
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: 未找到插件'))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
||||
|
||||
|
||||
@operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator)
|
||||
class PluginUpdateAllOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
try:
|
||||
plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()]
|
||||
|
||||
if plugins:
|
||||
yield entities.CommandReturn(text='正在更新插件...')
|
||||
updated = []
|
||||
try:
|
||||
for plugin_name in plugins:
|
||||
await self.ap.plugin_mgr.update_plugin(plugin_name)
|
||||
updated.append(plugin_name)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
||||
yield entities.CommandReturn(text='已更新插件: {}'.format(', '.join(updated)))
|
||||
else:
|
||||
yield entities.CommandReturn(text='没有可更新的插件')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
||||
|
||||
|
||||
@operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator)
|
||||
class PluginDelOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
||||
else:
|
||||
plugin_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
|
||||
|
||||
if plugin_container is not None:
|
||||
yield entities.CommandReturn(text='正在删除插件...')
|
||||
await self.ap.plugin_mgr.uninstall_plugin(plugin_name)
|
||||
yield entities.CommandReturn(text='插件删除成功,请重启程序以加载插件')
|
||||
else:
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: 未找到插件'))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: ' + str(e)))
|
||||
|
||||
|
||||
@operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator)
|
||||
class PluginEnableOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
||||
else:
|
||||
plugin_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True):
|
||||
yield entities.CommandReturn(text='已启用插件: {}'.format(plugin_name))
|
||||
else:
|
||||
yield entities.CommandReturn(
|
||||
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e)))
|
||||
|
||||
|
||||
@operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator)
|
||||
class PluginDisableOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
if len(context.crt_params) == 0:
|
||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
||||
else:
|
||||
plugin_name = context.crt_params[0]
|
||||
|
||||
try:
|
||||
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False):
|
||||
yield entities.CommandReturn(text='已禁用插件: {}'.format(plugin_name))
|
||||
else:
|
||||
yield entities.CommandReturn(
|
||||
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e)))
|
||||
@@ -1,20 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt')
|
||||
class PromptOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
"""执行"""
|
||||
if context.session.using_conversation is None:
|
||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
||||
else:
|
||||
reply_str = '当前对话所有内容:\n\n'
|
||||
|
||||
for msg in context.session.using_conversation.messages:
|
||||
reply_str += f'{msg.role}: {msg.content}\n'
|
||||
|
||||
yield entities.CommandReturn(text=reply_str)
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities, errors
|
||||
|
||||
|
||||
@operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend')
|
||||
class ResendOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
# 回滚到最后一条用户message前
|
||||
if context.session.using_conversation is None:
|
||||
yield entities.CommandReturn(error=errors.CommandError('当前没有对话'))
|
||||
else:
|
||||
conv_msg = context.session.using_conversation.messages
|
||||
|
||||
# 倒序一直删到最后一条用户message
|
||||
while len(conv_msg) > 0 and conv_msg[-1].role != 'user':
|
||||
conv_msg.pop()
|
||||
|
||||
if len(conv_msg) > 0:
|
||||
# 删除最后一条用户message
|
||||
conv_msg.pop()
|
||||
|
||||
# 不重发了,提示用户已删除就行了
|
||||
yield entities.CommandReturn(text='已删除最后一次请求记录')
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities
|
||||
|
||||
|
||||
@operator.operator_class(name='reset', help='重置当前会话', usage='!reset')
|
||||
class ResetOperator(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
"""执行"""
|
||||
context.session.using_conversation = None
|
||||
|
||||
yield entities.CommandReturn(text='已重置当前会话')
|
||||
@@ -1,11 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities
|
||||
|
||||
|
||||
@operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2)
|
||||
class UpdateCommand(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
yield entities.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。')
|
||||
@@ -1,19 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import operator, entities
|
||||
|
||||
|
||||
@operator.operator_class(name='version', help='显示版本信息', usage='!version')
|
||||
class VersionCommand(operator.CommandOperator):
|
||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
||||
reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}'
|
||||
|
||||
try:
|
||||
if await self.ap.ver_mgr.is_new_version_available():
|
||||
reply_str += '\n\n有新版本可用。'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
yield entities.CommandReturn(text=reply_str.strip())
|
||||
238
pkg/core/app.py
238
pkg/core/app.py
@@ -1,238 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import traceback
|
||||
import sys
|
||||
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 manager as plugin_mgr
|
||||
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 ..discover import engine as discover_engine
|
||||
from ..utils import logcache
|
||||
from . import taskmgr
|
||||
from . import entities as core_entities
|
||||
|
||||
|
||||
class Application:
|
||||
"""运行时应用对象和上下文"""
|
||||
|
||||
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
|
||||
|
||||
# TODO 移动到 pipeline 里
|
||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
||||
|
||||
# ======= 配置管理器 =======
|
||||
|
||||
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
|
||||
|
||||
# ======= 元数据配置管理器 =======
|
||||
|
||||
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_mgr: plugin_mgr.PluginManager = 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
|
||||
|
||||
http_ctrl: http_controller.HTTPController = None
|
||||
|
||||
log_cache: logcache.LogCache = None
|
||||
|
||||
# ========= HTTP Services =========
|
||||
|
||||
user_service: user_service.UserService = None
|
||||
|
||||
model_service: model_service.ModelsService = None
|
||||
|
||||
pipeline_service: pipeline_service.PipelineService = None
|
||||
|
||||
bot_service: bot_service.BotService = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
try:
|
||||
await self.plugin_mgr.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'应用运行致命异常: {e}')
|
||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
||||
|
||||
async def print_web_access_info(self):
|
||||
"""打印访问 webui 的提示"""
|
||||
|
||||
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)
|
||||
|
||||
async def reload(
|
||||
self,
|
||||
scope: core_entities.LifecycleControlScope,
|
||||
):
|
||||
match scope:
|
||||
case core_entities.LifecycleControlScope.PLATFORM.value:
|
||||
self.logger.info('执行热重载 scope=' + scope)
|
||||
await self.platform_mgr.shutdown()
|
||||
|
||||
self.platform_mgr = im_mgr.PlatformManager(self)
|
||||
|
||||
await self.platform_mgr.initialize()
|
||||
|
||||
self.task_mgr.create_task(
|
||||
self.platform_mgr.run(),
|
||||
name='platform-manager',
|
||||
scopes=[
|
||||
core_entities.LifecycleControlScope.APPLICATION,
|
||||
core_entities.LifecycleControlScope.PLATFORM,
|
||||
],
|
||||
)
|
||||
case core_entities.LifecycleControlScope.PLUGIN.value:
|
||||
self.logger.info('执行热重载 scope=' + scope)
|
||||
await self.plugin_mgr.destroy_plugins()
|
||||
|
||||
# 删除 sys.module 中所有的 plugins/* 下的模块
|
||||
for mod in list(sys.modules.keys()):
|
||||
if mod.startswith('plugins.'):
|
||||
del sys.modules[mod]
|
||||
|
||||
self.plugin_mgr = plugin_mgr.PluginManager(self)
|
||||
await self.plugin_mgr.initialize()
|
||||
|
||||
await self.plugin_mgr.initialize_plugins()
|
||||
|
||||
await self.plugin_mgr.load_plugins()
|
||||
await self.plugin_mgr.initialize_plugins()
|
||||
case core_entities.LifecycleControlScope.PROVIDER.value:
|
||||
self.logger.info('执行热重载 scope=' + scope)
|
||||
|
||||
await self.tool_mgr.shutdown()
|
||||
|
||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
|
||||
await llm_model_mgr_inst.initialize()
|
||||
self.model_mgr = llm_model_mgr_inst
|
||||
|
||||
llm_session_mgr_inst = llm_session_mgr.SessionManager(self)
|
||||
await llm_session_mgr_inst.initialize()
|
||||
self.sess_mgr = llm_session_mgr_inst
|
||||
|
||||
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self)
|
||||
await llm_tool_mgr_inst.initialize()
|
||||
self.tool_mgr = llm_tool_mgr_inst
|
||||
case _:
|
||||
pass
|
||||
@@ -1,170 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import typing
|
||||
import datetime
|
||||
import asyncio
|
||||
|
||||
import pydantic.v1 as pydantic
|
||||
|
||||
from ..provider import entities as llm_entities
|
||||
from ..provider.modelmgr import requester
|
||||
from ..provider.tools import entities as tools_entities
|
||||
from ..platform import adapter as msadapter
|
||||
from ..platform.types import message as platform_message
|
||||
from ..platform.types import events as platform_events
|
||||
|
||||
|
||||
class LifecycleControlScope(enum.Enum):
|
||||
APPLICATION = 'application'
|
||||
PLATFORM = 'platform'
|
||||
PLUGIN = 'plugin'
|
||||
PROVIDER = 'provider'
|
||||
|
||||
|
||||
class LauncherTypes(enum.Enum):
|
||||
"""一个请求的发起者类型"""
|
||||
|
||||
PERSON = 'person'
|
||||
"""私聊"""
|
||||
|
||||
GROUP = 'group'
|
||||
"""群聊"""
|
||||
|
||||
|
||||
class Query(pydantic.BaseModel):
|
||||
"""一次请求的信息封装"""
|
||||
|
||||
query_id: int
|
||||
"""请求ID,添加进请求池时生成"""
|
||||
|
||||
launcher_type: LauncherTypes
|
||||
"""会话类型,platform处理阶段设置"""
|
||||
|
||||
launcher_id: typing.Union[int, str]
|
||||
"""会话ID,platform处理阶段设置"""
|
||||
|
||||
sender_id: typing.Union[int, str]
|
||||
"""发送者ID,platform处理阶段设置"""
|
||||
|
||||
message_event: platform_events.MessageEvent
|
||||
"""事件,platform收到的原始事件"""
|
||||
|
||||
message_chain: platform_message.MessageChain
|
||||
"""消息链,platform收到的原始消息链"""
|
||||
|
||||
bot_uuid: typing.Optional[str] = None
|
||||
"""机器人UUID。"""
|
||||
|
||||
pipeline_uuid: typing.Optional[str] = None
|
||||
"""流水线UUID。"""
|
||||
|
||||
pipeline_config: typing.Optional[dict[str, typing.Any]] = None
|
||||
"""流水线配置,由 Pipeline 在运行开始时设置。"""
|
||||
|
||||
adapter: msadapter.MessagePlatformAdapter
|
||||
"""消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器"""
|
||||
|
||||
session: typing.Optional[Session] = None
|
||||
"""会话对象,由前置处理器阶段设置"""
|
||||
|
||||
messages: typing.Optional[list[llm_entities.Message]] = []
|
||||
"""历史消息列表,由前置处理器阶段设置"""
|
||||
|
||||
prompt: typing.Optional[llm_entities.Prompt] = None
|
||||
"""情景预设内容,由前置处理器阶段设置"""
|
||||
|
||||
user_message: typing.Optional[llm_entities.Message] = None
|
||||
"""此次请求的用户消息对象,由前置处理器阶段设置"""
|
||||
|
||||
variables: typing.Optional[dict[str, typing.Any]] = None
|
||||
"""变量,由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。"""
|
||||
|
||||
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
|
||||
"""使用的对话模型,由前置处理器阶段设置"""
|
||||
|
||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None
|
||||
"""使用的函数,由前置处理器阶段设置"""
|
||||
|
||||
resp_messages: (
|
||||
typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]]
|
||||
) = []
|
||||
"""由Process阶段生成的回复消息对象列表"""
|
||||
|
||||
resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None
|
||||
"""回复消息链,从resp_messages包装而得"""
|
||||
|
||||
# ======= 内部保留 =======
|
||||
current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None
|
||||
"""当前所处阶段"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
# ========== 插件可调用的 API(请求 API) ==========
|
||||
|
||||
def set_variable(self, key: str, value: typing.Any):
|
||||
"""设置变量"""
|
||||
if self.variables is None:
|
||||
self.variables = {}
|
||||
self.variables[key] = value
|
||||
|
||||
def get_variable(self, key: str) -> typing.Any:
|
||||
"""获取变量"""
|
||||
if self.variables is None:
|
||||
return None
|
||||
return self.variables.get(key)
|
||||
|
||||
def get_variables(self) -> dict[str, typing.Any]:
|
||||
"""获取所有变量"""
|
||||
if self.variables is None:
|
||||
return {}
|
||||
return self.variables
|
||||
|
||||
|
||||
class Conversation(pydantic.BaseModel):
|
||||
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation"""
|
||||
|
||||
prompt: llm_entities.Prompt
|
||||
|
||||
messages: list[llm_entities.Message]
|
||||
|
||||
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
||||
|
||||
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
||||
|
||||
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
|
||||
|
||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
|
||||
|
||||
uuid: typing.Optional[str] = None
|
||||
"""该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class Session(pydantic.BaseModel):
|
||||
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
|
||||
|
||||
launcher_type: LauncherTypes
|
||||
|
||||
launcher_id: typing.Union[int, str]
|
||||
|
||||
sender_id: typing.Optional[typing.Union[int, str]] = 0
|
||||
|
||||
use_prompt_name: typing.Optional[str] = 'default'
|
||||
|
||||
using_conversation: typing.Optional[Conversation] = None
|
||||
|
||||
conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list)
|
||||
|
||||
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
||||
|
||||
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
||||
|
||||
semaphore: typing.Optional[asyncio.Semaphore] = None
|
||||
"""当前会话的信号量,用于限制并发"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
@@ -1,103 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from .. import stage, app
|
||||
from ...utils import version, proxy, announce
|
||||
from ...pipeline import pool, controller, pipelinemgr
|
||||
from ...plugin import manager as plugin_mgr
|
||||
from ...command import cmdmgr
|
||||
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 ...platform import botmgr as im_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 ...discover import engine as discover_engine
|
||||
from ...utils import logcache
|
||||
from .. import taskmgr
|
||||
|
||||
|
||||
@stage.stage_class('BuildAppStage')
|
||||
class BuildAppStage(stage.BootingStage):
|
||||
"""构建应用阶段"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""构建app对象的各个组件对象并初始化"""
|
||||
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
|
||||
|
||||
discover = discover_engine.ComponentDiscoveryEngine(ap)
|
||||
discover.discover_blueprint('components.yaml')
|
||||
ap.discover = discover
|
||||
|
||||
proxy_mgr = proxy.ProxyManager(ap)
|
||||
await proxy_mgr.initialize()
|
||||
ap.proxy_mgr = proxy_mgr
|
||||
|
||||
ver_mgr = version.VersionManager(ap)
|
||||
await ver_mgr.initialize()
|
||||
ap.ver_mgr = ver_mgr
|
||||
|
||||
# 发送公告
|
||||
ann_mgr = announce.AnnouncementManager(ap)
|
||||
ap.ann_mgr = ann_mgr
|
||||
|
||||
ap.query_pool = pool.QueryPool()
|
||||
|
||||
log_cache = logcache.LogCache()
|
||||
ap.log_cache = log_cache
|
||||
|
||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
||||
ap.persistence_mgr = persistence_mgr_inst
|
||||
await persistence_mgr_inst.initialize()
|
||||
|
||||
plugin_mgr_inst = plugin_mgr.PluginManager(ap)
|
||||
await plugin_mgr_inst.initialize()
|
||||
ap.plugin_mgr = plugin_mgr_inst
|
||||
await plugin_mgr_inst.load_plugins()
|
||||
|
||||
cmd_mgr_inst = cmdmgr.CommandManager(ap)
|
||||
await cmd_mgr_inst.initialize()
|
||||
ap.cmd_mgr = cmd_mgr_inst
|
||||
|
||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(ap)
|
||||
await llm_model_mgr_inst.initialize()
|
||||
ap.model_mgr = llm_model_mgr_inst
|
||||
|
||||
llm_session_mgr_inst = llm_session_mgr.SessionManager(ap)
|
||||
await llm_session_mgr_inst.initialize()
|
||||
ap.sess_mgr = llm_session_mgr_inst
|
||||
|
||||
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap)
|
||||
await llm_tool_mgr_inst.initialize()
|
||||
ap.tool_mgr = llm_tool_mgr_inst
|
||||
|
||||
im_mgr_inst = im_mgr.PlatformManager(ap=ap)
|
||||
await im_mgr_inst.initialize()
|
||||
ap.platform_mgr = im_mgr_inst
|
||||
|
||||
pipeline_mgr = pipelinemgr.PipelineManager(ap)
|
||||
await pipeline_mgr.initialize()
|
||||
ap.pipeline_mgr = pipeline_mgr
|
||||
|
||||
http_ctrl = http_controller.HTTPController(ap)
|
||||
await http_ctrl.initialize()
|
||||
ap.http_ctrl = http_ctrl
|
||||
|
||||
user_service_inst = user_service.UserService(ap)
|
||||
ap.user_service = user_service_inst
|
||||
|
||||
model_service_inst = model_service.ModelsService(ap)
|
||||
ap.model_service = model_service_inst
|
||||
|
||||
pipeline_service_inst = pipeline_service.PipelineService(ap)
|
||||
ap.pipeline_service = pipeline_service_inst
|
||||
|
||||
bot_service_inst = bot_service.BotService(ap)
|
||||
ap.bot_service = bot_service_inst
|
||||
|
||||
ctrl = controller.Controller(ap)
|
||||
ap.ctrl = ctrl
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user