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

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

|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
|
||||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
|
||||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
|
||||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
|
||||||
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
|
||||||
|
|
||||||
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
|
|
||||||
|
|
||||||
Or visit the demo environment: https://demo.langbot.dev/
|
|
||||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
|
||||||
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment.
|
|
||||||
|
|
||||||
### Message Platform
|
|
||||||
|
|
||||||
| Platform | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | ✅ | |
|
|
||||||
| Personal QQ | ✅ | |
|
|
||||||
| QQ Official API | ✅ | |
|
|
||||||
| WeCom | ✅ | |
|
|
||||||
| WeComCS | ✅ | |
|
|
||||||
| WeCom AI Bot | ✅ | |
|
|
||||||
| Personal WeChat | ✅ | |
|
|
||||||
| Lark | ✅ | |
|
|
||||||
| DingTalk | ✅ | |
|
|
||||||
|
|
||||||
### LLMs
|
|
||||||
|
|
||||||
| LLM | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
|
|
||||||
|
|
||||||
## 🤝 Community Contribution
|
|
||||||
|
|
||||||
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
|
||||||
</a>
|
|
||||||
176
README_ES.md
Normal file
176
README_ES.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
📍 Guías prácticas: [desplegar un bot de IA multiplataforma en 5 minutos](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [conectar DeepSeek a WeChat, Discord y Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [ejecutar un Dify Agent en Discord, Telegram y Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) y [crear un chatbot con n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>
|
||||||
176
README_FR.md
Normal file
176
README_FR.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
📍 Guides pratiques : [déployer un bot IA multiplateforme en 5 minutes](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connecter DeepSeek à WeChat, Discord et Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [exécuter un Dify Agent dans Discord, Telegram et Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) et [créer un chatbot avec n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>
|
||||||
216
README_JP.md
216
README_JP.md
@@ -1,31 +1,70 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language)
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||||
|
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
<a href="https://langbot.app">ホーム</a> |
|
<a href="https://langbot.app">ホーム</a> |
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
<a href="https://link.langbot.app/ja/docs/features">機能</a> |
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
|
<a href="https://link.langbot.app/ja/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">プラグインマーケット</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。
|
---
|
||||||
|
|
||||||
## 📦 始め方
|
## LangBot とは?
|
||||||
|
|
||||||
#### Docker Compose デプロイ
|
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||||
|
|
||||||
|
### 主な機能
|
||||||
|
|
||||||
|
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAG(ナレッジベース)を内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) と深く統合。
|
||||||
|
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
|
||||||
|
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
|
||||||
|
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
|
||||||
|
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
|
||||||
|
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
|
||||||
|
|
||||||
|
[→ すべての機能について詳しく見る](https://link.langbot.app/ja/docs/features)
|
||||||
|
|
||||||
|
📍 実践ガイド: [5分でマルチプラットフォームAIボットをデプロイ](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/)、[DeepSeekをWeChat・Discord・Telegramに接続](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/)、[Dify AgentをDiscord・Telegram・Slackで動かす](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/)、[n8n連携チャットボットを構築](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## クイックスタート
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推奨)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — デプロイ不要、すぐに使えます。
|
||||||
|
|
||||||
|
### ワンライン起動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> [uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。http://localhost:5300 にアクセスして完了。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
@@ -33,93 +72,104 @@ cd LangBot/docker
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
http://localhost:5300 にアクセスして使用を開始します。
|
### ワンクリッククラウドデプロイ
|
||||||
|
|
||||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
|
||||||
|
|
||||||
#### Panelでのワンクリックデプロイ
|
|
||||||
|
|
||||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
|
||||||
|
|
||||||
#### Zeaburクラウドデプロイ
|
|
||||||
|
|
||||||
コミュニティが提供するZeaburテンプレート。
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
|
||||||
#### Railwayクラウドデプロイ
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
#### その他のデプロイ方法
|
**その他:** [Docker](https://link.langbot.app/en/docs/docker) · [手動デプロイ](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
|
---
|
||||||
|
|
||||||
## 😎 最新情報を入手
|
## 対応プラットフォーム
|
||||||
|
|
||||||
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ 機能
|
|
||||||
|
|
||||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
|
||||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
|
||||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
|
||||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
|
||||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
|
||||||
|
|
||||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
|
||||||
|
|
||||||
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
|
|
||||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
|
||||||
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
|
|
||||||
|
|
||||||
### メッセージプラットフォーム
|
|
||||||
|
|
||||||
| プラットフォーム | ステータス | 備考 |
|
| プラットフォーム | ステータス | 備考 |
|
||||||
| --- | --- | --- |
|
|----------|--------|-------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 公式 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 公式 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 公式 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 公式 |
|
||||||
| 個人QQ | ✅ | |
|
| QQ | ✅ | 個人・公式API(チャンネル・DM・グループ) |
|
||||||
| QQ公式API | ✅ | |
|
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||||
| WeCom | ✅ | |
|
| WeChat | ✅ | 個人・公式アカウント |
|
||||||
| WeComCS | ✅ | |
|
| Lark | ✅ | 公式 |
|
||||||
| WeCom AI Bot | ✅ | |
|
| DingTalk | ✅ | 公式 |
|
||||||
| 個人WeChat | ✅ | |
|
| KOOK | ✅ | 公式 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | Matrix、Satori |
|
||||||
|
| Matrix | ✅ | Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip など複数のブリッジ先プラットフォームに対応 |
|
||||||
|
|
||||||
### LLMs
|
---
|
||||||
|
|
||||||
| LLM | ステータス | 備考 |
|
## 対応LLMと統合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
|
|
||||||
|
|
||||||
## 🤝 コミュニティ貢献
|
| プロバイダー | タイプ | ステータス |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ゲートウェイ | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ゲートウェイ | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | ゲートウェイ | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ |
|
||||||
|
|
||||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
[→ すべての統合を表示](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## なぜ LangBot?
|
||||||
|
|
||||||
|
| ユースケース | LangBot の活用方法 |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
|
||||||
|
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
|
||||||
|
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
|
||||||
|
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ライブデモ
|
||||||
|
|
||||||
|
**今すぐ試す:** https://demo.langbot.dev/
|
||||||
|
- メール: `demo@langbot.app`
|
||||||
|
- パスワード: `langbot123456`
|
||||||
|
|
||||||
|
*注意: 公開デモ環境です。機密情報を入力しないでください。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コミュニティ
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 推移
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コントリビューター
|
||||||
|
|
||||||
|
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
|||||||
176
README_KO.md
Normal file
176
README_KO.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
📍 실전 가이드: [5분 만에 멀티 플랫폼 AI 봇 배포하기](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [DeepSeek를 WeChat, Discord, Telegram에 연결하기](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [Dify Agent를 Discord, Telegram, Slack에서 실행하기](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/), [n8n 기반 챗봇 만들기](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### ☁️ 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>
|
||||||
176
README_RU.md
Normal file
176
README_RU.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
📍 Практические руководства: [развернуть мультиплатформенного ИИ-бота за 5 минут](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [подключить DeepSeek к WeChat, Discord и Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [запустить Dify Agent в Discord, Telegram и Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) и [создать чат-бота на n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### ☁️ 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>
|
||||||
232
README_TW.md
232
README_TW.md
@@ -1,33 +1,72 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
|
||||||
|
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
[](https://gitcode.com/RockChinQ/LangBot)
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
<a href="https://langbot.app">主頁</a> |
|
<a href="https://langbot.app">官網</a> |
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文件</a> |
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">外掛介紹</a> |
|
<a href="https://link.langbot.app/zh/docs/guide">文件</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交外掛</a>
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">外掛市場</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
|
---
|
||||||
|
|
||||||
## 📦 開始使用
|
## 什麼是 LangBot?
|
||||||
|
|
||||||
#### Docker Compose 部署
|
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型(LLM)連接到各種聊天平台,幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG(知識庫),深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||||
|
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
|
||||||
|
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
|
||||||
|
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
📍 實踐指南:[5 分鐘部署多平台 AI 機器人](https://blog.langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[將 DeepSeek 接入微信、企業微信與 Discord](https://blog.langbot.app/zh/blog/connect-deepseek-to-wechat/)、[讓 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://blog.langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 建構多平台 AI 聊天機器人](https://blog.langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推薦)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,開箱即用。
|
||||||
|
|
||||||
|
### 一鍵啟動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)。訪問 http://localhost:5300 即可使用。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
@@ -35,94 +74,66 @@ cd LangBot/docker
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
訪問 http://localhost:5300 即可開始使用。
|
### 一鍵雲端部署
|
||||||
|
|
||||||
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
|
||||||
|
|
||||||
#### 寶塔面板部署
|
|
||||||
|
|
||||||
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
|
||||||
|
|
||||||
#### Zeabur 雲端部署
|
|
||||||
|
|
||||||
社群貢獻的 Zeabur 模板。
|
|
||||||
|
|
||||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||||
|
|
||||||
#### Railway 雲端部署
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
#### 手動部署
|
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手動部署](https://link.langbot.app/zh/docs/manual-deploy) · [寶塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
---
|
||||||
|
|
||||||
## 😎 保持更新
|
## 支援的平台
|
||||||
|
|
||||||
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ 特性
|
|
||||||
|
|
||||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
|
||||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
|
||||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
|
||||||
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
|
||||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
|
||||||
|
|
||||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
|
||||||
|
|
||||||
或訪問 demo 環境:https://demo.langbot.dev/
|
|
||||||
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
|
|
||||||
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
|
|
||||||
|
|
||||||
### 訊息平台
|
|
||||||
|
|
||||||
| 平台 | 狀態 | 備註 |
|
| 平台 | 狀態 | 備註 |
|
||||||
| --- | --- | --- |
|
|------|------|------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 官方 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 官方 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 官方 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 官方 |
|
||||||
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
|
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||||
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
|
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||||
| 微信 | ✅ | |
|
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||||
| 企微對外客服 | ✅ | |
|
| 飛書 | ✅ | 官方 |
|
||||||
| 企微智能機器人 | ✅ | |
|
| 釘釘 | ✅ | 官方 |
|
||||||
| 微信公眾號 | ✅ | |
|
| KOOK | ✅ | 官方 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支援多種橋接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
### 大模型能力
|
---
|
||||||
|
|
||||||
| 模型 | 狀態 | 備註 |
|
## 支援的大模型與整合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 介面格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型介面聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里雲百煉](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支援通過 MCP 協議獲取工具 |
|
|
||||||
|
|
||||||
### TTS
|
| 提供商 | 類型 | 狀態 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
### TTS(語音合成)
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
@@ -130,13 +141,54 @@ docker compose up -d
|
|||||||
### 文生圖
|
### 文生圖
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
## 😘 社群貢獻
|
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
感謝以下[程式碼貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
|
---
|
||||||
|
|
||||||
|
## 為什麼選擇 LangBot?
|
||||||
|
|
||||||
|
| 使用場景 | LangBot 如何幫助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
|
||||||
|
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
|
||||||
|
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
|
||||||
|
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 線上演示
|
||||||
|
|
||||||
|
**立即體驗:** https://demo.langbot.dev/
|
||||||
|
- 信箱:`demo@langbot.app`
|
||||||
|
- 密碼:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
- [Discord 社群](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 貢獻者
|
||||||
|
|
||||||
|
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
176
README_VI.md
Normal file
176
README_VI.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
📍 Hướng dẫn thực hành: [triển khai bot AI đa nền tảng trong 5 phút](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [kết nối DeepSeek với WeChat, Discord và Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [chạy Dify Agent trên Discord, Telegram và Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) và [xây dựng chatbot với n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bắt đầu nhanh
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Khuyên dùng)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Không cần triển khai, sẵn sàng sử dụng.
|
||||||
|
|
||||||
|
### Khởi chạy một dòng
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Yêu cầu [uv](https://docs.astral.sh/uv/getting-started/installation/). Truy cập http://localhost:5300 — xong.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triển khai đám mây một cú nhấp
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Thêm tùy chọn:** [Docker](https://link.langbot.app/en/docs/docker) · [Thủ công](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nền tảng được hỗ trợ
|
||||||
|
|
||||||
|
| Nền tảng | Trạng thái | Ghi chú |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Chính thức |
|
||||||
|
| Telegram | ✅ | Chính thức |
|
||||||
|
| Slack | ✅ | Chính thức |
|
||||||
|
| LINE | ✅ | Chính thức |
|
||||||
|
| QQ | ✅ | Cá nhân & API chính thức (Kênh, DM, Nhóm) |
|
||||||
|
| WeCom | ✅ | WeChat doanh nghiệp, CS bên ngoài, AI Bot |
|
||||||
|
| WeChat | ✅ | Cá nhân & Tài khoản công khai |
|
||||||
|
| Lark | ✅ | Chính thức |
|
||||||
|
| DingTalk | ✅ | Chính thức |
|
||||||
|
| KOOK | ✅ | Chính thức |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Hỗ trợ nhiều nền tảng qua bridge như Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip và hơn thế nữa |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLM và tích hợp được hỗ trợ
|
||||||
|
|
||||||
|
| Nhà cung cấp | Loại | Trạng thái |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM cục bộ | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM cục bộ | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Giao thức | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Cổng | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Cổng | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Cổng | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Cổng | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Cổng | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Nền tảng GPU | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Cổng | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Cổng | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Cổng | ✅ |
|
||||||
|
|
||||||
|
[→ Xem tất cả tích hợp](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tại sao chọn LangBot?
|
||||||
|
|
||||||
|
| Trường hợp sử dụng | LangBot giúp như thế nào |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Hỗ trợ khách hàng** | Triển khai agent AI trên Slack/Discord/Telegram để trả lời câu hỏi bằng cơ sở kiến thức của bạn |
|
||||||
|
| **Công cụ nội bộ** | Kết nối quy trình n8n/Dify với WeCom/DingTalk để tự động hóa quy trình kinh doanh |
|
||||||
|
| **Quản lý cộng đồng** | Quản lý nhóm QQ/Discord với tính năng lọc nội dung và tương tác được hỗ trợ bởi AI |
|
||||||
|
| **Đa nền tảng** | Một bot, tất cả nền tảng. Quản lý từ một bảng điều khiển duy nhất |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo trực tuyến
|
||||||
|
|
||||||
|
**Thử ngay:** https://demo.langbot.dev/
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Mật khẩu: `langbot123456`
|
||||||
|
|
||||||
|
*Lưu ý: Môi trường demo công khai. Không nhập thông tin nhạy cảm.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cộng đồng
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Cộng đồng Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lịch sử Star
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Người đóng góp
|
||||||
|
|
||||||
|
Cảm ơn tất cả [người đóng góp](https://github.com/langbot-app/LangBot/graphs/contributors) đã giúp LangBot trở nên tốt hơn:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
629
docker/README_K8S.md
Normal file
629
docker/README_K8S.md
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
# LangBot Kubernetes 部署指南 / Kubernetes Deployment Guide
|
||||||
|
|
||||||
|
[简体中文](#简体中文) | [English](#english)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 简体中文
|
||||||
|
|
||||||
|
### 概述
|
||||||
|
|
||||||
|
本指南提供了在 Kubernetes 集群中部署 LangBot 的完整步骤。Kubernetes 部署配置基于 `docker-compose.yaml`,适用于生产环境的容器化部署。
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Kubernetes 集群(版本 1.19+)
|
||||||
|
- `kubectl` 命令行工具已配置并可访问集群
|
||||||
|
- 集群中有可用的存储类(StorageClass)用于持久化存储(可选但推荐)
|
||||||
|
- 至少 2 vCPU 和 4GB RAM 的可用资源
|
||||||
|
|
||||||
|
### 架构说明
|
||||||
|
|
||||||
|
Kubernetes 部署包含以下组件:
|
||||||
|
|
||||||
|
1. **langbot**: 主应用服务
|
||||||
|
- 提供 Web UI(端口 5300)
|
||||||
|
- 处理平台 webhook(端口 2280-2290)
|
||||||
|
- 数据持久化卷
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: 插件运行时服务
|
||||||
|
- WebSocket 通信(端口 5400)
|
||||||
|
- 插件数据持久化卷
|
||||||
|
|
||||||
|
3. **持久化存储**:
|
||||||
|
- `langbot-data`: LangBot 主数据
|
||||||
|
- `langbot-plugins`: 插件文件
|
||||||
|
- `langbot-plugin-runtime-data`: 插件运行时数据
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
#### 1. 下载部署文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# 或直接下载 kubernetes.yaml
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 部署到 Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用所有配置
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# 检查部署状态
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# 查看 Pod 日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 访问 LangBot
|
||||||
|
|
||||||
|
默认情况下,LangBot 服务使用 ClusterIP 类型,只能在集群内部访问。您可以选择以下方式之一来访问:
|
||||||
|
|
||||||
|
**选项 A: 端口转发(推荐用于测试)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
然后访问 http://localhost:5300
|
||||||
|
|
||||||
|
**选项 B: NodePort(适用于开发环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 NodePort Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取节点 IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# 访问 http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 C: LoadBalancer(适用于云环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 LoadBalancer Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取外部 IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# 访问 http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 D: Ingress(推荐用于生产环境)**
|
||||||
|
|
||||||
|
确保集群中已安装 Ingress Controller(如 nginx-ingress),然后:
|
||||||
|
|
||||||
|
1. 编辑 `kubernetes.yaml` 中的 Ingress 配置
|
||||||
|
2. 修改域名为您的实际域名
|
||||||
|
3. 应用配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 访问 http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
#### 环境变量
|
||||||
|
|
||||||
|
在 `ConfigMap` 中配置环境变量:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # 修改为您的时区
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储配置
|
||||||
|
|
||||||
|
默认使用动态存储分配。如果您有特定的 StorageClass,请在 PVC 中指定:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 资源限制
|
||||||
|
|
||||||
|
根据您的需求调整资源限制:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用操作
|
||||||
|
|
||||||
|
#### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 LangBot 主服务日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# 查看插件运行时日志
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 重启服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重启 LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# 重启插件运行时
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新到最新版本
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# 检查更新状态
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 扩容(不推荐)
|
||||||
|
|
||||||
|
注意:由于 LangBot 使用 ReadWriteOnce 的持久化存储,不支持多副本扩容。如需高可用,请考虑使用 ReadWriteMany 存储或其他架构方案。
|
||||||
|
|
||||||
|
#### 备份数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 PVC 数据
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 卸载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除所有资源(保留 PVC)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# 删除 PVC(会删除数据)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# 删除命名空间
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排查
|
||||||
|
|
||||||
|
#### Pod 无法启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Pod 状态
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# 查看详细信息
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# 查看事件
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 PVC 状态
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# 检查 PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 网络访问问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# 检查端口转发
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境建议
|
||||||
|
|
||||||
|
1. **使用特定版本标签**:避免使用 `latest` 标签,使用具体版本号如 `rockchin/langbot:v1.0.0`
|
||||||
|
2. **配置资源限制**:根据实际负载调整 CPU 和内存限制
|
||||||
|
3. **使用 Ingress + TLS**:配置 HTTPS 访问和证书管理
|
||||||
|
4. **配置监控和告警**:集成 Prometheus、Grafana 等监控工具
|
||||||
|
5. **定期备份**:配置自动备份策略保护数据
|
||||||
|
6. **使用专用 StorageClass**:为生产环境配置高性能存储
|
||||||
|
7. **配置亲和性规则**:确保 Pod 调度到合适的节点
|
||||||
|
|
||||||
|
### 高级配置
|
||||||
|
|
||||||
|
#### 使用 Secrets 管理敏感信息
|
||||||
|
|
||||||
|
如果需要配置 API 密钥等敏感信息:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 Deployment 中引用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 配置水平自动扩缩容(HPA)
|
||||||
|
|
||||||
|
注意:需要确保使用 ReadWriteMany 存储类型
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参考资源
|
||||||
|
|
||||||
|
- [LangBot 官方文档](https://docs.langbot.app)
|
||||||
|
- [Docker 部署文档](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes 官方文档](https://kubernetes.io/docs/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
This guide provides complete steps for deploying LangBot in a Kubernetes cluster. The Kubernetes deployment configuration is based on `docker-compose.yaml` and is suitable for production containerized deployments.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes cluster (version 1.19+)
|
||||||
|
- `kubectl` command-line tool configured with cluster access
|
||||||
|
- Available StorageClass in the cluster for persistent storage (optional but recommended)
|
||||||
|
- At least 2 vCPU and 4GB RAM of available resources
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
The Kubernetes deployment includes the following components:
|
||||||
|
|
||||||
|
1. **langbot**: Main application service
|
||||||
|
- Provides Web UI (port 5300)
|
||||||
|
- Handles platform webhooks (ports 2280-2290)
|
||||||
|
- Data persistence volume
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: Plugin runtime service
|
||||||
|
- WebSocket communication (port 5400)
|
||||||
|
- Plugin data persistence volume
|
||||||
|
|
||||||
|
3. **Persistent Storage**:
|
||||||
|
- `langbot-data`: LangBot main data
|
||||||
|
- `langbot-plugins`: Plugin files
|
||||||
|
- `langbot-plugin-runtime-data`: Plugin runtime data
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
#### 1. Download Deployment Files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# Or download kubernetes.yaml directly
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Deploy to Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all configurations
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# Check deployment status
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# View Pod logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Access LangBot
|
||||||
|
|
||||||
|
By default, LangBot service uses ClusterIP type, accessible only within the cluster. Choose one of the following methods to access:
|
||||||
|
|
||||||
|
**Option A: Port Forwarding (Recommended for testing)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
Then visit http://localhost:5300
|
||||||
|
|
||||||
|
**Option B: NodePort (Suitable for development)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the NodePort Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get node IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# Visit http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: LoadBalancer (Suitable for cloud environments)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the LoadBalancer Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get external IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# Visit http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option D: Ingress (Recommended for production)**
|
||||||
|
|
||||||
|
Ensure an Ingress Controller (e.g., nginx-ingress) is installed in the cluster, then:
|
||||||
|
|
||||||
|
1. Edit the Ingress configuration in `kubernetes.yaml`
|
||||||
|
2. Change the domain to your actual domain
|
||||||
|
3. Apply configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Visit http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
Configure environment variables in ConfigMap:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # Change to your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Configuration
|
||||||
|
|
||||||
|
Uses dynamic storage provisioning by default. If you have a specific StorageClass, specify it in PVC:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource Limits
|
||||||
|
|
||||||
|
Adjust resource limits based on your needs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View LangBot main service logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# View plugin runtime logs
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# Restart plugin runtime
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update to latest version
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# Check update status
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scaling (Not Recommended)
|
||||||
|
|
||||||
|
Note: Due to LangBot using ReadWriteOnce persistent storage, multi-replica scaling is not supported. For high availability, consider using ReadWriteMany storage or alternative architectures.
|
||||||
|
|
||||||
|
#### Backup Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup PVC data
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete all resources (keep PVCs)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# Delete PVCs (will delete data)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# Delete namespace
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Pods Not Starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Pod status
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# View detailed information
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# View events
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PVC status
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# Check PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Network Access Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# Test port forwarding
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Recommendations
|
||||||
|
|
||||||
|
1. **Use specific version tags**: Avoid using `latest` tag, use specific version like `rockchin/langbot:v1.0.0`
|
||||||
|
2. **Configure resource limits**: Adjust CPU and memory limits based on actual load
|
||||||
|
3. **Use Ingress + TLS**: Configure HTTPS access and certificate management
|
||||||
|
4. **Configure monitoring and alerts**: Integrate monitoring tools like Prometheus, Grafana
|
||||||
|
5. **Regular backups**: Configure automated backup strategy to protect data
|
||||||
|
6. **Use dedicated StorageClass**: Configure high-performance storage for production
|
||||||
|
7. **Configure affinity rules**: Ensure Pods are scheduled to appropriate nodes
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
#### Using Secrets for Sensitive Information
|
||||||
|
|
||||||
|
If you need to configure sensitive information like API keys:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference in Deployment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Horizontal Pod Autoscaling (HPA)
|
||||||
|
|
||||||
|
Note: Requires ReadWriteMany storage type
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
- [LangBot Official Documentation](https://docs.langbot.app)
|
||||||
|
- [Docker Deployment Guide](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes Official Documentation](https://kubernetes.io/docs/)
|
||||||
74
docker/deploy-k8s-test.sh
Executable file
74
docker/deploy-k8s-test.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick test script for LangBot Kubernetes deployment
|
||||||
|
# This script helps you test the Kubernetes deployment locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 LangBot Kubernetes Deployment Test Script"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for kubectl
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
echo "❌ kubectl is not installed. Please install kubectl first."
|
||||||
|
echo "Visit: https://kubernetes.io/docs/tasks/tools/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ kubectl is installed"
|
||||||
|
|
||||||
|
# Check if kubectl can connect to a cluster
|
||||||
|
if ! kubectl cluster-info &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ No Kubernetes cluster found."
|
||||||
|
echo ""
|
||||||
|
echo "To test locally, you can use:"
|
||||||
|
echo " - kind: https://kind.sigs.k8s.io/"
|
||||||
|
echo " - minikube: https://minikube.sigs.k8s.io/"
|
||||||
|
echo " - k3s: https://k3s.io/"
|
||||||
|
echo ""
|
||||||
|
echo "Example with kind:"
|
||||||
|
echo " kind create cluster --name langbot-test"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Connected to Kubernetes cluster"
|
||||||
|
kubectl cluster-info
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ask user to confirm
|
||||||
|
read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Deployment cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Deploying LangBot..."
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Waiting for pods to be ready..."
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Deployment status:"
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🌐 To access LangBot Web UI, run:"
|
||||||
|
echo " kubectl port-forward -n langbot svc/langbot 5300:5300"
|
||||||
|
echo ""
|
||||||
|
echo "Then visit: http://localhost:5300"
|
||||||
|
echo ""
|
||||||
|
echo "📝 To view logs:"
|
||||||
|
echo " kubectl logs -n langbot -l app=langbot -f"
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ To uninstall:"
|
||||||
|
echo " kubectl delete namespace langbot"
|
||||||
|
echo ""
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Docker Compose configuration for LangBot
|
||||||
|
# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -12,7 +14,41 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
networks:
|
||||||
|
- langbot_network
|
||||||
|
|
||||||
|
# The Box sandbox runtime is optional. It is only started when you run
|
||||||
|
# ``docker compose --profile box up`` (or ``docker compose --profile all
|
||||||
|
# up``). With Box off, LangBot keeps the dashboard / skills list visible
|
||||||
|
# (read-only) but disables sandbox tools, skill add/edit and stdio MCP —
|
||||||
|
# set ``box.enabled: false`` in ``data/config.yaml`` (or
|
||||||
|
# ``BOX__ENABLED=false`` in the langbot service env below) to match.
|
||||||
|
langbot_box:
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
container_name: langbot_box
|
||||||
|
profiles: ["box", "all"]
|
||||||
|
volumes:
|
||||||
|
# Keep the source and target path identical because langbot_box uses the
|
||||||
|
# host Docker socket to create sandbox containers. Override
|
||||||
|
# LANGBOT_BOX_ROOT with an absolute path if you do not want the default.
|
||||||
|
- ${LANGBOT_BOX_ROOT:-${PWD}/data/box}:${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
|
# Mount container runtime socket for Box sandbox backend.
|
||||||
|
# Uncomment the one that matches your container runtime:
|
||||||
|
# - /var/run/podman/podman.sock:/var/run/podman/podman.sock # Podman
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock # Docker
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
# The Box runtime does NOT read box.local.* from config.yaml or env; it
|
||||||
|
# receives its configuration from LangBot via the INIT RPC action.
|
||||||
|
# Do not add LANGBOT_BOX_* / BOX__* here — they would be silently ignored.
|
||||||
|
# Launched through the same CLI entry point as the plugin runtime
|
||||||
|
# (`langbot_plugin.cli.__init__ <subcommand>`). WebSocket is the default
|
||||||
|
# control transport — mirrors `rt`, which also runs with no flag. Pass
|
||||||
|
# `-s` / `--stdio-control` only for the stdio mode LangBot uses outside
|
||||||
|
# containers.
|
||||||
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "box"]
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
@@ -21,13 +57,19 @@ services:
|
|||||||
container_name: langbot
|
container_name: langbot
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./plugins:/app/plugins
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
# Unified env-override convention: SECTION__SUBSECTION__KEY overrides the
|
||||||
|
# matching config.yaml field (see LoadConfigStage). These map onto
|
||||||
|
# box.local.* and are forwarded to the Box runtime via INIT RPC.
|
||||||
|
- BOX__LOCAL__HOST_ROOT=${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
|
- BOX__LOCAL__DEFAULT_WORKSPACE=default
|
||||||
|
- BOX__LOCAL__SKILLS_ROOT=skills
|
||||||
|
- BOX__LOCAL__ALLOWED_MOUNT_ROOTS=${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
ports:
|
ports:
|
||||||
- 5300:5300 # For web ui
|
- 5300:5300 # For web ui and webhook callback
|
||||||
- 2280-2290:2280-2290 # For platform webhook
|
- 2280-2285:2280-2285 # For platform reverse connection
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
|
|||||||
400
docker/kubernetes.yaml
Normal file
400
docker/kubernetes.yaml
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Kubernetes Deployment for LangBot
|
||||||
|
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# kubectl apply -f kubernetes.yaml
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - A Kubernetes cluster (1.19+)
|
||||||
|
# - kubectl configured to communicate with your cluster
|
||||||
|
# - (Optional) A StorageClass for dynamic volume provisioning
|
||||||
|
#
|
||||||
|
# Components:
|
||||||
|
# - Namespace: langbot
|
||||||
|
# - PersistentVolumeClaims for data persistence
|
||||||
|
# - Deployments for langbot and langbot_plugin_runtime
|
||||||
|
# - Services for network access
|
||||||
|
# - ConfigMap for timezone configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
# Namespace
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot plugins
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugins
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for Plugin Runtime data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# ConfigMap for environment configuration
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai"
|
||||||
|
PLUGIN__RUNTIME_WS_URL: "ws://langbot-plugin-runtime:5400/control/ws"
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot Plugin Runtime
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot-plugin-runtime
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 5400
|
||||||
|
name: runtime
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-data
|
||||||
|
mountPath: /app/data/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: plugin-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugin-runtime-data
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot Plugin Runtime
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
ports:
|
||||||
|
- port: 5400
|
||||||
|
targetPort: 5400
|
||||||
|
protocol: TCP
|
||||||
|
name: runtime
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5300
|
||||||
|
name: web
|
||||||
|
protocol: TCP
|
||||||
|
- containerPort: 2280
|
||||||
|
name: webhook-start
|
||||||
|
protocol: TCP
|
||||||
|
# Note: Kubernetes doesn't support port ranges directly in container ports
|
||||||
|
# The webhook ports 2280-2290 are available, but we only expose the start of the range
|
||||||
|
# If you need all ports exposed, consider using a Service with multiple port definitions
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
- name: PLUGIN__RUNTIME_WS_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: PLUGIN__RUNTIME_WS_URL
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: plugins
|
||||||
|
mountPath: /app/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-data
|
||||||
|
- name: plugins
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugins
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot (ClusterIP for internal access)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot
|
||||||
|
ports:
|
||||||
|
- port: 5300
|
||||||
|
targetPort: 5300
|
||||||
|
protocol: TCP
|
||||||
|
name: web
|
||||||
|
- port: 2280
|
||||||
|
targetPort: 2280
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2280
|
||||||
|
- port: 2281
|
||||||
|
targetPort: 2281
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2281
|
||||||
|
- port: 2282
|
||||||
|
targetPort: 2282
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2282
|
||||||
|
- port: 2283
|
||||||
|
targetPort: 2283
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2283
|
||||||
|
- port: 2284
|
||||||
|
targetPort: 2284
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2284
|
||||||
|
- port: 2285
|
||||||
|
targetPort: 2285
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2285
|
||||||
|
- port: 2286
|
||||||
|
targetPort: 2286
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2286
|
||||||
|
- port: 2287
|
||||||
|
targetPort: 2287
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2287
|
||||||
|
- port: 2288
|
||||||
|
targetPort: 2288
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2288
|
||||||
|
- port: 2289
|
||||||
|
targetPort: 2289
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2289
|
||||||
|
- port: 2290
|
||||||
|
targetPort: 2290
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2290
|
||||||
|
|
||||||
|
---
|
||||||
|
# Ingress for external access (Optional - requires Ingress Controller)
|
||||||
|
# Uncomment and modify the following section if you want to expose LangBot via Ingress
|
||||||
|
# apiVersion: networking.k8s.io/v1
|
||||||
|
# kind: Ingress
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-ingress
|
||||||
|
# namespace: langbot
|
||||||
|
# annotations:
|
||||||
|
# # Uncomment and modify based on your ingress controller
|
||||||
|
# # nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
# # cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
# spec:
|
||||||
|
# ingressClassName: nginx # Change based on your ingress controller
|
||||||
|
# rules:
|
||||||
|
# - host: langbot.yourdomain.com # Change to your domain
|
||||||
|
# http:
|
||||||
|
# paths:
|
||||||
|
# - path: /
|
||||||
|
# pathType: Prefix
|
||||||
|
# backend:
|
||||||
|
# service:
|
||||||
|
# name: langbot
|
||||||
|
# port:
|
||||||
|
# number: 5300
|
||||||
|
# # Uncomment for TLS/HTTPS
|
||||||
|
# # tls:
|
||||||
|
# # - hosts:
|
||||||
|
# # - langbot.yourdomain.com
|
||||||
|
# # secretName: langbot-tls
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with LoadBalancer (Alternative to Ingress)
|
||||||
|
# Uncomment the following if you want to expose LangBot directly via LoadBalancer
|
||||||
|
# This is useful in cloud environments (AWS, GCP, Azure, etc.)
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-loadbalancer
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: LoadBalancer
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 80
|
||||||
|
# targetPort: 5300
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook-start
|
||||||
|
# # Add more webhook ports as needed
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with NodePort (Alternative for exposing service)
|
||||||
|
# Uncomment if you want to expose LangBot via NodePort
|
||||||
|
# This is useful for testing or when LoadBalancer is not available
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-nodeport
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: NodePort
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 5300
|
||||||
|
# targetPort: 5300
|
||||||
|
# nodePort: 30300 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# nodePort: 30280 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook
|
||||||
291
docs/API_KEY_AUTH.md
Normal file
291
docs/API_KEY_AUTH.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# API Key Authentication
|
||||||
|
|
||||||
|
LangBot now supports API key authentication for external systems to access its HTTP service API.
|
||||||
|
|
||||||
|
## Managing API Keys
|
||||||
|
|
||||||
|
API keys can be managed through the web interface:
|
||||||
|
|
||||||
|
1. Log in to the LangBot web interface
|
||||||
|
2. Click the "API Keys" button at the bottom of the sidebar
|
||||||
|
3. Create, view, copy, or delete API keys as needed
|
||||||
|
|
||||||
|
## Using API Keys
|
||||||
|
|
||||||
|
### Authentication Headers
|
||||||
|
|
||||||
|
Include your API key in the request header using one of these methods:
|
||||||
|
|
||||||
|
**Method 1: X-API-Key header (Recommended)**
|
||||||
|
```
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method 2: Authorization Bearer token**
|
||||||
|
```
|
||||||
|
Authorization: Bearer lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available APIs
|
||||||
|
|
||||||
|
All existing LangBot APIs now support **both user token and API key authentication**. This means you can use API keys to access:
|
||||||
|
|
||||||
|
- **Model Management** - `/api/v1/provider/models/llm` and `/api/v1/provider/models/embedding`
|
||||||
|
- **Bot Management** - `/api/v1/platform/bots`
|
||||||
|
- **Pipeline Management** - `/api/v1/pipelines`
|
||||||
|
- **Knowledge Base** - `/api/v1/knowledge/*`
|
||||||
|
- **MCP Servers** - `/api/v1/mcp/servers`
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
### Authentication Methods
|
||||||
|
|
||||||
|
Each endpoint accepts **either**:
|
||||||
|
1. **User Token** (via `Authorization: Bearer <user_jwt_token>`) - for web UI and authenticated users
|
||||||
|
2. **API Key** (via `X-API-Key` or `Authorization: Bearer <api_key>`) - for external services
|
||||||
|
|
||||||
|
## Example: Model Management
|
||||||
|
|
||||||
|
### List All LLM Models
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"uuid": "model-uuid",
|
||||||
|
"name": "GPT-4",
|
||||||
|
"description": "OpenAI GPT-4 model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {...},
|
||||||
|
"abilities": ["chat", "vision"],
|
||||||
|
"created_at": "2024-01-01T00:00:00",
|
||||||
|
"updated_at": "2024-01-01T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Custom Model",
|
||||||
|
"description": "Description of the model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"api_keys": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"keys": ["sk-..."]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": ["chat"],
|
||||||
|
"extra_args": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Updated Model Name",
|
||||||
|
"description": "Updated description",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Bot Management
|
||||||
|
|
||||||
|
### List All Bots
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Bot
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Pipeline Management
|
||||||
|
|
||||||
|
### List All Pipelines
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Pipeline
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 401 Unauthorized
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "No valid authentication provided (user token or API key required)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Invalid API key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 404 Not Found
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Resource not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -2,
|
||||||
|
"msg": "Error message details"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Keep API keys secure**: Store them securely and never commit them to version control
|
||||||
|
2. **Use HTTPS**: Always use HTTPS in production to encrypt API key transmission
|
||||||
|
3. **Rotate keys regularly**: Create new API keys periodically and delete old ones
|
||||||
|
4. **Use descriptive names**: Give your API keys meaningful names to track their usage
|
||||||
|
5. **Delete unused keys**: Remove API keys that are no longer needed
|
||||||
|
6. **Use X-API-Key header**: Prefer using the `X-API-Key` header for clarity
|
||||||
|
|
||||||
|
## Example: Python Client
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_KEY = "lbk_your_api_key_here"
|
||||||
|
BASE_URL = "http://your-langbot-server:5300"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all models
|
||||||
|
response = requests.get(f"{BASE_URL}/api/v1/provider/models/llm", headers=headers)
|
||||||
|
models = response.json()["data"]["models"]
|
||||||
|
|
||||||
|
print(f"Found {len(models)} models")
|
||||||
|
for model in models:
|
||||||
|
print(f"- {model['name']}: {model['description']}")
|
||||||
|
|
||||||
|
# Create a new bot
|
||||||
|
bot_data = {
|
||||||
|
"name": "My Telegram Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {
|
||||||
|
"token": "your-telegram-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/api/v1/platform/bots",
|
||||||
|
headers=headers,
|
||||||
|
json=bot_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
bot_uuid = response.json()["data"]["uuid"]
|
||||||
|
print(f"Bot created with UUID: {bot_uuid}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all models
|
||||||
|
curl -X GET \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
http://your-langbot-server:5300/api/v1/provider/models/llm
|
||||||
|
|
||||||
|
# Create a new pipeline
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/pipelines
|
||||||
|
|
||||||
|
# Get bot logs
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"from_index": -1,
|
||||||
|
"max_count": 10
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/platform/bots/{bot_uuid}/logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The same endpoints work for both the web UI (with user tokens) and external services (with API keys)
|
||||||
|
- No need to learn different API paths - use the existing API documentation with API key authentication
|
||||||
|
- All endpoints that previously required user authentication now also accept API keys
|
||||||
|
|
||||||
412
docs/MIGRATION_SUMMARY.md
Normal file
412
docs/MIGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# WebChat 到 WebSocket 迁移总结
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
已完全移除旧的基于SSE的WebChat系统,并替换为基于WebSocket的双向实时通信系统。这是一个内置在LangBot中的完整IM系统,支持流式输出。
|
||||||
|
|
||||||
|
## 已删除的文件
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- ❌ `src/langbot/pkg/api/http/controller/groups/pipelines/webchat.py` - 旧的SSE路由
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.py` - 旧的WebChat适配器
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.yaml` - 旧的配置文件
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- ❌ BackendClient中所有SSE相关代码已完全移除
|
||||||
|
- ❌ DebugDialog中所有SSE相关逻辑已完全替换
|
||||||
|
|
||||||
|
## 新增的文件
|
||||||
|
|
||||||
|
### 后端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket连接管理器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_manager.py
|
||||||
|
```
|
||||||
|
- 管理所有并发WebSocket连接
|
||||||
|
- 线程安全的连接池
|
||||||
|
- 按流水线、会话类型分组
|
||||||
|
- 广播和单播消息功能
|
||||||
|
- 连接统计和监控
|
||||||
|
|
||||||
|
**2. WebSocket适配器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_adapter.py
|
||||||
|
```
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- **完整流式支持** (`reply_message_chunk` 方法)
|
||||||
|
- 双向消息流处理
|
||||||
|
- 消息历史管理
|
||||||
|
- 会话管理
|
||||||
|
|
||||||
|
**3. WebSocket路由控制器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/api/http/controller/groups/pipelines/websocket_chat.py
|
||||||
|
```
|
||||||
|
- WebSocket端点处理
|
||||||
|
- REST API接口
|
||||||
|
- 心跳机制
|
||||||
|
- 连接生命周期管理
|
||||||
|
|
||||||
|
**4. 配置文件**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket.yaml
|
||||||
|
```
|
||||||
|
- WebSocket适配器元数据
|
||||||
|
|
||||||
|
### 前端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket客户端**
|
||||||
|
```
|
||||||
|
web/src/app/infra/websocket/WebSocketClient.ts
|
||||||
|
```
|
||||||
|
- WebSocket连接管理
|
||||||
|
- 自动重连(最多5次)
|
||||||
|
- 心跳机制(30秒)
|
||||||
|
- 事件回调系统
|
||||||
|
|
||||||
|
**2. 更新的组件**
|
||||||
|
```
|
||||||
|
web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
|
||||||
|
```
|
||||||
|
- 完全重写,使用WebSocket
|
||||||
|
- 实时连接状态显示
|
||||||
|
- 流式消息支持
|
||||||
|
- 自动重连
|
||||||
|
|
||||||
|
**3. HTTP客户端更新**
|
||||||
|
```
|
||||||
|
web/src/app/infra/http/BackendClient.ts
|
||||||
|
```
|
||||||
|
- 移除所有旧的WebChat API
|
||||||
|
- 仅保留WebSocket API
|
||||||
|
|
||||||
|
### 测试工具
|
||||||
|
|
||||||
|
**Python测试客户端**
|
||||||
|
```
|
||||||
|
test_websocket_client.py
|
||||||
|
```
|
||||||
|
- 单连接交互测试
|
||||||
|
- 多连接并发测试
|
||||||
|
- 命令行工具
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
**使用文档**
|
||||||
|
```
|
||||||
|
WEBSOCKET_README.md
|
||||||
|
```
|
||||||
|
- 完整的API文档
|
||||||
|
- 架构说明
|
||||||
|
- 使用示例
|
||||||
|
- 故障排查
|
||||||
|
|
||||||
|
## 核心变更
|
||||||
|
|
||||||
|
### 后端变更
|
||||||
|
|
||||||
|
**1. botmgr.py**
|
||||||
|
- ❌ 移除 `webchat_proxy_bot`
|
||||||
|
- ✅ 仅保留 `websocket_proxy_bot`
|
||||||
|
- ✅ 更新适配器过滤逻辑(排除`websocket`而非`webchat`)
|
||||||
|
|
||||||
|
**2. 适配器注册**
|
||||||
|
```python
|
||||||
|
# 旧代码(已删除)
|
||||||
|
webchat_adapter_class = self.adapter_dict['webchat']
|
||||||
|
self.webchat_proxy_bot = RuntimeBot(...)
|
||||||
|
|
||||||
|
# 新代码
|
||||||
|
websocket_adapter_class = self.adapter_dict['websocket']
|
||||||
|
self.websocket_proxy_bot = RuntimeBot(
|
||||||
|
uuid='websocket-proxy-bot',
|
||||||
|
name='WebSocket',
|
||||||
|
adapter='websocket',
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端变更
|
||||||
|
|
||||||
|
**1. API调用完全更换**
|
||||||
|
|
||||||
|
旧代码(已删除):
|
||||||
|
```typescript
|
||||||
|
// SSE流式请求
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ is_stream: true })
|
||||||
|
})
|
||||||
|
// 手动解析 text/event-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
新代码:
|
||||||
|
```typescript
|
||||||
|
// WebSocket实时通信
|
||||||
|
const wsClient = new WebSocketClient(pipelineId, sessionType);
|
||||||
|
await wsClient.connect();
|
||||||
|
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
// 流式消息自动处理
|
||||||
|
setMessages(prev => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.sendMessage(messageChain);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 连接状态管理**
|
||||||
|
|
||||||
|
新增功能:
|
||||||
|
- ✅ 实时连接状态指示器(绿色/红色圆点)
|
||||||
|
- ✅ 连接/断开toast提示
|
||||||
|
- ✅ 自动重连逻辑
|
||||||
|
- ✅ 心跳保活
|
||||||
|
|
||||||
|
**3. 流式支持**
|
||||||
|
|
||||||
|
完整的流式消息处理:
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
if (message.is_final) {
|
||||||
|
// 最终消息
|
||||||
|
finalizeBotMessage(message);
|
||||||
|
} else {
|
||||||
|
// 中间消息块,实时更新UI
|
||||||
|
updateBotMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API对比
|
||||||
|
|
||||||
|
### WebSocket端点
|
||||||
|
|
||||||
|
**连接**
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**消息格式**
|
||||||
|
|
||||||
|
客户端发送:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{"type": "Plain", "text": "你好"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器响应(流式):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "你好,我是...",
|
||||||
|
"is_final": false,
|
||||||
|
"timestamp": "2025-01-28T..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/messages/<type>` | GET | 获取消息历史 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/reset/<type>` | POST | 重置会话 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/connections` | GET | 获取连接统计 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/broadcast` | POST | 广播消息 |
|
||||||
|
|
||||||
|
## 流式支持详解
|
||||||
|
|
||||||
|
### 后端流式实现
|
||||||
|
|
||||||
|
**WebSocket Adapter**
|
||||||
|
```python
|
||||||
|
async def reply_message_chunk(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
bot_message,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
quote_origin: bool = False,
|
||||||
|
is_final: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""回复消息块 - 流式"""
|
||||||
|
message_data = WebSocketMessage(
|
||||||
|
id=-1,
|
||||||
|
role='assistant',
|
||||||
|
content=str(message),
|
||||||
|
message_chain=[component.__dict__ for component in message],
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
is_final=is_final and bot_message.tool_calls is None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送到队列,由WebSocket连接处理发送
|
||||||
|
await session.resp_queues[message_id].put(message_data)
|
||||||
|
return message_data.model_dump()
|
||||||
|
|
||||||
|
async def is_stream_output_supported(self) -> bool:
|
||||||
|
"""WebSocket始终支持流式输出"""
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端流式处理
|
||||||
|
|
||||||
|
**DebugDialog组件**
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const existingIndex = prevMessages.findIndex(
|
||||||
|
(msg) => msg.role === 'assistant' && msg.content === 'Generating...'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// 更新正在生成的消息
|
||||||
|
const updatedMessages = [...prevMessages];
|
||||||
|
updatedMessages[existingIndex] = message;
|
||||||
|
return updatedMessages;
|
||||||
|
} else {
|
||||||
|
// 添加新消息
|
||||||
|
return [...prevMessages, message];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
### ⚠️ 不兼容旧版本
|
||||||
|
|
||||||
|
此次迁移**完全不兼容**旧的WebChat系统:
|
||||||
|
|
||||||
|
1. **API端点变更**
|
||||||
|
- 旧: `/api/v1/pipelines/<uuid>/chat/send`
|
||||||
|
- 新: `ws://.../<uuid>/ws/connect`
|
||||||
|
|
||||||
|
2. **通信协议变更**
|
||||||
|
- 旧: HTTP + SSE (Server-Sent Events)
|
||||||
|
- 新: WebSocket (双向)
|
||||||
|
|
||||||
|
3. **流式实现变更**
|
||||||
|
- 旧: `text/event-stream` 格式
|
||||||
|
- 新: WebSocket JSON消息
|
||||||
|
|
||||||
|
### 迁移要求
|
||||||
|
|
||||||
|
使用新系统需要:
|
||||||
|
1. ✅ 前端必须支持WebSocket
|
||||||
|
2. ✅ 后端必须运行新的WebSocket适配器
|
||||||
|
3. ✅ 清除旧的WebChat相关配置
|
||||||
|
|
||||||
|
## 优势对比
|
||||||
|
|
||||||
|
| 特性 | 旧WebChat (SSE) | 新WebSocket |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 双向通信 | ❌ 单向(服务器→客户端) | ✅ 双向 |
|
||||||
|
| 主动推送 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
| 连接管理 | ❌ 无状态 | ✅ 有状态,完整生命周期 |
|
||||||
|
| 流式输出 | ✅ 支持 | ✅ 支持(更优) |
|
||||||
|
| 心跳机制 | ❌ 无 | ✅ 30秒心跳 |
|
||||||
|
| 自动重连 | ❌ 无 | ✅ 最多5次 |
|
||||||
|
| 多连接 | ⚠️ 难以管理 | ✅ 完整支持 |
|
||||||
|
| 连接状态 | ❌ 不可见 | ✅ 实时显示 |
|
||||||
|
| 广播功能 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
### 1. Python测试客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 单连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试(5个连接)
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端测试
|
||||||
|
|
||||||
|
1. 启动LangBot服务器
|
||||||
|
2. 访问前端界面
|
||||||
|
3. 打开流水线调试对话框
|
||||||
|
4. 观察连接状态指示器(左下角圆点)
|
||||||
|
5. 发送消息测试流式响应
|
||||||
|
|
||||||
|
### 3. 浏览器控制台测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/<uuid>/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('已连接');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [{type: 'Plain', text: '你好'}]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log('收到:', JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么完全删除旧代码而不保留兼容性?
|
||||||
|
A: 根据需求,不需要考虑任何对老版本的兼容性,彻底迁移可以避免代码冗余和维护负担。
|
||||||
|
|
||||||
|
### Q: 流式输出如何工作?
|
||||||
|
A:
|
||||||
|
1. 后端通过`reply_message_chunk`发送消息块
|
||||||
|
2. 消息块放入队列
|
||||||
|
3. WebSocket连接从队列取出并发送
|
||||||
|
4. 前端实时更新UI
|
||||||
|
5. `is_final=true`表示最后一块
|
||||||
|
|
||||||
|
### Q: 如何确保连接不断开?
|
||||||
|
A:
|
||||||
|
1. 客户端每30秒发送心跳(ping)
|
||||||
|
2. 服务器响应pong
|
||||||
|
3. 连接断开时自动重连(最多5次)
|
||||||
|
|
||||||
|
### Q: 如何实现后端主动推送?
|
||||||
|
A:
|
||||||
|
1. 调用 `/api/v1/pipelines/<uuid>/ws/broadcast` API
|
||||||
|
2. 消息会被推送到该流水线的所有连接
|
||||||
|
3. 前端通过`onBroadcast`回调接收
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
✅ **完成的工作**
|
||||||
|
- 完全移除旧的WebChat/SSE系统
|
||||||
|
- 实现完整的WebSocket双向通信系统
|
||||||
|
- 支持流式输出
|
||||||
|
- 支持多连接并发
|
||||||
|
- 实现自动重连和心跳机制
|
||||||
|
- 提供完整的测试工具和文档
|
||||||
|
|
||||||
|
✅ **核心特性**
|
||||||
|
- 双向实时通信
|
||||||
|
- 流式消息支持
|
||||||
|
- 多连接管理
|
||||||
|
- 自动重连
|
||||||
|
- 心跳保活
|
||||||
|
- 连接状态可视化
|
||||||
|
- 广播消息
|
||||||
|
|
||||||
|
✅ **技术亮点**
|
||||||
|
- 异步架构(asyncio)
|
||||||
|
- 线程安全的连接管理
|
||||||
|
- 独立的消息队列
|
||||||
|
- 完整的错误处理
|
||||||
|
- 模块化设计
|
||||||
|
|
||||||
|
🎉 系统已完全迁移到WebSocket,无任何旧代码遗留!
|
||||||
117
docs/PYPI_INSTALLATION.md
Normal file
117
docs/PYPI_INSTALLATION.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# LangBot PyPI Package Installation
|
||||||
|
|
||||||
|
## Quick Start with uvx
|
||||||
|
|
||||||
|
The easiest way to run LangBot is using `uvx` (recommended for quick testing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically download and run the latest version of LangBot.
|
||||||
|
|
||||||
|
## Install with pip/uv
|
||||||
|
|
||||||
|
You can also install LangBot as a regular Python package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using pip
|
||||||
|
pip install langbot
|
||||||
|
|
||||||
|
# Using uv
|
||||||
|
uv pip install langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Python module syntax:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation with Frontend
|
||||||
|
|
||||||
|
When published to PyPI, the LangBot package includes the pre-built frontend files. You don't need to build the frontend separately.
|
||||||
|
|
||||||
|
## Data Directory
|
||||||
|
|
||||||
|
When running LangBot as a package, it will create a `data/` directory in your current working directory to store configuration, logs, and other runtime data. You can run LangBot from any directory, and it will set up its data directory there.
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
LangBot supports the following command line options:
|
||||||
|
|
||||||
|
- `--standalone-runtime`: Use standalone plugin runtime
|
||||||
|
- `--debug`: Enable debug mode
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Other Installation Methods
|
||||||
|
|
||||||
|
### PyPI Package (uvx/pip)
|
||||||
|
- **Pros**: Easy to install and update, no need to clone repository or build frontend
|
||||||
|
- **Cons**: Less flexible for development/customization
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
- **Pros**: Isolated environment, easy deployment
|
||||||
|
- **Cons**: Requires Docker
|
||||||
|
|
||||||
|
### Manual Source Installation
|
||||||
|
- **Pros**: Full control, easy to customize and develop
|
||||||
|
- **Cons**: Requires building frontend, managing dependencies manually
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
If you want to contribute or customize LangBot, you should still use the manual installation method by cloning the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot
|
||||||
|
uv sync
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With pip
|
||||||
|
pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uv
|
||||||
|
uv pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uvx (automatically uses latest)
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- Python 3.10.1 or higher
|
||||||
|
- Operating System: Linux, macOS, or Windows
|
||||||
|
|
||||||
|
## Differences from Source Installation
|
||||||
|
|
||||||
|
When running LangBot from the PyPI package (via uvx or pip), there are a few behavioral differences compared to running from source:
|
||||||
|
|
||||||
|
1. **Version Check**: The package version does not prompt for user input when the Python version is incompatible. It simply prints an error message and exits. This makes it compatible with non-interactive environments like containers and CI/CD.
|
||||||
|
|
||||||
|
2. **Working Directory**: The package version does not require being run from the LangBot project root. You can run `langbot` from any directory, and it will create a `data/` directory in your current working directory.
|
||||||
|
|
||||||
|
3. **Frontend Files**: The frontend is pre-built and included in the package, so you don't need to run `npm build` separately.
|
||||||
|
|
||||||
|
These differences are intentional to make the package more user-friendly and suitable for various deployment scenarios.
|
||||||
259
docs/SEEKDB_INTEGRATION.md
Normal file
259
docs/SEEKDB_INTEGRATION.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# SeekDB Vector Database Integration
|
||||||
|
|
||||||
|
This document describes how to use OceanBase SeekDB as the vector database backend for LangBot's knowledge base feature.
|
||||||
|
|
||||||
|
## What is SeekDB?
|
||||||
|
|
||||||
|
**OceanBase SeekDB** is an AI-native search database that unifies relational, vector, text, JSON and GIS in a single engine, enabling hybrid search and in-database AI workflows. It's developed by OceanBase and released under Apache 2.0 license.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Hybrid Search**: Combine vector search, full-text search and relational query in a single statement
|
||||||
|
- **Multi-Model Support**: Support relational, vector, text, JSON and GIS in a single engine
|
||||||
|
- **Lightweight**: Requires as little as 1 CPU core and 2 GB of memory
|
||||||
|
- **Multiple Deployment Modes**: Supports both embedded mode and client/server mode
|
||||||
|
- **MySQL Compatible**: Powered by OceanBase engine with full ACID compliance and MySQL compatibility
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
SeekDB support is automatically included when you install LangBot. The required dependency `pyseekdb` is listed in `pyproject.toml`.
|
||||||
|
|
||||||
|
If you need to install it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Platform Compatibility
|
||||||
|
|
||||||
|
### Embedded Mode
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full embedded mode support via `pylibseekdb` |
|
||||||
|
| macOS | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
| Windows | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
|
||||||
|
**Important**: Embedded mode requires the `pylibseekdb` library, which is only available on Linux. If you're on macOS or Windows, you must use server mode.
|
||||||
|
|
||||||
|
### Server Mode (Docker)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full Docker support |
|
||||||
|
| macOS | ⚠️ Known Issue | Docker container initialization failure - [See Issue #36](https://github.com/oceanbase/seekdb/issues/36) |
|
||||||
|
| Windows | ⚠️ Untested | Should work but not yet tested |
|
||||||
|
|
||||||
|
**macOS Users**: Currently, SeekDB Docker containers have an initialization issue on macOS ([oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36)). Until this is resolved, we recommend:
|
||||||
|
- Using ChromaDB or Qdrant as alternatives
|
||||||
|
- Connecting to a remote SeekDB server on Linux if available
|
||||||
|
|
||||||
|
### Server Mode (Remote Connection)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| All Platforms | ✅ Supported | Connect to SeekDB running on a remote Linux server |
|
||||||
|
|
||||||
|
**Recommendation for macOS/Windows users**: Deploy SeekDB on a Linux server and connect via server mode configuration.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Embedded Mode (Recommended for Development)
|
||||||
|
|
||||||
|
Embedded mode runs SeekDB directly within the LangBot process, storing data locally. This is the simplest setup and requires no external services.
|
||||||
|
|
||||||
|
Edit your `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: embedded
|
||||||
|
path: './data/seekdb' # Path to store SeekDB data
|
||||||
|
database: 'langbot' # Database name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Mode (For Production)
|
||||||
|
|
||||||
|
Server mode connects to a remote SeekDB server or OceanBase server. This is recommended for production deployments.
|
||||||
|
|
||||||
|
#### SeekDB Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: '' # Can also use SEEKDB_PASSWORD env var
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OceanBase Server
|
||||||
|
|
||||||
|
If you're using OceanBase with seekdb capabilities:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
tenant: 'sys' # OceanBase tenant name
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
|-----------|----------|--------------|-------------|
|
||||||
|
| `mode` | No | `embedded` | Deployment mode: `embedded` or `server` |
|
||||||
|
| `path` | No | `./data/seekdb` | Data directory for embedded mode |
|
||||||
|
| `database` | No | `langbot` | Database name |
|
||||||
|
| `host` | No | `localhost` | Server host (server mode only) |
|
||||||
|
| `port` | No | `2881` | Server port (server mode only) |
|
||||||
|
| `user` | No | `root` | Username (server mode only) |
|
||||||
|
| `password` | No | `''` | Password (server mode only) |
|
||||||
|
| `tenant` | No | None | OceanBase tenant (optional, server mode only) |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once configured, SeekDB will be used automatically for all knowledge base operations in LangBot:
|
||||||
|
|
||||||
|
1. **Creating Knowledge Bases**: Vectors will be stored in SeekDB collections
|
||||||
|
2. **Adding Documents**: Document embeddings will be indexed in SeekDB
|
||||||
|
3. **Searching**: Vector similarity search will use SeekDB's efficient indexing
|
||||||
|
4. **Deleting**: Document removal will delete vectors from SeekDB
|
||||||
|
|
||||||
|
No code changes are required - just update your configuration!
|
||||||
|
|
||||||
|
## Architecture Details
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The SeekDB adapter is implemented in `src/langbot/pkg/vector/vdbs/seekdb.py` and follows the same `VectorDatabase` interface as Chroma and Qdrant adapters.
|
||||||
|
|
||||||
|
Key methods:
|
||||||
|
- `add_embeddings()`: Add vectors with metadata to a collection
|
||||||
|
- `search()`: Perform vector similarity search
|
||||||
|
- `delete_by_file_id()`: Delete vectors by file ID metadata
|
||||||
|
- `get_or_create_collection()`: Manage collections
|
||||||
|
- `delete_collection()`: Remove entire collections
|
||||||
|
|
||||||
|
### Vector Storage
|
||||||
|
|
||||||
|
- Collections are created with HNSW (Hierarchical Navigable Small World) index
|
||||||
|
- Default distance metric: Cosine similarity
|
||||||
|
- Default vector dimension: 384 (adjusts automatically based on embeddings)
|
||||||
|
- Metadata is stored alongside vectors for filtering
|
||||||
|
|
||||||
|
## Advantages Over Other Vector Databases
|
||||||
|
|
||||||
|
### vs. ChromaDB
|
||||||
|
- ✅ Better MySQL compatibility
|
||||||
|
- ✅ Hybrid search capabilities (vector + full-text + SQL)
|
||||||
|
- ✅ Production-grade distributed mode support
|
||||||
|
- ✅ Lightweight embedded mode
|
||||||
|
|
||||||
|
### vs. Qdrant
|
||||||
|
- ✅ SQL query support
|
||||||
|
- ✅ MySQL ecosystem integration
|
||||||
|
- ✅ Simpler deployment (no Docker required for embedded mode)
|
||||||
|
- ✅ Multi-model data support (not just vectors)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Import Error
|
||||||
|
|
||||||
|
If you see: `ImportError: pyseekdb is not installed`
|
||||||
|
|
||||||
|
Solution:
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embedded Mode Error on macOS/Windows
|
||||||
|
|
||||||
|
**Error**:
|
||||||
|
```
|
||||||
|
RuntimeError: Embedded Client is not available because pylibseekdb is not available.
|
||||||
|
Please install pylibseekdb (Linux only) or use RemoteServerClient (host/port) instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `pylibseekdb` is only available on Linux platforms.
|
||||||
|
|
||||||
|
**Solution**: Use server mode instead:
|
||||||
|
1. Deploy SeekDB on a Linux server or VM
|
||||||
|
2. Configure LangBot to use server mode:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'your-seekdb-server-ip'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative**: Use ChromaDB or Qdrant, which work on all platforms:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: chroma # or qdrant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Container Fails on macOS
|
||||||
|
|
||||||
|
**Symptoms**:
|
||||||
|
```bash
|
||||||
|
docker run -d -p 2881:2881 oceanbase/seekdb:latest
|
||||||
|
# Container exits immediately with code 30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error in logs**:
|
||||||
|
```
|
||||||
|
[ERROR] Code: Agent.SeekDB.Not.Exists
|
||||||
|
Message: initialize failed: init agent failed: SeekDB not exists in current directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: This is a known issue with SeekDB Docker containers on macOS. See [oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36).
|
||||||
|
|
||||||
|
**Status**: Under investigation by OceanBase team.
|
||||||
|
|
||||||
|
**Workaround Options**:
|
||||||
|
1. **Use alternatives**: ChromaDB or Qdrant work perfectly on macOS
|
||||||
|
2. **Remote server**: Deploy SeekDB on a Linux server and connect remotely
|
||||||
|
3. **Wait for fix**: Monitor the GitHub issue for updates
|
||||||
|
|
||||||
|
### Connection Error (Server Mode)
|
||||||
|
|
||||||
|
If SeekDB server is not reachable, check:
|
||||||
|
1. Server is running: `ps aux | grep observer`
|
||||||
|
2. Port is accessible: `nc -zv localhost 2881`
|
||||||
|
3. Credentials are correct in config
|
||||||
|
4. Firewall allows connections on port 2881
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
For large datasets:
|
||||||
|
- Use server mode instead of embedded mode
|
||||||
|
- Ensure adequate memory allocation
|
||||||
|
- Consider using OceanBase distributed mode for very large scale
|
||||||
|
- Adjust HNSW index parameters if needed
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- SeekDB GitHub: https://github.com/oceanbase/seekdb
|
||||||
|
- pyseekdb SDK: https://github.com/oceanbase/pyseekdb
|
||||||
|
- OceanBase Documentation: https://oceanbase.ai
|
||||||
|
- LangBot Documentation: https://docs.langbot.app
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
SeekDB is licensed under Apache License 2.0.
|
||||||
394
docs/WEBSOCKET_README.md
Normal file
394
docs/WEBSOCKET_README.md
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
# LangBot WebSocket 双向通信系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
这是一个内置在 LangBot 中的完整 IM (即时通讯) 系统,支持:
|
||||||
|
|
||||||
|
- ✅ WebSocket 双向实时通信
|
||||||
|
- ✅ 多个客户端并发连接
|
||||||
|
- ✅ 前端到后端的消息发送
|
||||||
|
- ✅ 后端到前端的主动推送
|
||||||
|
- ✅ 流式响应支持
|
||||||
|
- ✅ 连接管理和会话隔离
|
||||||
|
- ✅ 心跳机制
|
||||||
|
- ✅ 广播消息功能
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **WebSocketConnectionManager** (`websocket_manager.py`)
|
||||||
|
- 管理所有活跃的 WebSocket 连接
|
||||||
|
- 支持按流水线、会话类型查询连接
|
||||||
|
- 提供广播和单播功能
|
||||||
|
- 线程安全的并发访问控制
|
||||||
|
|
||||||
|
2. **WebSocketAdapter** (`websocket_adapter.py`)
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- 处理消息的接收和发送
|
||||||
|
- 支持流式输出
|
||||||
|
- 管理消息历史
|
||||||
|
|
||||||
|
3. **WebSocketChatRouterGroup** (`websocket_chat.py`)
|
||||||
|
- WebSocket 路由控制器
|
||||||
|
- 处理连接建立、消息收发
|
||||||
|
- 实现心跳机制
|
||||||
|
- 提供 REST API 接口
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### WebSocket 连接
|
||||||
|
|
||||||
|
#### 建立连接
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `pipeline_uuid`: 流水线 UUID (必需)
|
||||||
|
- `session_type`: 会话类型,可选 `person` 或 `group` (默认: `person`)
|
||||||
|
|
||||||
|
**连接成功响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "connected",
|
||||||
|
"connection_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"pipeline_uuid": "your-pipeline-uuid",
|
||||||
|
"session_type": "person",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息格式
|
||||||
|
|
||||||
|
#### 客户端发送消息
|
||||||
|
|
||||||
|
**发送聊天消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{
|
||||||
|
"type": "Plain",
|
||||||
|
"text": "你好,这是一条测试消息"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**发送心跳:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**主动断开连接:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "disconnect"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务器响应消息
|
||||||
|
|
||||||
|
**聊天响应 (流式):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "这是机器人的回复",
|
||||||
|
"message_chain": [...],
|
||||||
|
"timestamp": "2025-01-28T12:00:00",
|
||||||
|
"is_final": false,
|
||||||
|
"connection_id": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**心跳响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pong",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**广播消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "broadcast",
|
||||||
|
"message": "这是一条广播消息",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"message": "错误描述"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API 接口
|
||||||
|
|
||||||
|
#### 1. 获取消息历史
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/messages/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"messages": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 重置会话
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/reset/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Session reset successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 获取连接统计
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"stats": {
|
||||||
|
"total_connections": 5,
|
||||||
|
"pipelines": 2,
|
||||||
|
"connections_by_pipeline": {
|
||||||
|
"pipeline-1": 3,
|
||||||
|
"pipeline-2": 2
|
||||||
|
},
|
||||||
|
"connections_by_session_type": {
|
||||||
|
"person": 4,
|
||||||
|
"group": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"connection_id": "...",
|
||||||
|
"session_type": "person",
|
||||||
|
"created_at": "2025-01-28T12:00:00",
|
||||||
|
"last_active": "2025-01-28T12:05:00",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 广播消息 (后端主动推送)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/broadcast
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "这是一条广播消息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Broadcast sent successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Python 客户端示例
|
||||||
|
|
||||||
|
使用提供的测试客户端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pip install websockets
|
||||||
|
|
||||||
|
# 单个连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 客户端示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 建立 WebSocket 连接
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/your-pipeline-uuid/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
// 连接建立
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket 连接已建立');
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [
|
||||||
|
{
|
||||||
|
type: 'Plain',
|
||||||
|
text: '你好'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'connected') {
|
||||||
|
console.log('连接成功:', data.connection_id);
|
||||||
|
} else if (data.type === 'response') {
|
||||||
|
console.log('机器人回复:', data.data.content);
|
||||||
|
if (data.data.is_final) {
|
||||||
|
console.log('响应完成');
|
||||||
|
}
|
||||||
|
} else if (data.type === 'broadcast') {
|
||||||
|
console.log('收到广播:', data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket 连接已关闭');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket 错误:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送心跳
|
||||||
|
setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ping' }));
|
||||||
|
}
|
||||||
|
}, 30000); // 每 30 秒发送一次心跳
|
||||||
|
```
|
||||||
|
|
||||||
|
## 特性说明
|
||||||
|
|
||||||
|
### 1. 多连接支持
|
||||||
|
|
||||||
|
系统支持同时建立多个 WebSocket 连接,每个连接都有唯一的 `connection_id`。连接按照流水线和会话类型进行分组管理。
|
||||||
|
|
||||||
|
### 2. 双向通信
|
||||||
|
|
||||||
|
- **前端 → 后端**: 客户端可以主动发送消息给服务器
|
||||||
|
- **后端 → 前端**: 服务器可以通过广播 API 主动推送消息给客户端
|
||||||
|
|
||||||
|
### 3. 流式响应
|
||||||
|
|
||||||
|
支持流式输出,机器人的响应会分块发送,客户端可以实时显示部分响应内容。
|
||||||
|
|
||||||
|
### 4. 会话隔离
|
||||||
|
|
||||||
|
支持 `person` 和 `group` 两种会话类型,不同类型的会话消息历史互不影响。
|
||||||
|
|
||||||
|
### 5. 连接管理
|
||||||
|
|
||||||
|
- 自动追踪连接状态
|
||||||
|
- 记录最后活跃时间
|
||||||
|
- 支持连接统计查询
|
||||||
|
- 连接断开时自动清理资源
|
||||||
|
|
||||||
|
### 6. 心跳机制
|
||||||
|
|
||||||
|
客户端可以定期发送 `ping` 消息,服务器会响应 `pong`,用于保持连接活跃和检测连接状态。
|
||||||
|
|
||||||
|
## 架构优势
|
||||||
|
|
||||||
|
1. **高并发**: 使用 asyncio 异步架构,支持大量并发连接
|
||||||
|
2. **可扩展**: 模块化设计,易于扩展新功能
|
||||||
|
3. **线程安全**: 连接管理器使用锁机制保证并发安全
|
||||||
|
4. **消息队列**: 每个连接独立的发送队列,避免消息混乱
|
||||||
|
5. **灵活路由**: 支持按流水线、会话类型灵活路由消息
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **认证**: 当前 WebSocket 连接不需要认证,生产环境建议添加认证机制
|
||||||
|
2. **心跳**: 建议客户端实现心跳机制,避免连接超时
|
||||||
|
3. **重连**: 客户端应实现断线重连逻辑
|
||||||
|
4. **消息大小**: 注意控制单条消息大小,避免内存溢出
|
||||||
|
5. **连接数限制**: 生产环境建议设置最大连接数限制
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 连接失败
|
||||||
|
|
||||||
|
1. 检查流水线 UUID 是否正确
|
||||||
|
2. 检查服务器是否正常运行
|
||||||
|
3. 检查防火墙设置
|
||||||
|
|
||||||
|
### 消息发送失败
|
||||||
|
|
||||||
|
1. 检查消息格式是否正确
|
||||||
|
2. 检查连接是否仍然活跃
|
||||||
|
3. 查看服务器日志获取详细错误信息
|
||||||
|
|
||||||
|
### 性能问题
|
||||||
|
|
||||||
|
1. 检查并发连接数是否过多
|
||||||
|
2. 检查消息处理速度
|
||||||
|
3. 考虑使用连接池或负载均衡
|
||||||
|
|
||||||
|
## 开发调试
|
||||||
|
|
||||||
|
启用详细日志:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_adapter').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_manager').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.api.http.controller.groups.pipelines.websocket_chat').setLevel(logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续改进建议
|
||||||
|
|
||||||
|
1. 添加用户认证和授权机制
|
||||||
|
2. 实现消息持久化
|
||||||
|
3. 添加消息加密
|
||||||
|
4. 实现更丰富的消息类型 (图片、文件等)
|
||||||
|
5. 添加消息已读/未读状态
|
||||||
|
6. 实现群组聊天功能
|
||||||
|
7. 添加在线状态显示
|
||||||
|
8. 实现消息撤回功能
|
||||||
149
docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md
Normal file
149
docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Agent-owned Context 协议设计
|
||||||
|
|
||||||
|
本文档描述插件化 AgentRunner 场景下的上下文边界**设计理由**。结论先行:LangBot 不应成为最终 agentic context manager;它提供 context substrate,AgentRunner 或其背后的 runtime 自己决定如何管理历史、压缩、召回和 KV cache。
|
||||||
|
|
||||||
|
> 涉及的数据结构(`AgentRunContext`、`ContextAccess`、`AgentRunAPIProxy` 等)唯一定义在 [PROTOCOL_V1.md](./PROTOCOL_V1.md)。本文只讲语义和约束,不重抄 schema。实现进度见 [PROGRESS.md](./PROGRESS.md)。
|
||||||
|
|
||||||
|
## 1. 设计原则
|
||||||
|
|
||||||
|
### 1.1 Agent 拥有上下文策略
|
||||||
|
|
||||||
|
不同 runner 背后的 runtime 差异很大:
|
||||||
|
|
||||||
|
- 官方 local-agent 可能依赖 LangBot 的模型、工具、知识库和存储。
|
||||||
|
- Claude Code SDK / Codex 类 runtime 有自己的 session、transcript、tool loop 和上下文压缩。
|
||||||
|
- Pi Agent SDK 或外部 agent 平台可能只需要当前事件和一个外部 conversation key。
|
||||||
|
|
||||||
|
因此 LangBot 不应强行决定最终传给模型的历史窗口。Host 只提供:当前事件的完整结构化信息、稳定身份和会话引用、可授权读取的 history / event / artifact / state API、可投影给外部 harness 的 scoped context / MCP / skill / resource refs、payload hard cap 和权限 guardrail。
|
||||||
|
|
||||||
|
### 1.2 Host 不定义通用历史窗口
|
||||||
|
|
||||||
|
历史窗口策略不是 AgentRunner 协议或 Query entry adapter 的核心概念。Host 只提供 history pull API、cursor、hard cap 和权限边界;runner 自己决定是否读取、读取多少、如何截断和压缩。
|
||||||
|
|
||||||
|
正确的问题不是"LangBot 每轮裁几轮历史给 agent",而是:
|
||||||
|
|
||||||
|
- 这类 runner 是否自管 context?
|
||||||
|
- 事件到来时 host 应 inline 哪些最小信息?
|
||||||
|
- agent 需要更多上下文时通过什么 API 拉取?
|
||||||
|
- host 如何保证安全、可审计和可分页?
|
||||||
|
|
||||||
|
### 1.3 Host 保存事实源,Agent 管理 working context
|
||||||
|
|
||||||
|
三类数据要分开:
|
||||||
|
|
||||||
|
- `EventLog`: Host 保存原始事件、工具调用、投递结果、错误和系统事件。
|
||||||
|
- `Transcript`: Host 从 EventLog 投影出的对话视图,用于 UI、审计和按需历史读取。
|
||||||
|
- `Working context`: Agent 本轮实际送进模型或 runtime 的上下文,由 AgentRunner 决定。
|
||||||
|
|
||||||
|
LangBot 不提供 host-side inline history window。简单 runner 如果需要历史窗口,应在 runner 内部通过 Host history API 拉取并裁剪。
|
||||||
|
|
||||||
|
## 2. Event 到来时传什么
|
||||||
|
|
||||||
|
默认 `AgentRunContext`(PROTOCOL_V1 §5.2)应尽量小且稳定。默认规则:
|
||||||
|
|
||||||
|
- Host MUST NOT inline full history by default.
|
||||||
|
- Host SHOULD inline only current event / input and context handles.
|
||||||
|
- Runner owns working-context assembly.
|
||||||
|
- Runner MAY use Host history / event / artifact / state / storage API when authorized.
|
||||||
|
- Official runners MUST consume Host infrastructure through the same public API as third-party runners.
|
||||||
|
|
||||||
|
### 2.1 必须 inline 的内容
|
||||||
|
|
||||||
|
当前 event 的类型/id/时间/source;当前输入文本和结构化内容;附件/文件/图片的 metadata 和 artifact ref;actor / subject / conversation / thread / bot / workspace;delivery 能力;已授权资源列表;context cursors 和可用 API 能力;Agent/runner config。这些是 agent 决定下一步所需的最低信息。
|
||||||
|
|
||||||
|
### 2.2 默认不 inline 的内容
|
||||||
|
|
||||||
|
完整历史消息、大文件全文、大工具结果、全量知识库内容、平台原始 payload 大对象、每轮重新生成的大段 summary。这些会破坏跨进程序列化成本、泄露范围、KV cache 稳定性,也会迫使 host 替 agent 做 context 策略。
|
||||||
|
|
||||||
|
### 2.3 不提供 Host Inline History Window
|
||||||
|
|
||||||
|
`AgentRunContext` 不包含 `bootstrap` 字段。Host 不下发历史窗口,也不通过 Pipeline 配置决定窗口大小。runner 若需要类似 `recent_tail` 的策略,应在自己的 manifest/config schema 中声明参数,并在 runner 内部通过 history API 读取、裁剪和压缩。Host 只负责权限、分页、hard cap 和事实源。
|
||||||
|
|
||||||
|
## 3. ContextAccess 的作用
|
||||||
|
|
||||||
|
`ContextAccess`(PROTOCOL_V1 §5.8)是 host 交给 agent 的上下文读取入口描述,告诉 agent:当前事件位于哪条 conversation / thread、若需要更多历史从哪个 cursor 开始拉、host inline 了什么没 inline 什么、当前 run 有哪些 context API 权限。
|
||||||
|
|
||||||
|
## 4. Agent 如何获取更多上下文
|
||||||
|
|
||||||
|
所有 API 都走 `AgentRunAPIProxy`(PROTOCOL_V1 §8),由 host 用 `run_id` 校验。
|
||||||
|
|
||||||
|
### 4.1 History
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.history.page(conversation_id=ctx.context.conversation_id,
|
||||||
|
before_cursor=ctx.context.latest_cursor,
|
||||||
|
limit=50, direction="backward", include_artifacts=False)
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class HistoryPage(BaseModel):
|
||||||
|
items: list[TranscriptItem]
|
||||||
|
next_cursor: str | None
|
||||||
|
prev_cursor: str | None
|
||||||
|
has_more: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:`limit` 有 host hard cap;默认只能读当前 conversation / thread;跨会话读取需 manifest permission + binding policy;返回 artifact ref,不默认返回大文件内容。
|
||||||
|
|
||||||
|
### 4.2 Search
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.history.search(query="用户之前提到的数据库连接信息",
|
||||||
|
filters={"conversation_id": ..., "event_types": ["message.received"]},
|
||||||
|
top_k=10)
|
||||||
|
```
|
||||||
|
|
||||||
|
Search 可先用数据库全文索引,后续接 embedding recall。它是 host 检索能力,不等于 agent 的长期记忆策略。
|
||||||
|
|
||||||
|
### 4.3 Event / Artifact / State
|
||||||
|
|
||||||
|
- Event API(`events.get` / `events.page`)用于读取非消息事件、工具事件、系统事件。Agent 不应把所有事件都当成 user/assistant message。
|
||||||
|
- Artifact API(`artifacts.metadata` / `read_range` / `open_stream`)必须校验 artifact 所属 conversation / run / binding,校验 MIME / 大小 / 过期 / 权限,大文件按 range/stream 读取,工具大结果也应 artifact 化。
|
||||||
|
- State API(`state.get` / `set`)是可选寄宿能力。自管 runtime 可以完全不用;依附 LangBot 的官方 runner 可以使用,例如 `external.session_id`、`summary.checkpoint`。
|
||||||
|
|
||||||
|
### 4.4 大文件与工具协作
|
||||||
|
|
||||||
|
大文件、多模态输入和工具产物不要内联进 prompt 或 tool result:message/content 里只放小文本和必要摘要;大文件、图片、音频、长工具输出返回 artifact ref(`artifact_id`、`mime_type`、`size`、`digest`、`summary`、`expires_at`、`permissions`)。工具之间传递大结果时传 artifact ref,不传完整 blob。Host 校验 artifact 是否属于当前 run / scope,默认不允许插件直接读任意本地路径;临时文件应有 TTL 和清理机制。
|
||||||
|
|
||||||
|
### 4.5 External harness context projection
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 这类 runtime 通常已有自己的 session、工具 loop、MCP 加载、上下文压缩和工作目录。LangBot 不应把它们改造成"host prompt assembler",而应提供可审计的事件和资源投影。推荐 projection 形态:
|
||||||
|
|
||||||
|
- `agent-context.json`:结构化 JSON,包含 `run_id`、`event`、`actor`、`subject`、`input`、`delivery`、`resources`、`context`、`state`、`runtime`。
|
||||||
|
- `LANGBOT_CONTEXT.md`:人类可读摘要。
|
||||||
|
- `resources`:只包含本次 run 授权后的句柄,不暴露 Host 内部私有对象。
|
||||||
|
- `skills`:已授权 skill 投影为目标 harness 可读目录(如 Claude Code 的 `.claude/skills/<name>/SKILL.md`)。
|
||||||
|
- `MCP config`:scoped MCP 配置,runner adapter 转成目标 harness 的配置文件或 CLI 参数。
|
||||||
|
- `state pointers`:外部 session id、working directory、checkpoint 等小型 JSON 状态通过 Host state API 保存。
|
||||||
|
|
||||||
|
当前 Claude Code runner 使用 schema `langbot.agent_runner.external_harness_context.v1`(现状见 OFFICIAL_RUNNER_PLUGINS §8)。这类 projection 是"把 LangBot 事实源和授权资源交给 harness",不是"由 LangBot 决定最终模型上下文"。
|
||||||
|
|
||||||
|
## 5. Runner manifest 中的上下文声明
|
||||||
|
|
||||||
|
`AgentRunnerContextPolicy`(PROTOCOL_V1 §4.5)声明 runner 的上下文能力:`supports_history_pull` / `supports_history_search` / `supports_artifact_pull` / `owns_compaction` / `wants_static_context_refs`。它表示 Host 只给当前事件和 context handles;runner 自己决定是否拉取历史、是否搜索、何时摘要、如何构造最终 prompt。
|
||||||
|
|
||||||
|
## 6. KV cache 友好的上下文管理
|
||||||
|
|
||||||
|
支持 Claude Code SDK、Codex、Pi Agent SDK 等 runtime 时,必须避免每轮由 LangBot 重组大块 prompt:
|
||||||
|
|
||||||
|
- 稳定 session key:`workspace/bot/binding/runner/conversation/thread`。
|
||||||
|
- 静态内容使用 `ref + version/hash`(`ctx.runtime.static_refs`):system prompt、resource manifest、tool schema、platform policy。
|
||||||
|
- 每轮只传 delta:当前 event、artifact refs、少量 runtime metadata。
|
||||||
|
- 历史 append-only:不要每轮改写同一段 history 文本。
|
||||||
|
- Summary checkpoint 稳定:只有压缩发生时产生新 checkpoint。
|
||||||
|
- 大文件和工具结果 artifact 化。
|
||||||
|
- Tool/context API schema 稳定,数据通过 API 拉取而非塞入 prompt。
|
||||||
|
- 对自管 runtime,优先让它复用自身 session/cache,而不是强制 LangBot 每轮重放 transcript。
|
||||||
|
- LiteLLM 接入后,模型窗口元信息应作为 resource/runtime metadata 暴露给 runner,由 runner 决定预算和压缩策略。
|
||||||
|
|
||||||
|
## 7. Host guardrail
|
||||||
|
|
||||||
|
Agent 自管 context 不代表无限制访问。LangBot 仍必须控制:每次 run 的 active `run_id`、runner identity、当前 binding 的 resource policy、conversation / actor / subject scope、page size / artifact read size / API rate limit、跨会话读取权限、数据脱敏和敏感变量过滤、审计日志。Host 不负责"最佳上下文策略",但负责"不越权、不爆内存、不不可审计"。
|
||||||
|
|
||||||
|
## 8. 官方 runner 与业务编排边界
|
||||||
|
|
||||||
|
官方 runner 插件可以把状态寄宿在 LangBot,但必须和第三方 runner 一样通过公开 Host API 消费。LangBot core 不内置官方 agent 的业务流程(prompt 组装、tool loop、RAG 编排、summary/compaction、"local-agent 专用"状态字段)。
|
||||||
|
|
||||||
|
官方 local-agent 应作为"依附 LangBot 基础设施的复杂 runner 参考实现":transcript/history 通过 `api.history` 读取,summary/checkpoint/外部 session id/用户偏好通过 `api.state` 或 `api.storage` 保存,图片/文件/工具大结果通过 `api.artifacts` 读取,模型/工具/知识库通过 `api.models` / `api.tools` / `api.knowledge` 调用。这样 LangBot 保持为通用 agent host,不变成内置 agent 框架。具体迁移要求见 [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md)。
|
||||||
97
docs/agent-runner-pluginization/EVENT_BASED_AGENT.md
Normal file
97
docs/agent-runner-pluginization/EVENT_BASED_AGENT.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Event Based Agent 预留设计
|
||||||
|
|
||||||
|
> **future design note**,不是当前分支实现范围。EventGateway、EventRouter、Event subscription/notification 由其他分支实现;本分支只预留 event-first 入口和 envelope/binding models。实现进度见 [PROGRESS.md](./PROGRESS.md)。
|
||||||
|
>
|
||||||
|
> 数据结构唯一定义在 [PROTOCOL_V1.md](./PROTOCOL_V1.md)(runner 可见)与 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md)(Host 内部模型);本文只讲 EBA 语义,不重抄 schema。
|
||||||
|
|
||||||
|
本文描述未来 EBA 接入时,事件如何进入 LangBot、如何触发 AgentRunner,以及如何复用插件化 agent 基础设施。本阶段不实现完整 EventBus / EventRouter / Platform API,目标是把协议边界设计对,避免当前消息入口继续绑死 Pipeline 和用户文本消息。
|
||||||
|
|
||||||
|
## 1. 设计目标
|
||||||
|
|
||||||
|
- 消息、撤回、入群、好友申请、定时任务、API 调用都能抽象为 host event。
|
||||||
|
- EventRouter 可以根据 event type、bot、workspace、conversation、actor、subject 解析 `AgentBinding`。
|
||||||
|
- AgentRunner 通过同一套 orchestrator 被调用。
|
||||||
|
- 非消息事件不伪造成用户文本消息。
|
||||||
|
- 平台动作执行通过显式 capability / permission / result type 预留,不混入普通文本回复。
|
||||||
|
|
||||||
|
## 2. 事件不是消息
|
||||||
|
|
||||||
|
`message.received` 只是事件的一种。协议不应假设:一定有用户文本、一定有 conversation history、一定要返回一条聊天消息、actor 一定等于 sender、subject 一定等于当前消息。
|
||||||
|
|
||||||
|
| event_type | actor | subject | input |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `message.received` | 发消息的人 | 当前消息 | 文本、图片、文件等 |
|
||||||
|
| `message.recalled` | 撤回操作者,未知时为系统 | 被撤回消息 | 通常为空 |
|
||||||
|
| `group.member_joined` | 新成员或邀请人 | 群/成员关系 | 通常为空 |
|
||||||
|
| `friend.request_received` | 申请人 | 好友申请 | 验证消息或申请理由 |
|
||||||
|
| `schedule.triggered` | 系统 | 定时任务 | 任务 payload |
|
||||||
|
| `api.invoked` | API caller | API request | request payload |
|
||||||
|
|
||||||
|
## 3. 稳定事件名
|
||||||
|
|
||||||
|
先保留的稳定事件名(作为插件协议的一部分保持稳定):
|
||||||
|
|
||||||
|
- `message.received`
|
||||||
|
- `message.recalled`
|
||||||
|
- `group.member_joined`
|
||||||
|
- `friend.request_received`
|
||||||
|
|
||||||
|
平台原始事件名只能进入 `ctx.event.source_event_type` / `raw_ref`,不能成为 `ctx.event.event_type` 的公共契约。
|
||||||
|
|
||||||
|
## 4. Event Envelope 与 Binding
|
||||||
|
|
||||||
|
- 入口事件用 `AgentEventEnvelope`(HOST_SDK §4.1)承载;顶层字段使用 LangBot 稳定协议名,平台原始事件名和原始 payload 放 `metadata` / `raw_ref`。
|
||||||
|
- 触发关系用 `AgentBinding`(HOST_SDK §4.2)表达。EBA 阶段 binding 通过 `event_types`、`scope`、`filters` 决定哪些事件触发当前 bot / channel 绑定的 Agent。
|
||||||
|
|
||||||
|
目标产品语义:一个 bot / IM channel 在同一时间只绑定一个负责 agentic
|
||||||
|
处理的 Agent;一个 Agent 可以被多个 bot / channel 复用。因此 EBA 主线按
|
||||||
|
single-agent dispatch 设计,不做默认 fan-out。
|
||||||
|
|
||||||
|
Binding scope 示例:workspace 全局、bot 级、platform channel 级、conversation / group / thread 级、user / actor 级。旧 Pipeline 可迁移为 `message.received` 的临时 binding source,但目标持久配置应是 Agent,不是 Pipeline。
|
||||||
|
|
||||||
|
Event Source 可包括:`platform_adapter`(飞书、QQ、微信、Telegram 等)、`webui`、`http_api`、`scheduler`、`system`。EventRouter 不应写死平台 adapter 的类名。
|
||||||
|
|
||||||
|
## 5. EventRouter 调用链
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform Adapter / WebUI / API
|
||||||
|
-> Event Gateway normalize payload
|
||||||
|
-> EventLog append raw event
|
||||||
|
-> EventRouter resolve one effective AgentBinding
|
||||||
|
-> AgentRunOrchestrator.run(event, binding)
|
||||||
|
-> AgentRunContextBuilder.build(event, binding)
|
||||||
|
-> PluginRuntimeConnector.run_agent()
|
||||||
|
-> AgentRunResult stream
|
||||||
|
-> DeliveryController render / platform action
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:必须复用现有 orchestrator,不能为 EBA 单独实现另一套 plugin runner 调用协议;非消息事件不能绕过 resource authorization;delivery 和 platform action 走统一权限模型;外部 harness runner 也通过同一套 envelope/binding/context/result 协议接入,不为 Claude Code / Codex / Kimi 单独发明队列协议。
|
||||||
|
|
||||||
|
若未来产品需要 observer agent、多个 agent 并行处理同一事件、或多 runner
|
||||||
|
裁决,应另行设计 fan-out 合并、delivery 冲突、state 写入冲突、platform
|
||||||
|
action 审批和 audit 语义。当前 EBA 预留不隐含这些能力。
|
||||||
|
|
||||||
|
## 6. 平台动作执行
|
||||||
|
|
||||||
|
EBA 后 `action.requested`(PROTOCOL_V1 §7.2,当前仅 telemetry 不执行)将用于请求 host 执行平台动作:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "action.requested",
|
||||||
|
"data": { "action": "friend.request.accept",
|
||||||
|
"target": {"platform": "wechat", "request_id": "..."},
|
||||||
|
"reason": "policy matched" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Host 必须校验:runner manifest 是否声明 `platform_api` capability、binding 是否授权该 action、actor / bot / workspace 是否允许、是否需要人工审批。EBA 还可能预留 `delivery.requested`(请求投递到某 surface)。
|
||||||
|
|
||||||
|
Delivery 方面,event 不一定回复到当前聊天窗口:消息事件通常带 reply target;系统事件可能没有默认 reply target,需要 runner 返回 `action.requested` 或由 binding 的 delivery policy 决定投递位置(`DeliveryContext` 见 PROTOCOL_V1 §5.7)。
|
||||||
|
|
||||||
|
## 7. 与 Context 协议的关系
|
||||||
|
|
||||||
|
EBA 事件进入 AgentRunner 时仍遵循 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md):inline 当前事件、大 payload 用 raw/artifact ref、不默认 inline 完整 history、agent 按需通过 API 拉取、Host 保留 EventLog 和权限 guardrail。非消息事件可以被投影进 Transcript,但不能强制伪装为 user message;AgentRunner 根据 event type 自己决定是否纳入模型上下文。
|
||||||
|
|
||||||
|
## 8. 未来 EBA 完整落地需要
|
||||||
|
|
||||||
|
EventGateway 完整实现、EventRouter 与 BindingResolver 集成、`AgentBinding` 持久模型和 UI、`DeliveryContext` 完整实现、platform action permission model 和执行器、真实平台事件接入。
|
||||||
|
|
||||||
|
落地顺序:① 把当前 Pipeline 消息入口适配成 `message.received` event(已完成)→ ② 增加 `AgentBinding` 抽象,先由 current config 生成(已完成)→ ③ context builder 改为从 event + binding 构造(已完成)→ ④ 引入 EventLog / Transcript(已完成)→ ⑤ 增加非消息事件的协议测试,不接真实平台 → ⑥ 接入真实 EventRouter 和 platform action。
|
||||||
240
docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md
Normal file
240
docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# LangBot Host 与 SDK 基础设施设计
|
||||||
|
|
||||||
|
本文档描述 LangBot 作为 agent host 的内部能力与分层架构,以及 Host 内部模型。
|
||||||
|
|
||||||
|
- SDK ↔ Host 的协议数据结构(`AgentRunContext`、`AgentRunnerManifest`、`AgentRunResult`、`AgentRunAPIProxy` 等)的**唯一定义在** [PROTOCOL_V1.md](./PROTOCOL_V1.md);本文只引用,不重抄。
|
||||||
|
- 实现进度见 [PROGRESS.md](./PROGRESS.md)。
|
||||||
|
- 本文定义的 Host 内部模型(`AgentEventEnvelope`、`AgentBinding`、`AgentRunnerDescriptor`)不属于 SDK 协议字段。
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
LangBot 要转为 agent host,而不是内置 runner 容器:
|
||||||
|
|
||||||
|
- 接收 IM、WebUI、API 和未来 EventRouter 产生的事件。
|
||||||
|
- 根据事件、bot、workspace、scope 解析应该调用的 Agent / agent binding。
|
||||||
|
- 发现、校验和调用插件提供的 AgentRunner。
|
||||||
|
- 为每次 run 提供受限资源、状态、存储、上下文引用和生命周期控制。
|
||||||
|
- 接收 AgentRunner 返回的事件流,投递到 IM、WebUI 或其他 output surface。
|
||||||
|
|
||||||
|
## 2. 非目标
|
||||||
|
|
||||||
|
- 不把 Pipeline 当作长期架构中心。
|
||||||
|
- 不要求所有 AgentRunner 依赖 LangBot 的上下文管理。
|
||||||
|
- 不要求官方 local-agent 的旧行为反向塑造 host 协议。
|
||||||
|
- 不在 host 中实现通用 agentic prompt assembler。
|
||||||
|
- 不强制 runner 使用 LangBot state / storage;只提供可选、受控的寄宿能力。
|
||||||
|
- 不实现 EventGateway:它是 future integration point,由外部 event branch 提供。本分支只定义 host-side envelope/binding models 和 `run(event, binding)` 入口。
|
||||||
|
|
||||||
|
## 3. 分层架构
|
||||||
|
|
||||||
|
```text
|
||||||
|
IM / WebUI / API / EventRouter (future)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Event Gateway (future - external event branch)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
AgentBindingResolver
|
||||||
|
|
|
||||||
|
v
|
||||||
|
AgentRunOrchestrator
|
||||||
|
|-- AgentRunnerRegistry
|
||||||
|
|-- AgentResourceBuilder
|
||||||
|
|-- AgentContextBuilder
|
||||||
|
|-- AgentRunSessionRegistry
|
||||||
|
|-- PersistentStateStore / EventLogStore / TranscriptStore / ArtifactStore
|
||||||
|
v
|
||||||
|
Plugin Runtime / AgentRunner
|
||||||
|
|
|
||||||
|
v
|
||||||
|
AgentRunResult stream
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Delivery / Renderer / Platform API
|
||||||
|
```
|
||||||
|
|
||||||
|
目标产品模型中,Agent 替代 Pipeline 承载 agent 配置:bot / IM
|
||||||
|
channel 绑定一个 Agent,一个 Agent 可以被多个 bot / channel 复用。
|
||||||
|
当前 Pipeline 只应接入在 Query entry adapter 位置:它可以继续产生
|
||||||
|
`message.received` 并投影出临时 `AgentBinding`,但不应再拥有 runner
|
||||||
|
选择、上下文裁剪和业务 agent 执行的核心语义。EventGateway 由外部 event
|
||||||
|
branch 实现。
|
||||||
|
|
||||||
|
## 4. LangBot 侧能力
|
||||||
|
|
||||||
|
### 4.1 Event Gateway(Future Integration Point)
|
||||||
|
|
||||||
|
> EventGateway 由外部 event branch 实现,不在本分支范围。本分支只预留 event-first 入口和 envelope/binding models。
|
||||||
|
|
||||||
|
Event Gateway 将把入口统一成 host event(IM 平台消息、WebUI debug chat、API 触发、后续非消息事件),输出稳定的 `AgentEventEnvelope`(Host 内部模型):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentEventEnvelope(BaseModel):
|
||||||
|
event_id: str
|
||||||
|
event_type: str
|
||||||
|
event_time: int | None
|
||||||
|
source: str
|
||||||
|
bot_id: str | None
|
||||||
|
workspace_id: str | None
|
||||||
|
conversation_id: str | None
|
||||||
|
thread_id: str | None
|
||||||
|
actor: ActorRef | None
|
||||||
|
subject: SubjectRef | None
|
||||||
|
input: AgentInput # 见 PROTOCOL_V1 §5.6
|
||||||
|
delivery: DeliveryContext # 见 PROTOCOL_V1 §5.7
|
||||||
|
raw_ref: RawEventRef | None
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
`AgentEventEnvelope` 是 Host 内部入口模型;投影给 runner 的是 `ctx.event`(PROTOCOL_V1 §5.4)。原始平台 payload 存为 raw event 或 artifact ref,不扩散到 runner 协议顶层。
|
||||||
|
|
||||||
|
**当前 adapter source**:`QueryEntryAdapter.query_to_event(query)` 从 Query 生成 `AgentEventEnvelope`。
|
||||||
|
|
||||||
|
### 4.2 AgentBinding
|
||||||
|
|
||||||
|
`AgentBinding` 是"什么事件调用哪个 AgentRunner、带什么 Agent 配置"的
|
||||||
|
Host 内部运行投影(不暴露给 SDK)。产品层的持久对象应是 Agent:
|
||||||
|
Agent 携带 runner id、runner config、resource/state/delivery policy,并可被
|
||||||
|
多个 bot / channel 复用。`AgentBinding` 是 EventRouter / 当前
|
||||||
|
QueryEntryAdapter 在一次运行前解析出的有效绑定。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentBinding(BaseModel):
|
||||||
|
binding_id: str
|
||||||
|
enabled: bool
|
||||||
|
scope: BindingScope
|
||||||
|
event_types: list[str]
|
||||||
|
filters: list[EventFilter] = [] # EBA 阶段使用,见 EVENT_BASED_AGENT
|
||||||
|
runner_id: str
|
||||||
|
runner_config: dict[str, Any]
|
||||||
|
resource_policy: ResourcePolicy
|
||||||
|
state_policy: StatePolicy
|
||||||
|
delivery_policy: DeliveryPolicy
|
||||||
|
```
|
||||||
|
|
||||||
|
一个 bot / IM channel 在同一时间只应解析出一个负责 agentic 处理的
|
||||||
|
AgentBinding。若未来需要 observer / fan-out / 多 agent 裁决,必须另行定义
|
||||||
|
delivery、state、platform action 和 result 合并语义;当前 v1/EBA 主线不隐式支持。
|
||||||
|
|
||||||
|
**当前 adapter source**:`QueryEntryAdapter.config_to_agent_config(query, runner_id)`
|
||||||
|
先把 current config 投影为迁移期 `AgentConfig`,再由
|
||||||
|
`AgentBindingResolver.resolve_one(event, [agent_config])` 解析出唯一
|
||||||
|
`AgentBinding`。Pipeline 当前只是迁移期 Agent config source(AI runner config
|
||||||
|
→ runner_config、extension preference → resource_policy、output settings →
|
||||||
|
delivery_policy),但新设计不再把这些字段命名为 Pipeline 专属概念。
|
||||||
|
|
||||||
|
### 4.3 AgentRunnerRegistry
|
||||||
|
|
||||||
|
Registry 收集 runner descriptor(来自插件 runtime、开发期本地插件):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerDescriptor(BaseModel):
|
||||||
|
id: str
|
||||||
|
source: Literal["plugin"]
|
||||||
|
label: I18nObject
|
||||||
|
description: I18nObject | None = None
|
||||||
|
protocol_version: str = "1"
|
||||||
|
capabilities: AgentRunnerCapabilities # 见 PROTOCOL_V1 §4.3
|
||||||
|
permissions: AgentRunnerPermissions # 见 PROTOCOL_V1 §4.4
|
||||||
|
config_schema: list[DynamicFormItemSchema]
|
||||||
|
plugin: PluginRef | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
职责:调用 `plugin_connector.list_agent_runners()` 拉取 runner、校验 manifest(`kind == AgentRunner`、`metadata.name/label` 存在、`protocol_version` 兼容、`spec.*` 类型正确)、输出 descriptor、缓存 discovery 结果并提供 `refresh()`。单个插件 manifest 失败只记 warning,不影响其它 runner。`plugin:author/name/runner` 是稳定 id 格式;多个 binding 指向同一 runner id 时**不创建多个插件实例**。
|
||||||
|
|
||||||
|
Host 内置 runner / adapter 不能作为 `AgentRunnerDescriptor.source` 绕过插件
|
||||||
|
runtime、`run_id`、`ctx.resources` 和 `AgentRunAPIProxy` 权限链。若需要
|
||||||
|
开发期调试 adapter,应放在 Host 内部测试入口,不进入可选 runner 列表。
|
||||||
|
|
||||||
|
刷新触发点:插件安装/卸载/升级/重启后;Pipeline metadata 请求时发现缓存为空;可选 TTL(优先保证正确性)。
|
||||||
|
|
||||||
|
### 4.4 AgentRunOrchestrator
|
||||||
|
|
||||||
|
Orchestrator 是唯一运行入口:
|
||||||
|
|
||||||
|
```text
|
||||||
|
run(event, binding)
|
||||||
|
-> resolve runner descriptor
|
||||||
|
-> build resources
|
||||||
|
-> build context
|
||||||
|
-> register run session
|
||||||
|
-> call plugin runtime
|
||||||
|
-> normalize result stream
|
||||||
|
-> update state
|
||||||
|
-> unregister run session
|
||||||
|
```
|
||||||
|
|
||||||
|
它负责:`run_id` 生成和生命周期、timeout/deadline/cancellation、插件异常隔离、result schema 校验和大小限制、`state.updated` 处理、delivery backpressure 和 telemetry。
|
||||||
|
|
||||||
|
`run_from_query()` 保留为 Query entry adapter 入口,但内部转换成 event + binding 后走统一 `run()`。约束:`ChatMessageHandler` 不解析 `plugin:*`、不实例化 wrapper、不知道 runner 组件细节;`PipelineService` 从 registry 读取 metadata,不直接访问插件 runtime;插件是无状态执行单元,跨请求持久化状态必须走授权 storage / 外部服务,不能隐式存在 per-pipeline 插件对象里。
|
||||||
|
|
||||||
|
### 4.5 Resource Authorization(三层裁剪)
|
||||||
|
|
||||||
|
LangBot 在每次 run 前生成 `ctx.resources`(PROTOCOL_V1 §6),来自三层约束:
|
||||||
|
|
||||||
|
1. runner manifest 声明的 `permissions`(最大能力)。
|
||||||
|
2. binding / resource policy 允许的资源范围。
|
||||||
|
3. 当前 event / actor / bot / workspace 的实际权限。
|
||||||
|
|
||||||
|
这次裁剪结果必须冻结为 run-scoped authorization snapshot,并由
|
||||||
|
`AgentRunSessionRegistry` 按 `run_id` 保存。`ctx.resources` 是投影给 runner
|
||||||
|
看的同一份授权结果;运行期每个 proxy action 只依据该 snapshot 校验 active
|
||||||
|
run session、caller plugin identity、resource id、scope、payload size、rate
|
||||||
|
limit 和 deadline。Handler 不应重新执行三层裁剪,否则 build-time 与 runtime
|
||||||
|
授权逻辑会漂移。
|
||||||
|
|
||||||
|
SDK 侧本地校验只用于开发体验,host 侧 run authorization snapshot 才是安全边界。
|
||||||
|
|
||||||
|
资源裁剪应通用,不写死 local-agent。selector 与资源的映射示例:`model-fallback-selector` → primary/fallback LLM、`llm-model-selector` → LLM、`rerank-model-selector` → rerank 模型、`knowledge-base-multi-selector` → 知识库;新增 selector 时在 resource builder 中统一扩展。
|
||||||
|
|
||||||
|
执行/文件/skill/MCP 等能力的接入方向:先由 Host 封装成普通 tool,再通过 `ctx.resources.tools` 进入 runner;runner 不应识别或硬编码执行环境 provider。
|
||||||
|
|
||||||
|
### 4.6 State / Storage
|
||||||
|
|
||||||
|
LangBot 可提供 host-owned state 让 runner 寄宿状态(conversation / actor / subject / runner / binding / workspace state),但**不是强制**。Host 只需提供:授权开关、scope key、get/set/list/delete API(见 PROTOCOL_V1 §8)、持久化 backend、审计和清理策略。外部 agent runtime 可维护自己的 session 和 memory。进程内 state store 只能作为过渡实现,不能作为正式生产语义。
|
||||||
|
|
||||||
|
### 4.7 EventLog / Transcript / Artifact(事实源)
|
||||||
|
|
||||||
|
- `EventLog`: durable append-only,保存原始事件、系统事件、工具调用、投递结果、错误。
|
||||||
|
- `Transcript`: 从 EventLog 投影出的对话视图,用于 UI、审计和按需历史读取。
|
||||||
|
- `ArtifactStore`: 保存大文件、多模态输入、工具大结果、平台附件。
|
||||||
|
|
||||||
|
三类数据与 working context 的边界、读取约束见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md)。AgentRunner 可读取这些能力,但不被迫使用 LangBot 作为唯一记忆系统。
|
||||||
|
|
||||||
|
### 4.8 Prompt / Instruction Package(占位)
|
||||||
|
|
||||||
|
当前 Query 入口不把 preprocessing 后的有效 prompt 放进 adapter metadata。目标形态是 Host 保存或生成一个 run-scoped instruction package,runner 通过 Host API 拉取:
|
||||||
|
|
||||||
|
- Host 记录静态绑定 prompt、host hook / user plugin 产生的 instruction fragment、来源和审计信息。
|
||||||
|
- `ctx.context.available_apis` 增加 `prompt_get` 能力位表示拉取是否可用。
|
||||||
|
- Runner 拉取后仍由自己决定如何与 history、RAG、tool 结果、memory 和当前输入组装最终 prompt。
|
||||||
|
- Host 不实现通用 agentic prompt assembler,也不把 Query entry adapter prompt 作为长期业务输入契约。
|
||||||
|
|
||||||
|
### 4.9 External harness resource projection
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 等外部 harness runner 可能不直接调用 LangBot 的 model/tool loop,而是把 LangBot 事件和授权资源投影到自己的 harness 执行。Host 侧仍保持统一边界:Host 负责构造 event-first context、资源授权、state/storage、EventLog/Transcript/ArtifactStore 和审计;Host 或 binding policy 决定哪些 MCP server、skill、artifact、history/state 句柄可投影给 runner;runner plugin 把 scoped projection 转成目标 harness 可消费形式;外部 harness 负责自己的 native session、tool loop、压缩、权限模式和 resume。
|
||||||
|
|
||||||
|
投影的具体形态(context 文件、skill 目录、MCP config、state pointers)见 AGENT_CONTEXT_PROTOCOL §4.5;Claude Code / Codex 当前实现见 OFFICIAL_RUNNER_PLUGINS §7。发布级隔离要求见 SECURITY_HARDENING。
|
||||||
|
|
||||||
|
## 5. SDK 侧协议
|
||||||
|
|
||||||
|
SDK 组件入口如下;所有数据结构定义见 PROTOCOL_V1。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunner(BaseComponent):
|
||||||
|
__kind__ = "AgentRunner"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_capabilities(cls) -> AgentRunnerCapabilities: ... # PROTOCOL_V1 §4.3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_schema(cls) -> list[dict]: ...
|
||||||
|
|
||||||
|
async def run(self, ctx: AgentRunContext) -> AsyncGenerator[AgentRunResult, None]: ...
|
||||||
|
# ctx: PROTOCOL_V1 §5.2 ; AgentRunResult: PROTOCOL_V1 §7
|
||||||
|
```
|
||||||
|
|
||||||
|
- Manifest / capabilities / permissions / context policy:PROTOCOL_V1 §4。
|
||||||
|
- `AgentRunContext`:PROTOCOL_V1 §5.2。`messages` / `bootstrap` 不是协议字段。
|
||||||
|
- `AgentRunResult`:PROTOCOL_V1 §7。
|
||||||
|
- `AgentRunAPIProxy`:PROTOCOL_V1 §8,是 runner 访问 host 能力的唯一入口,所有请求带 `run_id`。
|
||||||
147
docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md
Normal file
147
docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# 官方 AgentRunner 插件迁移计划
|
||||||
|
|
||||||
|
本文档描述内置 `RequestRunner` 迁出 LangBot 后,官方 runner 插件如何组织、迁移和验收。它是 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md) 和 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md) 的下游落地计划,不是 LangBot 宿主协议的设计前提。验收状态见 [PROGRESS.md](./PROGRESS.md),QA 入口见 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md)。
|
||||||
|
|
||||||
|
官方 `local-agent` 可以外移,也可以重写。设计重点不是保留旧内置 runner 的内部结构,而是验证一个依附 LangBot host 基础设施的官方 agent 能否完整工作。同时,LangBot host 协议必须服务 Claude Code SDK、Codex、Pi Agent SDK、外部 Agent 平台等自管 context/runtime 的 runner,不能被官方插件的实现细节绑死。
|
||||||
|
|
||||||
|
## 1. 仓库组织
|
||||||
|
|
||||||
|
官方 runner 插件与 LangBot 主仓库、SDK 仓库以不同节奏迭代:LangBot 主仓库只维护宿主协议和调度,SDK 仓库维护 AgentRunner 组件和 runtime 协议,官方 runner 插件承载业务 runner 的具体实现和第三方平台适配。
|
||||||
|
|
||||||
|
当前推荐"官方插件可独立发布,必要时共享 SDK helper"。开发期采用本地多目录布局:
|
||||||
|
|
||||||
|
```text
|
||||||
|
langbot-app/
|
||||||
|
langbot-local-agent/ # plugin:langbot/local-agent/default
|
||||||
|
manifest.yaml
|
||||||
|
components/agent_runner/default.{yaml,py}
|
||||||
|
langbot-agent-runner/ # 外部服务 runner 仓库
|
||||||
|
claude-code-agent/ codex-agent/ dify-agent/ n8n-agent/ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
后续可聚合进 monorepo,也可继续独立发布——这个选择不影响协议设计。重复逻辑优先沉淀到 SDK 或明确的共享 helper 包,不要把宿主私有结构泄漏给插件。旧 `src/langbot/pkg/provider/runners/*` 在官方插件迁移完成前保留作为行为对齐基准,不作为长期运行路径。
|
||||||
|
|
||||||
|
## 2. 插件命名和 runner id
|
||||||
|
|
||||||
|
| 旧 runner | 官方插件 | runner id |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `local-agent` | `langbot/local-agent` | `plugin:langbot/local-agent/default` |
|
||||||
|
| `dify-service-api` | `langbot/dify-agent` | `plugin:langbot/dify-agent/default` |
|
||||||
|
| `n8n-service-api` | `langbot/n8n-agent` | `plugin:langbot/n8n-agent/default` |
|
||||||
|
| `coze-api` | `langbot/coze-agent` | `plugin:langbot/coze-agent/default` |
|
||||||
|
| - | `langbot/claude-code-agent` | `plugin:langbot/claude-code-agent/default` |
|
||||||
|
| - | `langbot/codex-agent` | `plugin:langbot/codex-agent/default` |
|
||||||
|
| `dashscope-app-api` | `langbot/dashscope-agent` | `plugin:langbot/dashscope-agent/default` |
|
||||||
|
| `langflow-api` | `langbot/langflow-agent` | `plugin:langbot/langflow-agent/default` |
|
||||||
|
| `tbox-app-api` | `langbot/tbox-agent` | `plugin:langbot/tbox-agent/default` |
|
||||||
|
|
||||||
|
每个插件可后续提供多个 runner,但迁移目标的默认 runner 统一叫 `default`。
|
||||||
|
|
||||||
|
## 3. 迁移批次
|
||||||
|
|
||||||
|
- **Batch 1(打通协议)**:`local-agent`(能力最完整基准)、`claude-code-agent` / `codex-agent`(外部 code-agent harness 边界)、`dify-agent`(传统 service API runner)。
|
||||||
|
- **Batch 2(外部 workflow)**:`n8n-agent`、`langflow-agent`(webhook/workflow 输入输出、timeout、外部 conversation id)。
|
||||||
|
- **Batch 3(平台 Agent API)**:`coze-agent`、`dashscope-agent`、`tbox-agent`(平台特有响应格式、引用资料、文件/图片输入)。
|
||||||
|
|
||||||
|
## 4. 每个官方插件的组件要求
|
||||||
|
|
||||||
|
每个插件至少包含一个 `AgentRunner` 组件,manifest 示例:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: langbot/v1
|
||||||
|
kind: AgentRunner
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
label: { en_US: Dify Agent, zh_Hans: Dify Agent }
|
||||||
|
description:
|
||||||
|
en_US: Run a Dify application as a LangBot AgentRunner.
|
||||||
|
zh_Hans: 将 Dify 应用作为 LangBot AgentRunner 运行。
|
||||||
|
spec:
|
||||||
|
protocol_version: "1"
|
||||||
|
config: []
|
||||||
|
capabilities: # 字段语义见 PROTOCOL_V1 §4.3
|
||||||
|
streaming: true
|
||||||
|
event_context: true
|
||||||
|
stateful_session: true
|
||||||
|
permissions: # 字段语义见 PROTOCOL_V1 §4.4
|
||||||
|
storage: ["plugin"]
|
||||||
|
context: # 字段语义见 PROTOCOL_V1 §4.5
|
||||||
|
supports_history_pull: true
|
||||||
|
owns_compaction: true
|
||||||
|
execution:
|
||||||
|
python: { path: ./main.py, attr: DefaultAgentRunner }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. local-agent 插件方向
|
||||||
|
|
||||||
|
`local-agent` 是官方插件中能力最完整的消费者,但不是宿主协议的设计中心。它需要证明:一个主要依附 LangBot host 能力的 agent runner 可以通过公开协议完成模型、工具、知识库、状态、history、artifact、上下文压缩和消息投递。
|
||||||
|
|
||||||
|
迁移或重写需覆盖旧内置 runner 的用户可见能力:model primary/fallback 选择、prompt、knowledge-bases、rerank-model、rerank-top-k、function calling、streaming、multimodal input、conversation history、monitoring metadata。
|
||||||
|
|
||||||
|
责任边界与 Host API 消费方式见 AGENT_CONTEXT_PROTOCOL §8。关键约束:
|
||||||
|
|
||||||
|
- 从 `ctx.config` 读取静态绑定 `prompt`,**不**读取 `ctx.adapter.extra["prompt"]`;不消费 Query entry adapter 生成的历史窗口。
|
||||||
|
- 通过 `AgentRunAPIProxy.history` 拉取 transcript,而不是依赖 host 每轮强塞历史窗口。
|
||||||
|
- `ctx.input.contents` 保留图片/文件等多模态内容;RAG 只替换/插入文本部分,不丢图片/文件。
|
||||||
|
- 不能绕过 `ctx.resources` 调用未授权模型、工具或知识库。
|
||||||
|
- manifest 声明自管上下文能力(`context.supports_history_pull/search`、`owns_compaction` 等)。
|
||||||
|
|
||||||
|
### 5.1 Native Execution / Skills 后续接入
|
||||||
|
|
||||||
|
本阶段不把 sandbox/skills 做成 AgentRunner 协议字段。后续 sandbox/skills 分支合并后,命令执行、文件操作、skill、MCP managed process 应先由 Host 封装成 scoped tools,再通过 `ctx.resources.tools` 暴露给 runner。这让 local-agent 只消费授权后的 Host 基础设施,而不是直接持有宿主机执行能力。
|
||||||
|
|
||||||
|
## 6. 外部 runner 插件要求
|
||||||
|
|
||||||
|
外部平台 runner 迁移遵循:旧配置字段尽量保持同名便于 migration 复制;输出统一转换为 `AgentRunResult`;外部 API timeout 从 runner config 读取;平台 conversation id 存 plugin storage 或 context runtime state,不依赖 LangBot 内置 conversation uuid 私有结构;流式按平台能力声明,没有流式就只发 `message.completed`。
|
||||||
|
|
||||||
|
### 6.1 Code-agent harness runner
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 这类 runner 不一定通过 LangBot 的模型/工具 loop 执行,可以依赖自己的 harness,但仍必须遵守 Host 边界:输入来自 `ctx.event` / `ctx.input`,不依赖 Pipeline 私有 `Query`;授权资源投影为 harness 可读的 context 文件、MCP 配置、skill 目录、环境变量或 CLI 参数(投影形态见 AGENT_CONTEXT_PROTOCOL §4.5);外部 session id / workspace / checkpoint 写入 Host state 或 plugin storage,插件实例保持无状态;CLI / subprocess runner 必须处理 timeout、取消、空输出、非零退出和 stderr 映射;harness 的 permission mode / allow-deny / MCP 配置只是一层执行约束,Host 仍负责调用前的资源授权、路径策略、secret 过滤和审计(发布级要求见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md))。
|
||||||
|
|
||||||
|
### 6.2 SDK-owned LangBot MCP bridge
|
||||||
|
|
||||||
|
外部 harness 不能直接持有进程内的 `plugin_runtime_handler`,因此不能像 `local-agent` 一样直接调用 `AgentRunAPIProxy`。当前轻量方案是由 SDK 提供一层 per-run MCP bridge:
|
||||||
|
|
||||||
|
- `AgentRunner.create_external_mcp_bridge(ctx)` 是 runner 父类入口。
|
||||||
|
- Bridge 由 `AgentRunAPIProxy` 和 `AgentRunContext` 构造,生命周期只覆盖当前 run。
|
||||||
|
- Bridge 暴露 SDK 中显式注解的 `AgentRunExternalTools`,而不是导出全部 SDK action;MCP tool schema 由注解和 Pydantic args model 生成。
|
||||||
|
- stdio MCP proxy 只把外部 harness 的 MCP 调用转发回当前 run 的本地 bridge;run 结束后 bridge 关闭。
|
||||||
|
|
||||||
|
第一批工具保持很小:当前事件快照、history page、knowledge retrieve、authorized tool call。新增工具必须先进入 SDK-owned annotated surface,再由 MCP adapter 自动投影。
|
||||||
|
|
||||||
|
## 7. Claude Code / Codex runner 当前形态
|
||||||
|
|
||||||
|
`claude-code-agent` 与 `codex-agent` 是最小可运行 MVP / dev path,用来证明外部 harness runner 可以接入同一套 AgentRunner 协议。本地 smoke 验收记录见 [PROGRESS.md](./PROGRESS.md) 与 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md)。
|
||||||
|
|
||||||
|
MVP 含义:已验证 event-first context、resource projection、result stream 和
|
||||||
|
基础 resume state 可以跑通;不表示 Docker 生产部署、发布级执行隔离、
|
||||||
|
workspace lifecycle、secret projection、团队级 audit 或 runtime sidecar 已完成。
|
||||||
|
|
||||||
|
### 7.1 Claude Code runner
|
||||||
|
|
||||||
|
- Runner ID:`plugin:langbot/claude-code-agent/default`,执行方式:本地 Claude Code CLI print mode(默认 `claude -p`)。
|
||||||
|
- 默认输出 `message.completed` + `run.completed`;默认权限 `permission-mode=plan`、`max-turns=1`、`disallowedTools=AskUserQuestion`。
|
||||||
|
- 投影:写入 `agent-context.json`(schema `langbot.agent_runner.external_harness_context.v1`)和 `LANGBOT_CONTEXT.md`;可把 `skills-json` 投影到 `.claude/skills/<name>/SKILL.md`;可把 `mcp-config-json` 写成每次 run 的 MCP config 经 `--mcp-config` / `--strict-mcp-config` 传入;可通过 `enable-langbot-mcp=true` 启用 SDK-owned per-run LangBot MCP bridge。
|
||||||
|
- 状态:Claude Code 返回 `session_id` 时通过 `state.updated` 写回 `external.session_id`;工作目录优先用 config 的 `working-directory`,其次用 Host state 的 `external.working_directory`。
|
||||||
|
|
||||||
|
### 7.2 Codex runner
|
||||||
|
|
||||||
|
- Runner ID:`plugin:langbot/codex-agent/default`,执行方式:本地 Codex CLI,读取 LangBot event context。
|
||||||
|
- Codex `thread_id` 写回 host-owned state;支持 SDK-owned per-run LangBot MCP bridge;需要代理的本地环境可通过 config 的 `environment-json` 显式传递非 secret 环境变量。
|
||||||
|
|
||||||
|
### 7.3 当前限制
|
||||||
|
|
||||||
|
不是发布级安全边界实现;默认只做本地 CLI 调用,不实现完整执行隔离或 workspace 生命周期;不实现 issue-centric 队列、复杂 workflow engine 或长期任务调度;Docker 环境只能访问容器内 CLI 和凭据;Codex 仅验证协议形态,不代表 Codex 发布级能力或 Kimi runner 已完成。runtime 管控面方向见 [RUNTIME_CONTROL_PLANE_V2.md](./RUNTIME_CONTROL_PLANE_V2.md)。
|
||||||
|
|
||||||
|
## 8. 发布和安装策略
|
||||||
|
|
||||||
|
最终 LangBot 安装/升级时需保证官方 runner 插件可用,可选方案:首次启动检测缺失并提示安装;打包发行版预装;migration 前检查插件存在性。建议顺序:开发阶段用本地路径插件 → 发布前支持 marketplace 安装 → 历史配置 migration 只在官方插件可用时执行 → 迁移期间保留旧内置 runner 文件,直到对应官方插件通过 parity 验收。
|
||||||
|
|
||||||
|
## 9. 验收标准
|
||||||
|
|
||||||
|
- 每个旧 runner 都有对应官方 AgentRunner 插件,旧配置能无损复制到新 `runner_config[id]`。
|
||||||
|
- LangBot 主聊天路径不再通过 `RequestRunner` 执行业务 runner。
|
||||||
|
- 官方插件测试覆盖非流式、流式、错误、timeout、配置缺失。
|
||||||
|
- `local-agent` 能完成模型 fallback、tool calling、知识库检索、多模态输入、静态绑定 prompt 消费、history API 拉取、rerank。
|
||||||
|
- `claude-code-agent` 或同类 code-agent harness runner 能消费 event-first context、投影 scoped resources、保存 external session state,并通过 WebUI Debug Chat smoke。
|
||||||
|
- 对外行为与旧内置 local-agent runner 一致;代码结构不需要相同。
|
||||||
245
docs/agent-runner-pluginization/PHASE1_QA_ACCEPTANCE_MATRIX.md
Normal file
245
docs/agent-runner-pluginization/PHASE1_QA_ACCEPTANCE_MATRIX.md
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
# Agent Runner QA 指南
|
||||||
|
|
||||||
|
本文档是 agent-runner 插件化下一轮测试的唯一 QA 入口。它合并并取代旧的 Phase 1 验收矩阵与 2026-05-18 / 2026-05-29 两份本地 QA 报告。
|
||||||
|
|
||||||
|
目标不是保留完整历史流水账,而是指导测试 agent 用最小但高价值的路径判断当前分支是否仍然健康。
|
||||||
|
|
||||||
|
## 1. 测试边界
|
||||||
|
|
||||||
|
当前主线验证的是 AgentRunner Protocol v1:
|
||||||
|
|
||||||
|
```text
|
||||||
|
event -> binding -> runner.run(ctx) -> result stream
|
||||||
|
```
|
||||||
|
|
||||||
|
本指南验证:
|
||||||
|
|
||||||
|
- Host 能通过当前 Query entry adapter 进入 event-first `run(event, binding)` 主链路。
|
||||||
|
- Runner 来自插件 registry,而不是旧内置 runner 分支。
|
||||||
|
- `local-agent` 能消费 Host 模型、工具、知识库、history、state、artifact 等基础设施。
|
||||||
|
- 外部 harness runner(Claude Code / Codex)能消费 event-first context,并把 session / working directory 等指针写回 host-owned state。
|
||||||
|
- 错误、权限裁剪、无输出、timeout 等路径不会破坏主聊天流程。
|
||||||
|
|
||||||
|
本指南不验证:
|
||||||
|
|
||||||
|
- Runtime Control Plane v2。
|
||||||
|
- EventGateway / EventRouter 完整落地。
|
||||||
|
- 发布级 path isolation、secret filtering、MCP allowlist、资源配额和 workspace cleanup。
|
||||||
|
- 所有外部服务 runner 的真实凭据联调。
|
||||||
|
|
||||||
|
这些属于后续能力或发布门槛,分别见 [RUNTIME_CONTROL_PLANE_V2.md](./RUNTIME_CONTROL_PLANE_V2.md) 与 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
|
||||||
|
## 2. 状态定义
|
||||||
|
|
||||||
|
测试报告只使用以下状态:
|
||||||
|
|
||||||
|
| 状态 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| PASS | 按步骤执行,用户可见行为和日志证据都满足通过条件。 |
|
||||||
|
| FAIL | 环境可用,但行为不满足通过条件。 |
|
||||||
|
| BLOCKED | 凭据、CLI、外部服务、测试数据或本地配置缺失导致无法执行。必须写清阻塞原因。 |
|
||||||
|
| N/A | 当前 runner 或平台明确不支持该能力。必须引用 manifest、文档或配置说明。 |
|
||||||
|
|
||||||
|
不能使用“看起来正常”“大概通过”“基本没问题”等模糊状态。
|
||||||
|
|
||||||
|
## 3. 执行顺序
|
||||||
|
|
||||||
|
推荐按以下顺序执行,前一层失败时不要继续扩大测试面:
|
||||||
|
|
||||||
|
1. Host / SDK / runner 单测。
|
||||||
|
2. WebUI 登录与 Pipeline Debug Chat 基础 smoke。
|
||||||
|
3. `local-agent` 高价值场景。
|
||||||
|
4. Claude Code / Codex 外部 harness smoke。
|
||||||
|
5. 权限和错误路径补充检查。
|
||||||
|
6. 汇总 PASS / FAIL / BLOCKED,并给出下一步建议。
|
||||||
|
|
||||||
|
用户可见流程必须通过 WebUI 或真实消息平台验证。API / curl 只能作为诊断证据,不能单独让 UI case PASS。
|
||||||
|
|
||||||
|
## 4. 必跑基线
|
||||||
|
|
||||||
|
### 4.1 单测基线
|
||||||
|
|
||||||
|
在 LangBot 仓库运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run --frozen pytest tests/unit_tests/agent
|
||||||
|
```
|
||||||
|
|
||||||
|
如果本次改动只触及默认配置或 API service,也至少补跑相关目标测试,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/unit_tests/api/test_pipeline_service_defaults.py
|
||||||
|
```
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- agent 单测全 PASS,或失败项已确认与本次 agent-runner 路径无关。
|
||||||
|
- 若失败来自 `context_builder`、`orchestrator`、`session_registry`、`resource_builder`、`plugin/handler.py` 的 run action 权限路径,不应进入 UI smoke。
|
||||||
|
|
||||||
|
### 4.2 环境基线
|
||||||
|
|
||||||
|
用 `langbot-skills` 做环境检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$LANGBOT_SKILLS_REPO"
|
||||||
|
bin/lbs env doctor
|
||||||
|
bin/lbs case list
|
||||||
|
```
|
||||||
|
|
||||||
|
`LANGBOT_SKILLS_REPO` 指向当前工作区里的 `langbot-skills` 仓库。优先使用已有 case,而不是临时发明测试路径。
|
||||||
|
|
||||||
|
推荐首批 case:
|
||||||
|
|
||||||
|
- `webui-login-state`
|
||||||
|
- `pipeline-debug-chat`
|
||||||
|
- `local-agent-basic-debug-chat`
|
||||||
|
- `local-agent-rag-debug-chat`(改动涉及 RAG / knowledge)
|
||||||
|
- `local-agent-plugin-tool-call-debug-chat`(改动涉及 tool / resource policy)
|
||||||
|
|
||||||
|
## 5. WebUI 主链路 Smoke
|
||||||
|
|
||||||
|
### 5.1 Runner registry
|
||||||
|
|
||||||
|
步骤:
|
||||||
|
|
||||||
|
1. 打开 WebUI Pipeline 配置页。
|
||||||
|
2. 查看 AI runner 下拉列表。
|
||||||
|
3. 选择 `plugin:langbot/local-agent/default`。
|
||||||
|
4. 保存并刷新页面。
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- runner 选项来自插件 registry。
|
||||||
|
- 保存后配置仍为 `ai.runner.id` + `ai.runner_config[id]`。
|
||||||
|
- `runner_config` 表示 Agent/runner config,不表示插件实例状态。
|
||||||
|
- 插件没有循环重启或 metadata 加载失败。
|
||||||
|
|
||||||
|
### 5.2 主聊天路径
|
||||||
|
|
||||||
|
步骤:
|
||||||
|
|
||||||
|
1. 使用绑定 `plugin:langbot/local-agent/default` 的 Pipeline。
|
||||||
|
2. 在 Debug Chat 发送确定性普通文本。
|
||||||
|
3. 查看 WebUI 回复和后端日志。
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- 用户可见回复正常。
|
||||||
|
- 后端日志显示走 `AgentRunOrchestrator` / `RUN_AGENT`。
|
||||||
|
- 不走旧内置 local-agent 主执行分支。
|
||||||
|
- conversation transcript 写入用户消息和助手消息。
|
||||||
|
|
||||||
|
## 6. `local-agent` 高价值测试
|
||||||
|
|
||||||
|
只保留最能覆盖架构边界的场景。
|
||||||
|
|
||||||
|
| ID | 场景 | 操作 | 通过条件 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| LA-01 | 绑定 prompt | 配置 system prompt 后发送文本。 | runner 使用 `ctx.config.prompt`,不读取 `ctx.adapter.extra["prompt"]`;回复体现绑定 prompt。 |
|
||||||
|
| LA-02 | history API | 连续两轮对话,第二轮引用第一轮 marker。 | runner 通过 Host history API 或自管上下文读取历史,不依赖 inline history window。 |
|
||||||
|
| LA-03 | 流式 / 非流式 | 分别用支持流式和关闭流式的路径发送文本。 | 流式 UI 不重复、不空白;非流式只输出最终消息。 |
|
||||||
|
| LA-04 | 工具调用 | 绑定测试工具,发送会触发工具的 prompt。 | `ctx.resources.tools` 只包含授权工具;工具调用 started/completed;最终回复包含工具结果。 |
|
||||||
|
| LA-05 | RAG | 绑定测试知识库,发送命中文档的 prompt。 | `ctx.resources.knowledge_bases` 包含所选知识库;runner 通过授权 API 检索;回复使用检索内容。 |
|
||||||
|
| LA-06 | 多模态 | 发送图片输入。 | `ctx.input.contents` 保留图片;支持视觉模型时正常处理,不支持时受控失败。 |
|
||||||
|
| LA-07 | fallback / 错误 | 模拟 primary 模型失败或 runner 抛错。 | fallback 或 `run.failed` 行为受控;后续请求不受影响。 |
|
||||||
|
| LA-08 | 无输出保护 | 测试 runner 完成但不产出消息。 | 不产生空白成功回复;按受控失败或明确缺陷处理。 |
|
||||||
|
|
||||||
|
Rerank、remove-think、文件输入等场景只在本次改动直接涉及时补测,不作为每轮必跑项。
|
||||||
|
|
||||||
|
## 7. 外部 Harness Runner Smoke
|
||||||
|
|
||||||
|
这些测试用于验证 Claude Code / Codex 这类自管 runtime 能走同一条 Host 协议路径。若本机没有 CLI、登录态或代理配置,标记 BLOCKED,不要伪造 PASS。
|
||||||
|
|
||||||
|
### 7.1 Claude Code runner
|
||||||
|
|
||||||
|
步骤:
|
||||||
|
|
||||||
|
1. 确认 `claude` CLI 在 LangBot runtime host 上可执行。
|
||||||
|
2. 绑定 `plugin:langbot/claude-code-agent/default`。
|
||||||
|
3. 使用保守权限模式和确定性 prompt。
|
||||||
|
4. 在 Debug Chat 执行一次真实 smoke。
|
||||||
|
5. 检查 context / skill / MCP projection 和 host-owned state。
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- WebUI 可见回复包含预期 sentinel。
|
||||||
|
- context JSON schema 为 `langbot.agent_runner.external_harness_context.v1` 或当前文档声明的等价 schema。
|
||||||
|
- context 包含 event、input、delivery、resources、context、state。
|
||||||
|
- 如启用 skills / MCP,投影路径和配置可被 Claude Code 读取。
|
||||||
|
- `external.session_id` / `external.working_directory` 写入 host-owned state。
|
||||||
|
- CLI missing、nonzero exit、timeout、empty output 都转成受控 `run.failed`。
|
||||||
|
|
||||||
|
### 7.2 Codex runner
|
||||||
|
|
||||||
|
步骤:
|
||||||
|
|
||||||
|
1. 确认 `codex` CLI 在 LangBot runtime host 上可执行。
|
||||||
|
2. 绑定 `plugin:langbot/codex-agent/default`。
|
||||||
|
3. 如需要代理,使用 Agent/runner config 的 `environment-json` 显式传入。
|
||||||
|
4. 在 Debug Chat 执行一次真实 smoke。
|
||||||
|
5. 检查 JSONL 事件、last message、host-owned state。
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- WebUI 可见回复包含预期 sentinel。
|
||||||
|
- Codex JSONL 至少包含 thread/session 起始事件、agent message、turn completed。
|
||||||
|
- `external.session_id` / `external.working_directory` 写入 host-owned state。
|
||||||
|
- timeout/cancel 不遗留 orphan CLI 子进程。
|
||||||
|
- CLI missing、nonzero exit、timeout、empty output 都转成受控 `run.failed`。
|
||||||
|
|
||||||
|
### 7.3 API 型外部 runner
|
||||||
|
|
||||||
|
Dify、n8n、Coze、DashScope、Langflow、Tbox 等外部服务 runner 不作为每轮必跑项。只有在本次改动触及对应 runner 或凭据已经可用时执行 smoke。
|
||||||
|
|
||||||
|
通过条件:
|
||||||
|
|
||||||
|
- runner 可选,配置可保存。
|
||||||
|
- 请求成功,或外部服务错误被清晰返回。
|
||||||
|
- 外部服务凭据缺失时标记 BLOCKED,并记录缺失项。
|
||||||
|
|
||||||
|
## 8. 权限与隔离补充
|
||||||
|
|
||||||
|
以下优先用单测 / targeted fixture 覆盖,不要求每次通过 UI 人工构造恶意 runner。
|
||||||
|
|
||||||
|
| 场景 | 推荐证据 |
|
||||||
|
| --- | --- |
|
||||||
|
| 未授权模型调用被拒绝 | `plugin/handler.py` run action 权限测试或目标单测。 |
|
||||||
|
| 未授权工具调用被拒绝 | `ctx.resources.tools` 与 host action 拒绝日志。 |
|
||||||
|
| 未授权知识库检索被拒绝 | `ctx.resources.knowledge_bases` 与 host action 拒绝日志。 |
|
||||||
|
| run_id 结束后复用被拒绝 | session registry 注销测试。 |
|
||||||
|
| 插件身份不匹配被拒绝 | `caller_plugin_identity` mismatch 测试。 |
|
||||||
|
| storage/state scope 越权被拒绝 | state/storage proxy 单测。 |
|
||||||
|
|
||||||
|
如果这些单测失败,不能用 WebUI 正常回复替代。
|
||||||
|
|
||||||
|
## 9. 证据要求
|
||||||
|
|
||||||
|
每轮测试报告至少记录:
|
||||||
|
|
||||||
|
- LangBot commit、SDK commit、相关 runner 插件 commit。
|
||||||
|
- Pipeline UUID/name、runner id、关键 runner config 摘要。
|
||||||
|
- WebUI 截图或 Playwright 操作记录。
|
||||||
|
- 后端日志中对应 query id / run id 的关键行。
|
||||||
|
- `langbot-skills` case/report 路径。
|
||||||
|
- 外部 harness runner 的 context 文件、session id、working directory、CLI 错误摘要。
|
||||||
|
- FAIL/BLOCKED 的复现步骤和归属仓库建议。
|
||||||
|
|
||||||
|
报告结论必须回答:
|
||||||
|
|
||||||
|
- 是否建议继续进入下一阶段测试。
|
||||||
|
- 是否存在主聊天路径阻塞。
|
||||||
|
- 是否只是凭据 / 外部服务 / 本机 CLI 缺失导致 BLOCKED。
|
||||||
|
- 是否需要进入 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md) 的发布级验收。
|
||||||
|
|
||||||
|
## 10. 历史高价值记录
|
||||||
|
|
||||||
|
历史报告已合并为本指南,不再保留单独文档。后续若需要追溯,优先查看 `langbot-skills/reports/` 下的原始执行报告。
|
||||||
|
|
||||||
|
截至 2026-05-29,已有本地 smoke 证明:
|
||||||
|
|
||||||
|
- `local-agent` 可以通过 Pipeline Debug Chat 走插件化 `AgentRunOrchestrator` 主链路。
|
||||||
|
- Claude Code runner 可以通过同一条 `run(event, binding)` 路径执行。
|
||||||
|
- Claude Code runner 可以读取 LangBot event-first context / skill / MCP 投影,并写回 `external.session_id` / `external.working_directory`。
|
||||||
|
- Codex runner 可以通过同一条路径执行,并把 Codex `thread_id` 写回 host-owned state。
|
||||||
|
|
||||||
|
这些记录只证明本地协议闭环可用,不代表发布级 security hardening 已完成。
|
||||||
160
docs/agent-runner-pluginization/PROGRESS.md
Normal file
160
docs/agent-runner-pluginization/PROGRESS.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Agent Runner 插件化实现进度
|
||||||
|
|
||||||
|
本文档跟踪 Agent Runner 插件化的实现状态,便于快速了解当前进度。
|
||||||
|
|
||||||
|
> 本文是 agent-runner 插件化**实现状态的唯一事实源**。协议规范见 [PROTOCOL_V1.md](./PROTOCOL_V1.md),Host 架构见 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md)。规范类文档不再各自维护"当前状态/✅"段落,状态一律以本文为准。
|
||||||
|
|
||||||
|
## 总体进度
|
||||||
|
|
||||||
|
**当前阶段**: Phase 3.5 已完成,Event-first 基础设施已完成;2026-05-29 已通过本地 `local-agent` 与 Claude Code runner smoke。
|
||||||
|
|
||||||
|
| Phase | 描述 | 状态 |
|
||||||
|
|-------|------|------|
|
||||||
|
| Phase 0 | PoC 验证 | ✅ 完成 |
|
||||||
|
| Phase 1 | 核心架构(Registry、Orchestrator、上下文模型) | ✅ 完成 |
|
||||||
|
| Phase 2 | 权限、能力声明、资源注入 | ✅ 完成 |
|
||||||
|
| Phase 3 | 内置 runner 迁移到插件 | ✅ 完成(7/7) |
|
||||||
|
| Phase 3.5 | Event-first 基础设施 | ✅ 完成 |
|
||||||
|
| Phase 3.6 | 外部 harness runner 协议 smoke | ✅ 完成(Claude Code MVP) |
|
||||||
|
| Phase 4 | EBA 事件支持 | 🔲 未开始(已预留 event-first 入口,EventGateway 由其他分支实现) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细状态
|
||||||
|
|
||||||
|
### SDK 侧 (`langbot-plugin-sdk`)
|
||||||
|
|
||||||
|
| 组件 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| `AgentRunner` 组件 | ✅ | `api/definition/components/agent_runner/runner.py` |
|
||||||
|
| `AgentRunContext` | ✅ | `api/entities/builtin/agent_runner/context.py` |
|
||||||
|
| `AgentRunResult` | ✅ | `api/entities/builtin/agent_runner/result.py` |
|
||||||
|
| `AgentRunnerCapabilities` | ✅ | `api/entities/builtin/agent_runner/capabilities.py` |
|
||||||
|
| `AgentRunnerPermissions` | ✅ | `api/entities/builtin/agent_runner/permissions.py` |
|
||||||
|
| EBA 事件模型 (Event/Actor/Subject) | ✅ | `api/entities/builtin/agent_runner/event.py` |
|
||||||
|
| `LIST_AGENT_RUNNERS` action | ✅ | `runtime/io/handlers/control.py` |
|
||||||
|
| `RUN_AGENT` action | ✅ | `runtime/io/handlers/control.py` |
|
||||||
|
| `AgentRunAPIProxy` | ✅ | `api/proxies/agent_run_api.py` |
|
||||||
|
| Pull API handlers (State/History/Event/Artifact) | ✅ | `runtime/io/handlers/plugin.py` |
|
||||||
|
| `caller_plugin_identity` injection | ✅ | Pull API handlers inject caller identity |
|
||||||
|
|
||||||
|
### LangBot 侧
|
||||||
|
|
||||||
|
| 组件 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| `AgentRunnerRegistry` | ✅ | `pkg/agent/runner/registry.py` |
|
||||||
|
| `AgentRunOrchestrator` | ✅ | `pkg/agent/runner/orchestrator.py` - event-first `run(event, binding)` |
|
||||||
|
| `AgentRunnerDescriptor` | ✅ | `pkg/agent/runner/descriptor.py` |
|
||||||
|
| `AgentResourceBuilder` | ✅ | `pkg/agent/runner/resource_builder.py` |
|
||||||
|
| `AgentRunContextBuilder` | ✅ | `pkg/agent/runner/context_builder.py` - event-first context |
|
||||||
|
| `AgentResultNormalizer` | ✅ | `pkg/agent/runner/result_normalizer.py` |
|
||||||
|
| `ConfigMigration` | ✅ | `pkg/agent/runner/config_migration.py` |
|
||||||
|
| `QueryEntryAdapter` | ✅ | `pkg/agent/runner/query_entry_adapter.py` - Query → Event + Binding |
|
||||||
|
| `run_from_query()` → `run(event, binding)` | ✅ | Pipeline 路径委托到 event-first path |
|
||||||
|
| `ChatMessageHandler` 集成 | ✅ | 使用 orchestrator 替代 wrapper |
|
||||||
|
| `PipelineService` 集成 | ✅ | 从 registry 获取 runner metadata |
|
||||||
|
| Plugin connector | ✅ | `list_agent_runners()` / `run_agent()` |
|
||||||
|
| `EventLogStore` | ✅ | `pkg/agent/runner/event_log_store.py` |
|
||||||
|
| `TranscriptStore` | ✅ | `pkg/agent/runner/transcript_store.py` |
|
||||||
|
| `ArtifactStore` | ✅ | `pkg/agent/runner/artifact_store.py` |
|
||||||
|
| `PersistentStateStore` | ✅ | `pkg/agent/runner/persistent_state_store.py` |
|
||||||
|
| History / Event pull APIs | ✅ | Orchestrator + APIProxy |
|
||||||
|
| Artifact pull APIs | ✅ | Orchestrator + APIProxy |
|
||||||
|
| State pull APIs | ✅ | Orchestrator + APIProxy |
|
||||||
|
| `artifact.created` / `state.updated` handling | ✅ | Event-first handlers in orchestrator |
|
||||||
|
| Pipeline path host capability coverage | ✅ | EventLog/Transcript/ArtifactStore/PersistentStateStore |
|
||||||
|
| External harness state handoff | ✅ | `external.session_id` / `external.working_directory` 写入 PersistentStateStore |
|
||||||
|
|
||||||
|
### 官方插件
|
||||||
|
|
||||||
|
> 外部服务插件仓库:`/home/glwuy/langbot-app/langbot-agent-runner/`
|
||||||
|
> 本地 Local Agent 插件仓库:`/home/glwuy/langbot-app/langbot-local-agent/`
|
||||||
|
|
||||||
|
| 插件 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| `local-agent` | ✅ 已完成 | 核心功能:模型、工具、知识库、流式、会话 |
|
||||||
|
| `dify-agent` | ✅ 已完成 | 支持 chat/agent/workflow 三种应用类型 |
|
||||||
|
| `n8n-agent` | ✅ 已完成 | Webhook 调用,支持 basic/jwt/header 认证 |
|
||||||
|
| `coze-agent` | ✅ 已完成 | 多模态输入,思维链处理 |
|
||||||
|
| `claude-code-agent` | ✅ MVP smoke 通过 | 本地 Claude Code CLI;context / skill / MCP 投影;host-owned resume state |
|
||||||
|
| `dashscope-agent` | ✅ 已完成 | 阿里云百炼,支持 agent/workflow 两种模式 |
|
||||||
|
| `langflow-agent` | ✅ 已完成 | SSE 流式,tweaks 配置支持 |
|
||||||
|
| `tbox-agent` | ✅ 已完成 | 蚂蚁百宝箱,多模态输入 |
|
||||||
|
|
||||||
|
**注意**: LangBot 内置 runner(`pkg/provider/runners/`)已停用,文件顶部添加了 DEPRECATED 注释。
|
||||||
|
|
||||||
|
### 本地验收
|
||||||
|
|
||||||
|
| 日期 | 范围 | 状态 | 证据 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 2026-05-29 | `local-agent` Pipeline Debug Chat | ✅ PASS | `langbot-skills/reports/2026-05-29-17-59-00-462-08-00-pipeline-debug-chat.md` |
|
||||||
|
| 2026-05-29 | `claude-code-agent` Pipeline Debug Chat | ✅ PASS | `langbot-skills/reports/2026-05-29-18-03-31-169-08-00-pipeline-debug-chat.md` |
|
||||||
|
| 2026-05-29 | Claude Code context / skill / MCP projection | ✅ PASS | `langbot-skills/reports/claude-code-agent-resource-context-20260529.md` |
|
||||||
|
| 2026-05-29 | Claude Code resume state | ✅ PASS | `langbot-skills/reports/claude-code-agent-real-workdir-20260529.md` |
|
||||||
|
| 2026-05-29 | `codex-agent` Debug Chat + thread_id resume state | ✅ PASS | 见 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md) §10 / `langbot-skills/reports/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未完成但仍属本分支收尾
|
||||||
|
|
||||||
|
以下项目属于本分支收尾工作:
|
||||||
|
|
||||||
|
- [x] Smoke / manual validation — `local-agent`、Claude Code MVP、Codex MVP 已通过本地 WebUI smoke
|
||||||
|
- [ ] Docs final QA
|
||||||
|
- [ ] Claude Code runner 文档、安装和 marketplace 发布准备
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 非本分支范围
|
||||||
|
|
||||||
|
以下能力由其他分支负责:
|
||||||
|
|
||||||
|
| 能力 | 负责分支 | 备注 |
|
||||||
|
|------|----------|------|
|
||||||
|
| EventGateway implementation | event branch | 完整事件网关、事件路由、持久化管理 |
|
||||||
|
| Event subscription / notification | event branch | 事件订阅、推送通知 |
|
||||||
|
| BindingResolver persistence UI | 其他模块 | 绑定配置的持久化 UI |
|
||||||
|
| Event router integration | event branch | 与 BindingResolver 集成 |
|
||||||
|
| Scheduler / background event source | 其他模块 | 定时任务、后台事件源 |
|
||||||
|
| Security release hardening | 后续 release gate | 路径隔离、权限边界、secret、MCP/skill 投影策略、资源配额、审计 |
|
||||||
|
| Codex / Kimi runner 全量接入 | 后续 runner 插件工作 | Codex MVP 已打通;Codex 发布级能力、Kimi runner 和全量 hardening 仍不扩大到当前协议闭环 |
|
||||||
|
| Issue-centric 产品模型 / 异步队列 / workflow engine | 后续产品架构 | 不属于当前 agent-runner plugin 协议闭环 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
### 高优先级
|
||||||
|
|
||||||
|
- [x] 工具详情 API — SDK `GET_TOOL_DETAIL` action、`AgentRunAPIProxy.get_tool_detail()` 与 Host 侧授权校验已接通
|
||||||
|
- [x] Pipeline `run_from_query()` → `run(event, binding)` — 已完成
|
||||||
|
- [x] EventLog / Transcript / ArtifactStore / PersistentStateStore — 已完成
|
||||||
|
- [x] History / Event / Artifact / State pull APIs — 已完成
|
||||||
|
- [x] `caller_plugin_identity` 验证路径 — 已完成
|
||||||
|
|
||||||
|
### 低优先级 / 未来
|
||||||
|
|
||||||
|
- [ ] EBA 完整集成 — EventGateway、event subscription、event notification 由其他分支实现
|
||||||
|
- [ ] 平台 API 动作执行 — `action.requested` 结果类型存在但未执行
|
||||||
|
- [ ] 安全发布级 hardening — 作为生产默认启用前的 release gate,不阻塞当前协议闭环
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键决策记录
|
||||||
|
|
||||||
|
| 日期 | 决策 |
|
||||||
|
|------|------|
|
||||||
|
| 2026-05-10 | Phase 0 集成测试通过,SDK v1 协议验证成功 |
|
||||||
|
| 2026-05-13 | Phase 3 完成:所有 7 个官方 runner 插件迁移完成 |
|
||||||
|
| 2026-05-23 | Phase 3.5 完成:`run_from_query()` 委托到 event-first `run(event, binding)`,Pipeline path 获得 host capabilities |
|
||||||
|
| 2026-05-29 | 本地 `local-agent` 与 `claude-code-agent` 通过 WebUI smoke;Claude Code runner 验证 external harness context 投影和 host-owned resume state |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [README.md](./README.md) — 总体设计与路由
|
||||||
|
- [PROTOCOL_V1.md](./PROTOCOL_V1.md) — 协议规范(唯一 schema 事实源)
|
||||||
|
- [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md) — Agent Runner QA 指南和下一轮测试入口
|
||||||
|
- [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md) — 官方插件仓库计划
|
||||||
|
- [SECURITY_HARDENING.md](./SECURITY_HARDENING.md) — 安全发布级 hardening 后续门槛
|
||||||
531
docs/agent-runner-pluginization/PROTOCOL_V1.md
Normal file
531
docs/agent-runner-pluginization/PROTOCOL_V1.md
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
# LangBot AgentRunner Protocol v1
|
||||||
|
|
||||||
|
本文档是 LangBot Host 与插件 SDK / Runtime / AgentRunner 之间协议合同的**唯一规范来源(single source of truth)**。
|
||||||
|
|
||||||
|
- 本文件描述"稳定接口应是什么",是 normative spec,不混入实现进度。实现状态见 [PROGRESS.md](./PROGRESS.md)。
|
||||||
|
- 本文件之外的任何文档**不得重新定义这里的数据结构**,只能引用,例如"见 PROTOCOL_V1 §4.2"。
|
||||||
|
- Host 内部模型(`AgentEventEnvelope`、`AgentBinding`、Descriptor、各 Store)不属于 SDK 协议,定义在 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md)。
|
||||||
|
|
||||||
|
## 1. 协议目标
|
||||||
|
|
||||||
|
Protocol v1 只解决四件事:
|
||||||
|
|
||||||
|
- LangBot 如何发现插件提供的 AgentRunner。
|
||||||
|
- LangBot 如何把一次事件调用封装成 `AgentRunContext`。
|
||||||
|
- AgentRunner 如何以事件流形式返回运行结果。
|
||||||
|
- AgentRunner 如何通过受限 API 访问 LangBot host 能力。
|
||||||
|
|
||||||
|
Protocol v1 **不定义**:
|
||||||
|
|
||||||
|
- LangBot 内部如何持久化 `AgentBinding`(见 HOST_SDK)。
|
||||||
|
- AgentRunner 内部如何组装 prompt、压缩历史、管理 memory(见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md))。
|
||||||
|
- 官方 runner 的具体实现(见 [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md))。
|
||||||
|
- Pipeline 的长期配置模型。
|
||||||
|
- 发布级安全 hardening 的完整实现(见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md))。
|
||||||
|
|
||||||
|
## 2. 参与方
|
||||||
|
|
||||||
|
| 名称 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| LangBot Host | 事件入口、绑定解析、权限、资源、存储、生命周期、结果投递。 |
|
||||||
|
| Plugin Runtime | 加载插件,响应 Host 的 runner discovery 和 run 调用。 |
|
||||||
|
| AgentRunner | 插件提供的 agent 执行组件。 |
|
||||||
|
| AgentRunAPIProxy | AgentRunner 访问 Host 能力的受限 API。 |
|
||||||
|
| AgentBinding | Host 内部的事件到 runner 绑定配置,不直接暴露给 SDK(见 HOST_SDK §4.2)。 |
|
||||||
|
|
||||||
|
产品层的 `Agent` 替代旧 Pipeline 承载 agent 配置:bot / IM channel
|
||||||
|
绑定一个 Agent,一个 Agent 可以被多个 bot / channel 复用。Host 内部的
|
||||||
|
`AgentBinding` 是一次事件运行前解析出的有效绑定,只影响 Host 构造出的
|
||||||
|
`ctx.config`、`ctx.resources`、`ctx.context` 和 `ctx.delivery`。SDK 不需要知道
|
||||||
|
Agent / binding 的持久化形态。
|
||||||
|
|
||||||
|
外部 harness runner(Claude Code、Codex、Kimi Code 等)也是 `AgentRunner`:它们消费 event-first `AgentRunContext`、返回 `AgentRunResult`,并通过 Host 授权的 state/storage/artifact API 保存跨轮次指针。它们内部可以继续使用自己的 session、tool loop、MCP、上下文压缩和权限模型。
|
||||||
|
|
||||||
|
## 3. 版本协商
|
||||||
|
|
||||||
|
- `AgentRunnerManifest.protocol_version` 声明 runner 实现的协议大版本,当前为 `"1"`。
|
||||||
|
- `AgentRuntimeContext.protocol_version`(`ctx.runtime.protocol_version`)声明 Host 下发的协议大版本。
|
||||||
|
- Host 发现 runner 时校验 `protocol_version` 兼容性;不兼容的 runner 不进入可用列表,只记 warning。
|
||||||
|
- 字段级演进规则:新增可选字段不提升大版本;删除或改语义需要提升大版本。
|
||||||
|
- 结果流演进:Host **必须忽略未知 result type 并记录 warning**(除非该 type 明确要求强校验)。新增 result type 不提升大版本。
|
||||||
|
|
||||||
|
## 4. Discovery 协议
|
||||||
|
|
||||||
|
### 4.1 LIST_AGENT_RUNNERS
|
||||||
|
|
||||||
|
Host 调用 Plugin Runtime 获取当前插件暴露的 runner 列表,请求无额外 payload。返回:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ListAgentRunnersResponse(BaseModel):
|
||||||
|
runners: list[AgentRunnerManifest]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 AgentRunnerManifest
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerManifest(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
label: I18nObject
|
||||||
|
description: I18nObject | None = None
|
||||||
|
protocol_version: str = "1"
|
||||||
|
capabilities: AgentRunnerCapabilities
|
||||||
|
permissions: AgentRunnerPermissions
|
||||||
|
context: AgentRunnerContextPolicy
|
||||||
|
config_schema: list[DynamicFormItemSchema] = []
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `id` 必须稳定,格式 `plugin:author/name/runner`。
|
||||||
|
- `name` 是插件内 runner 名称,例如 `default`。
|
||||||
|
- `config_schema` 只描述绑定配置表单,不代表插件实例状态。
|
||||||
|
- `metadata` 只放展示、诊断、非稳定扩展信息。
|
||||||
|
|
||||||
|
### 4.3 Capabilities
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerCapabilities(BaseModel):
|
||||||
|
streaming: bool = False
|
||||||
|
tool_calling: bool = False
|
||||||
|
knowledge_retrieval: bool = False
|
||||||
|
multimodal_input: bool = False
|
||||||
|
event_context: bool = True
|
||||||
|
platform_api: bool = False
|
||||||
|
interrupt: bool = False
|
||||||
|
stateful_session: bool = False
|
||||||
|
self_managed_context: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
语义:
|
||||||
|
|
||||||
|
- `streaming`: runner 可以返回 `message.delta`。
|
||||||
|
- `tool_calling`: runner 可能调用 Host tool API。
|
||||||
|
- `knowledge_retrieval`: runner 可能调用 Host knowledge API。
|
||||||
|
- `multimodal_input`: runner 可以处理非纯文本 input / artifact。
|
||||||
|
- `event_context`: runner 理解 event-first 输入。
|
||||||
|
- `platform_api`: runner 可能请求平台动作。
|
||||||
|
- `interrupt`: runner 支持取消或中断。
|
||||||
|
- `stateful_session`: runner 可能维护跨 run 会话状态。
|
||||||
|
- `self_managed_context`: runner 自己管理 working context,Host 不应默认 inline 历史。
|
||||||
|
|
||||||
|
> Capabilities 字段全部是 `bool`。runner 是否寄宿 host-owned state **不在 capabilities 表达**,而通过 `permissions.storage` 声明(见 §4.4),避免出现非 bool 取值。
|
||||||
|
|
||||||
|
### 4.4 Permissions
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerPermissions(BaseModel):
|
||||||
|
models: list[Literal["invoke", "stream", "rerank"]] = []
|
||||||
|
tools: list[Literal["detail", "call"]] = []
|
||||||
|
knowledge_bases: list[Literal["list", "retrieve"]] = []
|
||||||
|
history: list[Literal["page", "search"]] = []
|
||||||
|
events: list[Literal["get", "page"]] = []
|
||||||
|
artifacts: list[Literal["metadata", "read"]] = []
|
||||||
|
storage: list[Literal["plugin", "workspace", "binding"]] = []
|
||||||
|
files: list[Literal["config", "knowledge"]] = []
|
||||||
|
platform_api: list[str] = []
|
||||||
|
```
|
||||||
|
|
||||||
|
Manifest permissions 是 runner 需要的**最大能力**。实际可用资源还要经过 Host binding policy 和当前 run scope 裁剪(三层裁剪见 HOST_SDK §4.5)。
|
||||||
|
|
||||||
|
### 4.5 Context Policy
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerContextPolicy(BaseModel):
|
||||||
|
supports_history_pull: bool = True
|
||||||
|
supports_history_search: bool = False
|
||||||
|
supports_artifact_pull: bool = True
|
||||||
|
owns_compaction: bool = True
|
||||||
|
wants_static_context_refs: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Host 不使用该声明给 runner inline 历史窗口。默认原则:
|
||||||
|
|
||||||
|
- Host 不得默认 inline 全量历史。
|
||||||
|
- Host 只 inline 当前 event / input 和 context handles。
|
||||||
|
- Runner 拥有 working context assembly。
|
||||||
|
- Runner 可在授权后通过 Host history / event / artifact / state API 拉取更多上下文。
|
||||||
|
- 历史窗口策略不属于 Protocol v1 字段,也不属于 Host 通用语义。
|
||||||
|
|
||||||
|
context 边界的设计理由见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md)。
|
||||||
|
|
||||||
|
## 5. Run 协议
|
||||||
|
|
||||||
|
### 5.1 RUN_AGENT
|
||||||
|
|
||||||
|
Host 调用 Runtime:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunRequest(BaseModel):
|
||||||
|
runner_id: str
|
||||||
|
runner_name: str
|
||||||
|
context: AgentRunContext
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime 返回 `AgentRunResult` 异步流。底层 transport 可继续用 `plugin_author` / `plugin_name` / `runner_name` 定位组件,但协议语义以 `runner_id` 和 `context` 为准。
|
||||||
|
|
||||||
|
### 5.2 AgentRunContext
|
||||||
|
|
||||||
|
这是 SDK 看到的**唯一权威 context 定义**。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunContext(BaseModel):
|
||||||
|
run_id: str
|
||||||
|
trigger: AgentTrigger
|
||||||
|
event: AgentEventContext
|
||||||
|
conversation: ConversationContext | None = None
|
||||||
|
actor: ActorContext | None = None
|
||||||
|
subject: SubjectContext | None = None
|
||||||
|
input: AgentInput
|
||||||
|
delivery: DeliveryContext
|
||||||
|
resources: AgentResources
|
||||||
|
context: ContextAccess
|
||||||
|
state: AgentRunState
|
||||||
|
runtime: AgentRuntimeContext
|
||||||
|
config: dict[str, Any] = {}
|
||||||
|
adapter: AdapterContext | None = None
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
核心约束:
|
||||||
|
|
||||||
|
- `event` 是必选字段,Protocol v1 是 event-first。
|
||||||
|
- `input` 表示当前事件的主输入,不等于历史消息。
|
||||||
|
- `bootstrap` / `messages` **不是协议字段**;Host 不内联历史窗口。
|
||||||
|
- `adapter` 只放入口 adapter 的非核心元数据,runner 不应依赖它做长期能力。
|
||||||
|
- `config` 是 Agent/runner config,不是插件实例状态。
|
||||||
|
|
||||||
|
### 5.3 AgentTrigger
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentTrigger(BaseModel):
|
||||||
|
type: str
|
||||||
|
source: Literal["platform", "webui", "api", "scheduler", "system", "host_adapter"]
|
||||||
|
timestamp: int | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
`trigger.type` 应与 `event.event_type` 一致或更粗粒度。例如入口适配器触发消息时:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "message.received", "source": "host_adapter" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 AgentEventContext
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentEventContext(BaseModel):
|
||||||
|
event_id: str
|
||||||
|
event_type: str
|
||||||
|
event_time: int | None = None
|
||||||
|
source: str
|
||||||
|
source_event_type: str | None = None
|
||||||
|
raw_ref: RawEventRef | None = None
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `event_type` 使用 LangBot 稳定协议名,例如 `message.received`。稳定事件名清单见 [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md)。
|
||||||
|
- 平台原始事件名放入 `source_event_type`。
|
||||||
|
- 大型原始 payload 必须放入 `raw_ref` 或 artifact,不应直接塞入 `data`。
|
||||||
|
|
||||||
|
### 5.5 Conversation / Actor / Subject
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ConversationContext(BaseModel):
|
||||||
|
conversation_id: str | None = None
|
||||||
|
thread_id: str | None = None
|
||||||
|
launcher_type: str | None = None
|
||||||
|
launcher_id: str | None = None
|
||||||
|
bot_id: str | None = None
|
||||||
|
workspace_id: str | None = None
|
||||||
|
|
||||||
|
class ActorContext(BaseModel):
|
||||||
|
actor_type: str
|
||||||
|
actor_id: str | None = None
|
||||||
|
actor_name: str | None = None
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
|
||||||
|
class SubjectContext(BaseModel):
|
||||||
|
subject_type: str
|
||||||
|
subject_id: str | None = None
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
- 消息事件:actor 是发消息的人,subject 是当前消息。
|
||||||
|
- 入群事件:actor 是新成员或邀请人,subject 是群/成员关系。
|
||||||
|
- 定时事件:actor 可以是 system,subject 是 schedule。
|
||||||
|
|
||||||
|
### 5.6 AgentInput
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentInput(BaseModel):
|
||||||
|
text: str | None = None
|
||||||
|
contents: list[ContentElement] = []
|
||||||
|
attachments: list[ArtifactRef] = []
|
||||||
|
message_chain: dict[str, Any] | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
- 文本、多模态、附件都属于当前 event input。
|
||||||
|
- 大文件、图片、音频、工具大结果应以 artifact ref 传递。
|
||||||
|
- `message_chain` 是平台兼容字段,不应成为长期稳定依赖。
|
||||||
|
|
||||||
|
### 5.7 DeliveryContext
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DeliveryContext(BaseModel):
|
||||||
|
surface: str
|
||||||
|
reply_target: dict[str, Any] | None = None
|
||||||
|
supports_streaming: bool = False
|
||||||
|
supports_edit: bool = False
|
||||||
|
supports_reaction: bool = False
|
||||||
|
max_message_size: int | None = None
|
||||||
|
platform_capabilities: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Runner 可参考 delivery 能力决定返回 `message.delta`、`message.completed` 或 `action.requested`。
|
||||||
|
|
||||||
|
### 5.8 ContextAccess
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ContextAccess(BaseModel):
|
||||||
|
conversation_id: str | None = None
|
||||||
|
thread_id: str | None = None
|
||||||
|
latest_cursor: str | None = None
|
||||||
|
event_seq: int | None = None
|
||||||
|
transcript_seq: int | None = None
|
||||||
|
has_history_before: bool = False
|
||||||
|
inline_policy: InlineContextPolicy
|
||||||
|
available_apis: ContextAPICapabilities
|
||||||
|
|
||||||
|
class InlineContextPolicy(BaseModel):
|
||||||
|
mode: Literal["none", "current_event", "recent_tail", "summary_tail"]
|
||||||
|
delivered_count: int = 0
|
||||||
|
source_total_count: int | None = None
|
||||||
|
messages_complete: bool = False
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
class ContextAPICapabilities(BaseModel):
|
||||||
|
history_page: bool = False
|
||||||
|
history_search: bool = False
|
||||||
|
event_get: bool = False
|
||||||
|
event_page: bool = False
|
||||||
|
artifact_metadata: bool = False
|
||||||
|
artifact_read: bool = False
|
||||||
|
state: bool = False
|
||||||
|
storage: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
`ContextAccess` 告诉 runner:Host inline 了什么、没 inline 什么、需要更多上下文时走哪些 API。它是 runner 按需读取上下文的入口说明,不是 Host 的业务上下文编排策略。
|
||||||
|
|
||||||
|
### 5.9 AgentRuntimeContext
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRuntimeContext(BaseModel):
|
||||||
|
host: str = "langbot"
|
||||||
|
protocol_version: str = "1"
|
||||||
|
langbot_version: str | None = None
|
||||||
|
trace_id: str
|
||||||
|
deadline_at: float | None = None
|
||||||
|
locale: str | None = None
|
||||||
|
timezone: str | None = None
|
||||||
|
static_refs: dict[str, StaticContextRef] = {}
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
`static_refs` 用于 KV cache 友好的静态上下文引用(system policy、tool schema、resource manifest 的 hash/version)。理由见 AGENT_CONTEXT_PROTOCOL §6。
|
||||||
|
|
||||||
|
### 5.10 AgentRunState
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunState(BaseModel):
|
||||||
|
conversation: dict[str, Any] = {}
|
||||||
|
actor: dict[str, Any] = {}
|
||||||
|
subject: dict[str, Any] = {}
|
||||||
|
runner: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
State 是可选 host-owned snapshot。Runner 也可以完全自管状态。
|
||||||
|
|
||||||
|
## 6. Resources
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentResources(BaseModel):
|
||||||
|
models: list[ModelResource] = []
|
||||||
|
tools: list[ToolResource] = []
|
||||||
|
knowledge_bases: list[KnowledgeBaseResource] = []
|
||||||
|
files: list[FileResource] = []
|
||||||
|
storage: StorageResource = StorageResource()
|
||||||
|
platform_capabilities: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
资源列表是本次 run 的授权结果。History / Event / Artifact 访问通过 permissions、`ctx.context.available_apis` 和 Host 侧 run session 校验控制,不作为可枚举 resource list 暴露。Runner 只能通过 `AgentRunAPIProxy` 访问这些能力。
|
||||||
|
|
||||||
|
## 7. Result Stream
|
||||||
|
|
||||||
|
### 7.1 AgentRunResult
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunResult(BaseModel):
|
||||||
|
run_id: str
|
||||||
|
type: str
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
sequence: int | None = None
|
||||||
|
timestamp: int | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 稳定 result types
|
||||||
|
|
||||||
|
| type | 说明 | 当前消费 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `message.delta` | 流式消息片段。 | ✅ |
|
||||||
|
| `message.completed` | 完整消息。 | ✅ |
|
||||||
|
| `tool.call.started` | 工具调用开始的可观测事件。 | telemetry |
|
||||||
|
| `tool.call.completed` | 工具调用完成的可观测事件。 | telemetry |
|
||||||
|
| `artifact.created` | runner 生成 artifact。 | ✅ |
|
||||||
|
| `state.updated` | runner 请求更新 host-owned state。 | ✅ |
|
||||||
|
| `action.requested` | runner 请求 Host 执行平台动作。 | **reserved / 仅 telemetry,不执行** |
|
||||||
|
| `run.completed` | run 正常结束。 | ✅ |
|
||||||
|
| `run.failed` | run 失败。 | ✅ |
|
||||||
|
|
||||||
|
`action.requested` 是为 EBA 和 platform API 预留的协议表面:当前阶段 Host 收到后只记 telemetry,**不执行**,runner 作者不应依赖其副作用。执行模型见 EVENT_BASED_AGENT §6。
|
||||||
|
|
||||||
|
### 7.3 示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "message.delta", "data": { "chunk": { "role": "assistant", "content": "hel" } } }
|
||||||
|
{ "type": "message.completed", "data": { "message": { "role": "assistant", "content": "hello" } } }
|
||||||
|
{ "type": "state.updated", "data": { "scope": "conversation", "key": "external.session_id", "value": "abc" } }
|
||||||
|
{ "type": "action.requested", "data": { "action": "message.edit", "target": {"message_id": "..."}, "payload": {"text": "..."} } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Host 必须校验 `state.updated` 的 scope、key、value 大小和 JSON 可序列化性。
|
||||||
|
|
||||||
|
## 8. AgentRunAPIProxy
|
||||||
|
|
||||||
|
所有 proxy action 必须携带 `run_id`。Host 必须校验:active run session 存在、caller plugin identity 匹配、resource 在本次 `ctx.resources` 中授权、scope 不越界、payload size / rate limit / deadline 合法。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Model
|
||||||
|
await api.models.invoke(model_id, messages, tools=None, extra_args=None)
|
||||||
|
await api.models.stream(model_id, messages, tools=None, extra_args=None)
|
||||||
|
await api.models.rerank(model_id, query, documents, top_k=None)
|
||||||
|
|
||||||
|
# Tool
|
||||||
|
await api.tools.get_detail(tool_name)
|
||||||
|
await api.tools.call(tool_name, parameters)
|
||||||
|
|
||||||
|
# Knowledge
|
||||||
|
await api.knowledge.retrieve(kb_id, query_text, top_k=5, filters=None)
|
||||||
|
|
||||||
|
# History(返回 Transcript projection,不返回原始平台 payload)
|
||||||
|
await api.history.page(conversation_id=None, before_cursor=None, after_cursor=None,
|
||||||
|
limit=50, direction="backward", include_artifacts=False)
|
||||||
|
await api.history.search(query, filters=None, top_k=10)
|
||||||
|
|
||||||
|
# Event(返回稳定 event envelope 或受限 raw ref,不默认返回大 payload)
|
||||||
|
await api.events.get(event_id)
|
||||||
|
await api.events.page(before_cursor=None, limit=50)
|
||||||
|
|
||||||
|
# Artifact(必须支持大小限制、MIME 校验、过期时间和授权范围)
|
||||||
|
await api.artifacts.metadata(artifact_id)
|
||||||
|
await api.artifacts.read_range(artifact_id, offset=0, length=65536)
|
||||||
|
await api.artifacts.open_stream(artifact_id)
|
||||||
|
|
||||||
|
# State / Storage
|
||||||
|
await api.state.get(scope, key); await api.state.set(scope, key, value); await api.state.delete(scope, key)
|
||||||
|
await api.storage.get(area, key); await api.storage.set(area, key, value)
|
||||||
|
await api.storage.delete(area, key); await api.storage.list(area, prefix=None)
|
||||||
|
|
||||||
|
# Platform(受限能力,默认不开放,需 manifest + binding policy + 用户审批同时允许)
|
||||||
|
await api.platform.request_action(action, target, payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
`state` 与 `storage` 的建议边界:`state` 放小型 JSON(conversation / actor / runner / binding),`storage` 放 blob 或较大数据(插件私有数据、workspace 数据、checkpoint)。
|
||||||
|
|
||||||
|
返回数据结构(如 `HistoryPage`、artifact metadata)见 AGENT_CONTEXT_PROTOCOL §4。
|
||||||
|
|
||||||
|
## 9. 错误模型
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentAPIError(BaseModel):
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
retryable: bool = False
|
||||||
|
details: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
| code | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `unauthorized` | 未授权访问资源或 scope。 |
|
||||||
|
| `not_found` | 资源不存在或对当前 runner 不可见。 |
|
||||||
|
| `deadline_exceeded` | 超过 run deadline。 |
|
||||||
|
| `payload_too_large` | 请求或响应过大。 |
|
||||||
|
| `rate_limited` | Host 限流。 |
|
||||||
|
| `invalid_argument` | 参数错误。 |
|
||||||
|
| `runtime_error` | Host 或下游能力错误。 |
|
||||||
|
|
||||||
|
Runner 失败使用 `run.failed`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "run.failed", "data": { "code": "runner.error", "message": "failed to call external agent", "retryable": false } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Timeout 与 Cancellation
|
||||||
|
|
||||||
|
- Host 在 `ctx.runtime.deadline_at` 下发总 deadline;SDK proxy 必须用该 deadline 限制单次 action timeout。
|
||||||
|
- Host 可以取消 active run;Runtime 应尽力中断 runner。
|
||||||
|
- Runner 支持中断时应返回或触发 `run.failed`,code 为 `cancelled`。
|
||||||
|
- Host 必须 unregister active run session。
|
||||||
|
|
||||||
|
## 11. Security 与 Guardrail(协议层)
|
||||||
|
|
||||||
|
Protocol v1 的安全边界在 Host:
|
||||||
|
|
||||||
|
- Runner 不能直接访问未授权 model/tool/kb/history/artifact/storage。
|
||||||
|
- SDK 本地校验只提升开发体验,不能替代 Host 校验。
|
||||||
|
- 所有 resource id 对 runner 来说都是 opaque。
|
||||||
|
- 默认只能访问当前 conversation / thread 的 history;跨会话、workspace 级访问必须额外授权。
|
||||||
|
- 大 payload 必须 artifact 化。
|
||||||
|
- Host 必须记录 run_id、runner_id、action、resource、scope、result。
|
||||||
|
|
||||||
|
Host 不负责业务编排:不拼接全量历史、不替 runner 做 prompt assembly、不内置 agent memory / tool loop / 上下文压缩策略。这些由官方或第三方 AgentRunner 插件实现。
|
||||||
|
|
||||||
|
对外部 harness runner,Host 在调用前完成 binding/resource policy 裁剪、路径策略、secret 过滤和审计;runner plugin 把授权后的 context/resource projection 适配为目标 harness 的形式;harness 的 native permission mode、allowed/disallowed tools 只是额外执行约束,不能替代 Host 授权。
|
||||||
|
|
||||||
|
> 发布级路径隔离、MCP allowlist、secret redaction、配额、workspace 清理等**不属于** v1 协议闭环,是生产默认启用前的 release gate,见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
|
||||||
|
## 12. Pipeline Adapter 边界
|
||||||
|
|
||||||
|
Pipeline 是当前入口 adapter,不是协议中心。目标产品模型中 Agent 会替代
|
||||||
|
Pipeline 承载 runner config、resource policy 和 delivery policy;当前 Query
|
||||||
|
entry adapter 只是迁移桥。它负责:
|
||||||
|
|
||||||
|
- 从 `Query` 构造 `AgentEventContext` 和临时 `AgentBinding`(见 HOST_SDK §4.2)。
|
||||||
|
- 从当前 Agent/runner config 构造 `ctx.config`。
|
||||||
|
- 将 Query-only 字段放入 `ctx.adapter`,例如 filtered params 放 `ctx.adapter.extra["params"]`。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- adapter **不**定义历史窗口、prompt 组装或 agentic context 策略。
|
||||||
|
- `ctx.adapter.extra` 只允许承载一次性、JSON-safe、入口相关的非核心元数据,例如 `params`;不得承载 `prompt`、history window、RAG 结果、tool schema 或授权资源。
|
||||||
|
- 静态绑定 prompt 属于 `ctx.config.prompt`。preprocessing / hook 后的动态有效指令不通过 `ctx.adapter.extra` 主动推送;后续如需要保留这类能力,应通过 Host prompt/instruction pull API 暴露(占位见 HOST_SDK §4.8)。
|
||||||
|
- 新 runner 不应长期依赖 `adapter`,应只依赖 event-first context 和 Host API。
|
||||||
|
|
||||||
|
## 13. 已确认约束
|
||||||
|
|
||||||
|
- v1 / EBA 主线是 `one event -> one AgentBinding -> one run_id -> one runner`。
|
||||||
|
- 一个 bot / IM channel 在同一时间只绑定一个负责 agentic 处理的 Agent;一个 Agent 可以被多个 bot / channel 复用。
|
||||||
|
- 如果配置层出现多个匹配 AgentBinding,BindingResolver 必须按明确规则选出一个或拒绝配置,不应默认 fan-out。
|
||||||
|
- observer agent、多 runner fan-out、并行裁决、result 合并等能力需要单独设计 delivery、state、platform action 和 audit 语义,不属于当前 v1 契约。
|
||||||
|
- `AgentRunnerDescriptor.source` 只允许 `plugin`;Host 内置 adapter 不能作为 runner source 绕过插件/runtime/proxy 权限链。
|
||||||
|
- `ctx.resources` 与 proxy action 校验必须来自同一个 run authorization snapshot;runtime handler 不应重新执行资源裁剪。
|
||||||
|
- 外部 harness runner 当前是 MVP / dev path,证明协议可接入,不代表发布级安全边界或 Docker 生产可用性完成。
|
||||||
|
|
||||||
|
## 14. 开放问题
|
||||||
|
|
||||||
|
- `AgentBinding` 是否需要进入 SDK 文档作为只读诊断信息,还是完全 Host 内部。
|
||||||
|
- `TranscriptItem` 的最小字段集如何定义。
|
||||||
|
- ArtifactStore 是否复用现有 BinaryStorage backend,还是引入独立实体。
|
||||||
|
- State 与 Storage 的边界是否需要更强类型。
|
||||||
|
- `platform_api` action 的审批模型如何表达。
|
||||||
|
- Host 侧 scoped MCP / skill / workspace projection 是否需要从 runner config 上移为一等 resource projection API。
|
||||||
147
docs/agent-runner-pluginization/README.md
Normal file
147
docs/agent-runner-pluginization/README.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Agent Runner 插件化文档入口
|
||||||
|
|
||||||
|
本文档是 agent-runner 插件化工作的路由页。具体设计拆到独立文档中维护,避免把 LangBot 宿主架构、SDK 协议、上下文管理、EBA 预留和官方 runner 迁移混在同一份 README 里。
|
||||||
|
|
||||||
|
## 文档维护原则(单一事实源)
|
||||||
|
|
||||||
|
- **协议数据结构(schema)唯一定义在 [PROTOCOL_V1.md](./PROTOCOL_V1.md)。** 其他文档不得重抄 schema,只能引用,例如"见 PROTOCOL_V1 §4.2"。
|
||||||
|
- **实现状态唯一记录在 [PROGRESS.md](./PROGRESS.md)。** 规范类文档不维护"当前状态/✅"段落。
|
||||||
|
- Host 内部模型(`AgentEventEnvelope`、`AgentBinding`、Descriptor、各 Store)定义在 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md),不属于 SDK 协议。
|
||||||
|
- 其余专题文档只讲"为什么/边界/怎么用",避免重复叙述。
|
||||||
|
|
||||||
|
## 本分支目标
|
||||||
|
|
||||||
|
**本分支目标:AgentRunner 外化 / 插件化基础设施**
|
||||||
|
|
||||||
|
本分支只做 LangBot 作为 Agent Host 的基础能力建设,为后续用 `Agent`
|
||||||
|
替代 Pipeline 承载 agent 配置打底:
|
||||||
|
|
||||||
|
- LangBot 与 SDK 的稳定协议合同(Protocol v1)
|
||||||
|
- Host-side `AgentEventEnvelope` / `AgentBinding` 模型
|
||||||
|
- `run(event, binding)` event-first 入口
|
||||||
|
- `QueryEntryAdapter`:Query → AgentEventEnvelope + AgentBinding
|
||||||
|
- EventLog / Transcript / ArtifactStore / PersistentStateStore
|
||||||
|
- History / Event / Artifact / State pull APIs
|
||||||
|
- SDK runtime forwarding pull APIs + `caller_plugin_identity` 验证路径
|
||||||
|
|
||||||
|
## 本分支不实现
|
||||||
|
|
||||||
|
以下能力由其他分支负责,本分支只预留 integration point:
|
||||||
|
|
||||||
|
- **EventGateway**:完整事件网关实现、事件路由、事件持久化管理
|
||||||
|
- **Event subscription / Event notification**:事件订阅、推送通知
|
||||||
|
- **BindingResolver persistence UI**:绑定配置的持久化 UI 和 event router 集成(如由其他模块负责)
|
||||||
|
- **Scheduler / Background event source**:定时任务、后台事件源
|
||||||
|
- **Runtime control plane v2**:runtime registry、heartbeat、task queue、daemon claim、progress/cancel 和 runtime audit
|
||||||
|
|
||||||
|
EventGateway 在本文档中描述为 **future integration point**,由外部 event branch 提供。本分支只定义 host-side envelope/binding models 和 `run(event, binding)` orchestrator 入口。
|
||||||
|
|
||||||
|
## 目标产品模型
|
||||||
|
|
||||||
|
未来产品层应把 `Agent` 理解为 Pipeline 的替代物:原先 bot 绑定
|
||||||
|
Pipeline,Pipeline 携带 agent/provider/RAG/tool 等配置;后续应改为 bot 或
|
||||||
|
IM channel 绑定一个 Agent,Agent 携带 runner id、runner config、
|
||||||
|
resource/state/delivery policy 等 agent 配置。
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 一个 bot / IM channel 在同一时间只绑定一个负责 agentic 处理的 Agent。
|
||||||
|
- 一个 Agent 可以被多个 bot / channel 复用,类似旧 Pipeline 可被多个 bot 共享。
|
||||||
|
- Agent 配置是运行绑定配置,不是插件实例状态;多个 Agent 指向同一
|
||||||
|
AgentRunner 时不创建多个插件实例。
|
||||||
|
- 当前 Pipeline path 只是迁移期入口 adapter:它把旧 Pipeline 配置投影为临时
|
||||||
|
`AgentBinding`,不代表目标架构仍由 Pipeline 承载 agent 语义。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
**当前 Pipeline 是入口 adapter,不再是 agent runner 设计核心。**
|
||||||
|
|
||||||
|
主入口仍可由 Pipeline 触发,但内部已转换成 event-first path:`run_from_query()` 经 `QueryEntryAdapter` 把 `Query` 转换为 `AgentEventEnvelope` + `AgentBinding`,再委托到统一的 `run(event, binding, ...)`。Pipeline path 因此获得了 event-first host capabilities(EventLog / Transcript / ArtifactStore / PersistentStateStore 写入,History / Event / Artifact / State pull API 可用)。
|
||||||
|
|
||||||
|
详细实现进度、已验收能力和未完成收尾见 [PROGRESS.md](./PROGRESS.md)。
|
||||||
|
|
||||||
|
## 设计文档
|
||||||
|
|
||||||
|
| 文档 | 关注点 |
|
||||||
|
| --- | --- |
|
||||||
|
| [PROTOCOL_V1.md](./PROTOCOL_V1.md) | **🔒 唯一 schema 事实源**。LangBot Host 与 SDK / Runtime / AgentRunner 的协议合同:版本协商、discovery、run context、result stream、proxy actions、错误和 adapter 边界。 |
|
||||||
|
| [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md) | LangBot 宿主能力与分层架构、Host 内部模型(`AgentEventEnvelope` / `AgentBinding` / Descriptor / 各 Store)、runner 发现、绑定、资源授权、状态、存储、生命周期和调用链。 |
|
||||||
|
| [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md) | Agent-owned context 方向:事件到来时 LangBot 传什么,agent 如何按需拉取更多历史 / artifact / state,以及如何支持 KV cache 友好的上下文管理。 |
|
||||||
|
| [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md) | EBA 预留:事件模型、事件来源、触发绑定、非消息事件如何复用 AgentRunner 调度。**标注为 future design note**。 |
|
||||||
|
| [RUNTIME_CONTROL_PLANE_V2.md](./RUNTIME_CONTROL_PLANE_V2.md) | Agent Platform v2 / runtime 管控面预留:Host 新增 runtime registry、heartbeat、task queue、daemon 执行和 audit;管理插件构建在这些 Host 能力之上。**标注为 future design note**。 |
|
||||||
|
| [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md) | 官方 runner 插件迁移,包括 local-agent 和外部 runner。它是下游落地计划,不是 LangBot 基础能力设计的前置约束。 |
|
||||||
|
| [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md) | Agent Runner QA 指南:保留最高价值测试路径,指导 agent 开展下一轮 WebUI / runner smoke 验证。 |
|
||||||
|
| [SECURITY_HARDENING.md](./SECURITY_HARDENING.md) | 安全发布级 hardening 的后续发布门槛:路径隔离、权限边界、secret、资源配额、MCP / skill 投影和审计。 |
|
||||||
|
| [PROGRESS.md](./PROGRESS.md) | **🔒 唯一状态事实源**。当前实现进度、已验收能力、未完成收尾和非本分支范围。 |
|
||||||
|
|
||||||
|
## 工作拆分
|
||||||
|
|
||||||
|
### 1. LangBot + SDK 基础设施
|
||||||
|
|
||||||
|
目标是把 LangBot 从内置 runner 执行器变成 agent host:
|
||||||
|
|
||||||
|
- LangBot 与 SDK 的稳定协议合同
|
||||||
|
- runner manifest / descriptor / registry
|
||||||
|
- Agent / binding 配置解析
|
||||||
|
- run orchestration 和生命周期管理
|
||||||
|
- resource authorization 与 `run_id` 级权限校验
|
||||||
|
- host-owned state / storage / event log / transcript / artifact 能力
|
||||||
|
- SDK `AgentRunner`、`AgentRunContext`、`AgentRunResult`、`AgentRunAPIProxy`
|
||||||
|
|
||||||
|
协议合同详见 [PROTOCOL_V1.md](./PROTOCOL_V1.md)。
|
||||||
|
|
||||||
|
详见 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md)。
|
||||||
|
|
||||||
|
### 2. Agent-owned context
|
||||||
|
|
||||||
|
LangBot 不应成为最终 agentic context manager。它应提供事实源、默认上下文引用和按需读取 API;agent 或其背后的 runtime 负责历史剪裁、摘要、召回和 KV cache 策略。
|
||||||
|
|
||||||
|
Host 不定义通用历史窗口字段或策略;runner 通过 Host pull API 按需拉取历史并自行管理 working context。
|
||||||
|
|
||||||
|
详见 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md)。
|
||||||
|
|
||||||
|
### 3. Event Based Agent(Future)
|
||||||
|
|
||||||
|
消息只是事件的一种。后续 `message.received`、`message.recalled`、`group.member_joined`、`friend.request_received` 等事件都应能通过统一事件 envelope 触发 AgentRunner。
|
||||||
|
|
||||||
|
EBA 主线按单 Agent 调度设计:EventRouter 对一个 bot / channel / scope
|
||||||
|
解析出一个有效 AgentBinding,再调用一次 `AgentRunOrchestrator.run(event,
|
||||||
|
binding)`。多 agent fan-out、observer agent 或并行裁决不属于当前目标语义。
|
||||||
|
|
||||||
|
**本分支不实现 EBA 完整能力,只预留:**
|
||||||
|
- event-first envelope (`AgentEventEnvelope`)
|
||||||
|
- AgentBinding model
|
||||||
|
- `run(event, binding)` 入口
|
||||||
|
- QueryEntryAdapter(当前 AgentEventEnvelope / AgentBinding 的 Query entry adapter source)
|
||||||
|
|
||||||
|
详见 [EVENT_BASED_AGENT.md](./EVENT_BASED_AGENT.md)。
|
||||||
|
|
||||||
|
### 4. 官方 runner 插件
|
||||||
|
|
||||||
|
官方 `local-agent` 和外部 runner 迁移是下游工作。它们需要依附 LangBot 提供的宿主能力,但不应反过来决定宿主协议。
|
||||||
|
|
||||||
|
`local-agent` 可以外移,也可以重写。验收重点是它能完整消费 LangBot 的模型、工具、知识库、存储、事件、history API 和 result stream,而不是保留旧内置 runner 的内部结构。
|
||||||
|
|
||||||
|
详见 [OFFICIAL_RUNNER_PLUGINS.md](./OFFICIAL_RUNNER_PLUGINS.md)。
|
||||||
|
|
||||||
|
### 5. Runtime Control Plane v2(Future)
|
||||||
|
|
||||||
|
当前 AgentRunner v1 主线只负责 `event -> binding -> runner.run(ctx) -> result stream`。
|
||||||
|
后续 Agent Platform v2 可以在 Host 侧新增 runtime registry、heartbeat、task queue、daemon claim、progress/cancel 和 runtime audit。
|
||||||
|
|
||||||
|
在这些 Host 能力之上,可以构建独立 agent 管控面插件;插件负责 UI、策略和编排体验,runtime/task 的事实源仍由 Host 持有。
|
||||||
|
|
||||||
|
详见 [RUNTIME_CONTROL_PLANE_V2.md](./RUNTIME_CONTROL_PLANE_V2.md)。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
- 一个插件可以声明多个 `AgentRunner` 组件,每个组件独立暴露 manifest、配置 schema、能力和权限。
|
||||||
|
- 插件本身按单实例、无状态执行单元理解;不同绑定不创建多个插件实例。
|
||||||
|
- Agent / binding 只保存 runner id 和绑定配置,不代表插件实例状态。
|
||||||
|
- bot / IM channel 绑定一个 Agent;Agent 可被多个 bot / channel 复用。
|
||||||
|
- LangBot 可以提供 host-owned state / storage 能力,让 runner 把状态寄宿在 LangBot;但这应该是授权能力,不是强制要求。
|
||||||
|
- 官方 runner 插件是协议消费者,不是协议设计的优先约束。
|
||||||
|
- Pipeline 是当前入口 adapter,不是未来架构中心。
|
||||||
|
- Event dispatch 主线是 one event -> one AgentBinding -> one run_id -> one runner。
|
||||||
|
- EventGateway 是 future integration point,由外部 event branch 提供。
|
||||||
|
- Runtime control plane 是 v2 Host capability layer,不阻塞当前 AgentRunner v1 主线;agent 管控面插件应构建在该 Host 能力层之上。
|
||||||
227
docs/agent-runner-pluginization/RUNTIME_CONTROL_PLANE_V2.md
Normal file
227
docs/agent-runner-pluginization/RUNTIME_CONTROL_PLANE_V2.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
# Agent Runtime Control Plane V2
|
||||||
|
|
||||||
|
本文档记录后续 Agent Platform / runtime 管控面的设计方向。它是当前讨论中的 **v2 文档**,但这里的 v2 指 Host capability layer / runtime control plane,不是 `AgentRunner Protocol v2`,也不属于当前 AgentRunner Protocol v1 插件化主线的交付范围。
|
||||||
|
|
||||||
|
> **future design note**。协议数据结构见 [PROTOCOL_V1.md](./PROTOCOL_V1.md),实现进度见 [PROGRESS.md](./PROGRESS.md)。本文只讲 v2 管控面方向,不重抄 schema。
|
||||||
|
|
||||||
|
## 1. 结论
|
||||||
|
|
||||||
|
当前主线应继续收口 AgentRunner v1:
|
||||||
|
|
||||||
|
```text
|
||||||
|
message/event -> binding -> runner.run(ctx) -> result stream
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime Control Plane v2 在 Host 侧新增 runtime control plane:
|
||||||
|
|
||||||
|
```text
|
||||||
|
event -> task -> runtime selection -> daemon claim -> execute -> progress/audit/result
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Runtime Control Plane v2 之上,可以构建独立的 agent 管控面插件。插件负责 UI、策略和编排体验;runtime、task、heartbeat、audit 的事实源必须属于 LangBot Host,而不是插件私有 storage。
|
||||||
|
|
||||||
|
## 2. 不影响 v1 主线
|
||||||
|
|
||||||
|
v2 不应改变 AgentRunner v1 的基本契约:
|
||||||
|
|
||||||
|
- 现有 `local-agent`、Dify、n8n、Coze 等 runner 仍可按 v1 直接执行。
|
||||||
|
- 当前 Claude Code / Codex MVP runner 可以继续作为本机 subprocess 开发路径。
|
||||||
|
- Host v1 已有的 event-first context、resource authorization、history / event / artifact / state / storage pull APIs 继续保留。
|
||||||
|
- Pipeline 仍只是当前入口 adapter,不参与 v2 runtime 管控面的设计中心。
|
||||||
|
|
||||||
|
v2 只是在 Host 上新增一层可选能力。需要管控面的 runner 或管理插件可以声明使用它;不需要的 runner 不受影响。
|
||||||
|
|
||||||
|
## 3. 当前 Host 能力与缺口
|
||||||
|
|
||||||
|
当前 Host 已经具备 v2 的基础设施底座:
|
||||||
|
|
||||||
|
- `AgentEventEnvelope` / `AgentBinding`
|
||||||
|
- run-scoped resource authorization
|
||||||
|
- EventLog / Transcript / ArtifactStore / PersistentStateStore
|
||||||
|
- History / Event / Artifact / State / Storage pull APIs
|
||||||
|
- AgentRunner result stream 和受控错误回流
|
||||||
|
- Agent/runner config 与 host-owned state
|
||||||
|
|
||||||
|
这些能力足够支持一次 `runner.run(ctx)` 内的安全执行,但不足以承担完整 runtime 管控面。
|
||||||
|
|
||||||
|
v2 还需要 Host 新增:
|
||||||
|
|
||||||
|
- runtime registry:runtime id、所属 workspace、所在机器、provider 能力、状态。
|
||||||
|
- capability discovery:`claude` / `codex` / 其它 CLI 是否存在、版本、登录状态、执行隔离能力。
|
||||||
|
- heartbeat / liveness:runtime 在线、忙闲、最后心跳、可用 slot。
|
||||||
|
- task queue:enqueue、claim、start、progress、complete、fail、cancel。
|
||||||
|
- workspace mapping:LangBot workspace / project 如何映射到 runtime 上的真实目录、仓库或挂载。
|
||||||
|
- secret / env projection:按授权向 runtime 投影 token、代理、MCP 配置、技能和环境变量。
|
||||||
|
- runtime audit:stdout、stderr、事件流、产物、失败原因、执行耗时、使用量。
|
||||||
|
- control API / UI:选择 runtime、测试 runtime、查看状态、下线、取消任务、重试任务。
|
||||||
|
|
||||||
|
## 4. 角色边界
|
||||||
|
|
||||||
|
### 4.1 LangBot Host
|
||||||
|
|
||||||
|
Host 是事实源和控制面内核:
|
||||||
|
|
||||||
|
- 保存 runtime / task / heartbeat / audit 状态。
|
||||||
|
- 做权限校验、资源裁剪、workspace 绑定和审计。
|
||||||
|
- 决定任务是否可被某 runtime claim。
|
||||||
|
- 将执行结果统一回写到 event / transcript / artifact / state。
|
||||||
|
|
||||||
|
Host 不应内置具体 agent CLI 的复杂业务逻辑,也不应把某个官方 runner 的特殊行为提升为通用协议。
|
||||||
|
|
||||||
|
### 4.2 Agent 管控面插件
|
||||||
|
|
||||||
|
管理插件是 v2 control plane 的产品化管理层:
|
||||||
|
|
||||||
|
- 展示 runtime、agent、task、进度、失败、审计。
|
||||||
|
- 提供策略配置,例如默认 runtime、provider 偏好、并发限制、重试策略。
|
||||||
|
- 触发 runtime 测试、任务取消、任务重试、手动分配。
|
||||||
|
|
||||||
|
管理插件不应把 runtime/task 的事实源放进自己的 plugin storage。它应该调用 Host v2 API。
|
||||||
|
|
||||||
|
### 4.3 Runtime daemon / worker
|
||||||
|
|
||||||
|
Runtime daemon 负责真实执行:
|
||||||
|
|
||||||
|
- 在所在机器上检测 CLI 和版本。
|
||||||
|
- 管理工作目录、仓库、挂载、临时文件和进程。
|
||||||
|
- 从 Host claim 任务,执行后上报 progress / complete / fail。
|
||||||
|
- 将 stdout / stderr / artifacts / session id 回流 Host。
|
||||||
|
|
||||||
|
Claude Code、Codex、OpenCode、Gemini CLI 等 provider 适配逻辑应主要落在 daemon / worker 或 provider adapter 中。
|
||||||
|
|
||||||
|
## 5. 部署形态
|
||||||
|
|
||||||
|
### 5.1 uv / local embedded
|
||||||
|
|
||||||
|
用户用 `uv` 或源码直接启动 LangBot 时,LangBot 进程所在机器就是 runtime host。
|
||||||
|
|
||||||
|
这种模式下可以直接检测用户主机上的 `claude`、`codex` 等 CLI,也可以直接 subprocess 执行。它适合个人开发和本地 smoke,但不应作为团队级管控面的唯一形态。
|
||||||
|
|
||||||
|
### 5.2 Docker embedded
|
||||||
|
|
||||||
|
用户用 Docker 启动 LangBot 时,runtime host 是容器,不是宿主机。
|
||||||
|
|
||||||
|
因此:
|
||||||
|
|
||||||
|
- 只能检测容器内的 `claude`、`codex`。
|
||||||
|
- 只能使用容器内的 HOME、PATH、凭据和挂载目录。
|
||||||
|
- 如果镜像未安装 CLI,或未挂载认证文件 / workspace,CLI runner 会不可用。
|
||||||
|
|
||||||
|
Docker embedded 可以作为高级部署选项,但需要用户显式安装 CLI、挂载工作区和凭据。Host 不应假设 Docker 容器能自动访问宿主机 CLI。
|
||||||
|
|
||||||
|
### 5.3 Sidecar daemon
|
||||||
|
|
||||||
|
推荐的 v2 形态是 sidecar daemon:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LangBot Host (Docker or server)
|
||||||
|
<-> Runtime daemon on user host / worker host
|
||||||
|
-> claude / codex / other CLI
|
||||||
|
```
|
||||||
|
|
||||||
|
这种模式下,LangBot 可以跑在 Docker 内,runtime daemon 跑在宿主机或独立 worker 机器上。daemon 负责检测本机 CLI、持有本机凭据和工作区访问能力。
|
||||||
|
|
||||||
|
### 5.4 Remote runtime
|
||||||
|
|
||||||
|
团队场景可以使用远端 runtime:
|
||||||
|
|
||||||
|
- 开发机、构建机、云主机或专用 worker。
|
||||||
|
- 多个 workspace 可绑定不同 runtime。
|
||||||
|
- Host 只通过 registry / task queue / heartbeat / audit 进行管理。
|
||||||
|
|
||||||
|
### 5.5 API-only agent
|
||||||
|
|
||||||
|
Dify、n8n、Coze、DashScope 等 API 型 runner 不依赖本地 CLI。它们可以继续按 v1 直接执行,也可以在未来按需要接入 v2 task/audit。
|
||||||
|
|
||||||
|
## 6. 与 Claude Code / Codex MVP runner 的关系
|
||||||
|
|
||||||
|
当前 Claude Code / Codex runner 是 v1 runner:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runner.run(ctx) -> subprocess("claude" / "codex")
|
||||||
|
```
|
||||||
|
|
||||||
|
它们适合验证 Host context 投影、state resume、result stream 和基础 CLI 调用,但有明确限制:
|
||||||
|
|
||||||
|
- 命令只在 LangBot runtime host 上执行。
|
||||||
|
- Docker 环境只能看到容器内 CLI。
|
||||||
|
- 没有 runtime registry、heartbeat、task queue、cancel、workspace lifecycle。
|
||||||
|
- 不提供发布级执行隔离、secret projection、团队级 audit。
|
||||||
|
|
||||||
|
v2 不需要删除这些 runner。它们可以继续作为 dev / MVP 路径存在。未来若接入管控面,可以增加 runtime-managed 执行模式:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runner binding -> Host task -> runtime daemon -> provider CLI -> Host result
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 最小 v2 API 草案
|
||||||
|
|
||||||
|
以下仅记录能力边界,不代表最终 API 命名。
|
||||||
|
|
||||||
|
Runtime:
|
||||||
|
|
||||||
|
- `runtime.register`
|
||||||
|
- `runtime.heartbeat`
|
||||||
|
- `runtime.list`
|
||||||
|
- `runtime.get`
|
||||||
|
- `runtime.disable`
|
||||||
|
- `runtime.capabilities.report`
|
||||||
|
- `runtime.capabilities.probe`
|
||||||
|
|
||||||
|
Task:
|
||||||
|
|
||||||
|
- `task.enqueue`
|
||||||
|
- `task.claim`
|
||||||
|
- `task.start`
|
||||||
|
- `task.progress`
|
||||||
|
- `task.complete`
|
||||||
|
- `task.fail`
|
||||||
|
- `task.cancel`
|
||||||
|
- `task.retry`
|
||||||
|
|
||||||
|
Workspace:
|
||||||
|
|
||||||
|
- `runtime.workspace.bind`
|
||||||
|
- `runtime.workspace.unbind`
|
||||||
|
- `runtime.workspace.resolve`
|
||||||
|
|
||||||
|
Audit / artifacts:
|
||||||
|
|
||||||
|
- `task.log.append`
|
||||||
|
- `task.artifact.create`
|
||||||
|
- `task.events.page`
|
||||||
|
|
||||||
|
这些 API 应由 Host 提供,并受 workspace、runtime、binding、actor 和 plugin identity 约束。
|
||||||
|
|
||||||
|
## 8. 管控面插件可以构建的能力
|
||||||
|
|
||||||
|
基于 v2 Host 能力,可以实现一个类似 Multica 的 agent 管控面插件:
|
||||||
|
|
||||||
|
- runtime 列表、在线状态、CLI 能力、版本、认证状态。
|
||||||
|
- agent profile 与 runtime/provider 绑定。
|
||||||
|
- 任务看板、任务详情、进度流、失败原因、重试和取消。
|
||||||
|
- workspace 到 runtime 目录 / 仓库的映射管理。
|
||||||
|
- provider capability 测试,例如 Claude Code / Codex 是否可执行。
|
||||||
|
- 审计视图:输入、输出、工具、artifact、stdout/stderr、session id。
|
||||||
|
- 策略配置:并发、队列、默认 runtime、fallback runtime、权限模式。
|
||||||
|
|
||||||
|
该插件应该是 Host v2 的消费者,而不是 Host v2 的替代品。
|
||||||
|
|
||||||
|
## 9. 设计原则
|
||||||
|
|
||||||
|
- v1 先稳定,v2 可选叠加。
|
||||||
|
- Host 保存事实源,插件提供管理体验。
|
||||||
|
- Runtime daemon 执行具体 CLI 和本机资源访问。
|
||||||
|
- Docker 不假设拥有宿主机 CLI;需要 sidecar 或显式挂载。
|
||||||
|
- Pipeline 不进入 v2 控制面中心。
|
||||||
|
- 直接 subprocess runner 可保留,但只作为 local/dev/MVP 路径。
|
||||||
|
- 发布级能力必须经过 Host 权限、审计和资源边界。
|
||||||
|
|
||||||
|
## 10. 待定问题
|
||||||
|
|
||||||
|
- runtime daemon 与 Host 的认证模型:workspace token、device token、还是 scoped PAT。
|
||||||
|
- task 与 AgentRunner binding 的映射关系:由 binding 直接 enqueue,还是由独立 task policy 决定。
|
||||||
|
- runtime capability schema 的稳定字段:provider、version、login status、execution isolation、workspace access、slot。
|
||||||
|
- secret projection 的边界:Host 存储、用户本机存储、或外部 secret manager。
|
||||||
|
- Docker compose 是否提供官方 sidecar daemon 示例。
|
||||||
|
- v2 UI 是核心前端的一部分,还是完全由管理插件提供。
|
||||||
74
docs/agent-runner-pluginization/SECURITY_HARDENING.md
Normal file
74
docs/agent-runner-pluginization/SECURITY_HARDENING.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Agent Runner Security Hardening
|
||||||
|
|
||||||
|
本文档记录 agent-runner 插件化进入生产发布前需要补齐的安全与稳定加固项。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
**当前结论:暂不塞进本阶段 agent-runner plugin 协议闭环。**
|
||||||
|
|
||||||
|
本阶段目标是验证 LangBot 可以通过统一的 `run(event, binding)` 协议接入 `local-agent` 与外部 harness runner(如 Claude Code runner),并能传递事件、上下文、资源句柄、状态和结果流。
|
||||||
|
|
||||||
|
安全发布级 hardening 是后续 release gate,不应阻塞当前协议闭环,但必须作为进入生产默认启用前的验收条件。
|
||||||
|
|
||||||
|
> **硬规则**:能执行代码 / 访问工作目录的外部 harness runner(Claude Code、Codex、Kimi Code 等)在本文 Release Gate Checklist 完成前,**不得在生产环境默认启用**。本地 smoke 通过不等于可生产默认开启。
|
||||||
|
|
||||||
|
## 责任边界
|
||||||
|
|
||||||
|
### LangBot Host 负责
|
||||||
|
|
||||||
|
- 资源授权:决定某个 `run_id` / binding 可以访问哪些模型、RAG、MCP、skill、artifact、history、state。
|
||||||
|
- 资源投影:只把授权后的资源句柄、配置片段或上下文文件传给 runner。
|
||||||
|
- 路径策略:限制 workspace / context file / artifact 的允许路径和清理策略。
|
||||||
|
- Secret 策略:过滤环境变量、配置、日志和 transcript 中的 secret。
|
||||||
|
- 运行约束:配置超时、轮次、并发、配额、输出大小和取消路径。
|
||||||
|
- 审计记录:记录事件、绑定、资源授权、runner 调用、外部 harness session id、关键错误和结果摘要。
|
||||||
|
|
||||||
|
### Runner Plugin 负责
|
||||||
|
|
||||||
|
- 遵守 LangBot 下发的 Agent/runner config、授权资源和运行约束。
|
||||||
|
- 将 LangBot 资源投影成目标 runner 可消费的形式,例如 context 文件、MCP 配置、环境变量或 CLI 参数。
|
||||||
|
- 不把长期状态保存在插件实例内;需要跨轮次保存的外部 session id / working directory 等状态应写入 host-owned state。
|
||||||
|
- 对外部进程做最小必要封装,包括命令参数构造、超时、取消、输出解析和错误映射。
|
||||||
|
|
||||||
|
### 外部 Harness 负责
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 等外部 harness 可以继续使用自身的权限模型、工具 allow / deny 规则、MCP 加载策略、session/resume 机制和沙箱能力。
|
||||||
|
|
||||||
|
但外部 harness 不是 LangBot 的唯一安全边界。LangBot 仍必须在调用前完成资源授权、路径限制、secret 过滤和审计记录。
|
||||||
|
|
||||||
|
## 当前 MVP 可接受边界
|
||||||
|
|
||||||
|
当前阶段可以接受以下前提:
|
||||||
|
|
||||||
|
- 由可信管理员配置 runner binding。
|
||||||
|
- 工作目录和 context 输出目录为显式配置或 host 生成路径。
|
||||||
|
- 外部 runner 默认使用保守权限,例如 plan / no-write 模式或禁用高风险工具。
|
||||||
|
- 通过 timeout、max turns、输出长度和进程取消降低失控风险。
|
||||||
|
- 通过 host-owned state 保存 `external.session_id`、`external.working_directory` 等 resume 所需指针。
|
||||||
|
|
||||||
|
这些前提足够做本地 E2E 与协议验收,不等同于生产发布完成。
|
||||||
|
|
||||||
|
## Release Gate Checklist
|
||||||
|
|
||||||
|
进入生产默认启用前,需要补齐:
|
||||||
|
|
||||||
|
- Path isolation:workspace allowlist、路径规范化、防止 `..` 逃逸、context / artifact 清理。
|
||||||
|
- Permission boundary:runner 能力声明、binding 级资源授权、run 级权限校验。
|
||||||
|
- Secret handling:环境变量白名单、配置脱敏、日志和 transcript redaction。
|
||||||
|
- MCP policy:MCP server allowlist、scoped token、tool allow / deny、危险工具审计。
|
||||||
|
- Skill projection policy:skill 来源验证、只读投影、版本和摘要记录。
|
||||||
|
- Process isolation:进程组管理、取消、超时、CPU / 内存 / 输出配额。
|
||||||
|
- State lifecycle:session id、workspace、artifact 的过期、清理、迁移和审计。
|
||||||
|
- Audit first-class:事件、资源授权、外部命令、session id、结果摘要可追踪。
|
||||||
|
- UI / Admin control:管理员能看到 runner 权限、风险提示、资源绑定和禁用入口。
|
||||||
|
- Test matrix:路径逃逸、secret 泄漏、权限拒绝、timeout、取消、MCP deny、resume、cleanup、audit 完整性。
|
||||||
|
|
||||||
|
## 非当前范围
|
||||||
|
|
||||||
|
以下内容不属于本阶段协议闭环:
|
||||||
|
|
||||||
|
- 完整异步队列与 issue-centric 产品模型。
|
||||||
|
- 复杂 workflow engine。
|
||||||
|
- Codex / Kimi runner 全量接入。
|
||||||
|
- EBA 分支完整迁移和联调。
|
||||||
|
- 发布级安全 hardening 的完整实现。
|
||||||
595
docs/review/box-architecture.md
Normal file
595
docs/review/box-architecture.md
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
# Box 系统架构深度分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> 相关文档: [SaaS 阻塞项](./box-issues.md) | [Session 作用域](./box-session-scope.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 全局架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LangBot 主进程 │
|
||||||
|
│ │
|
||||||
|
│ LocalAgentRunner ──> ToolManager ──> NativeToolLoader │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ exec / read / write / edit │
|
||||||
|
│ │ │ glob / grep │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> MCPLoader ──> BoxStdioSession │
|
||||||
|
│ │ │ (shared 容器, 多 process) │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> SkillToolLoader (activate 工具) │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> SkillAuthoringToolLoader │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ └──> PluginToolLoader │
|
||||||
|
│ │ │
|
||||||
|
│ BoxService (门面) │
|
||||||
|
│ ├─ Profile 管理 (locked 字段) │
|
||||||
|
│ ├─ Host mount 校验 (allowed_mount_roots) │
|
||||||
|
│ ├─ Workspace quota 检查 │
|
||||||
|
│ ├─ 输出截断 (head+tail) │
|
||||||
|
│ ├─ Session ID 模板解析 (resolve_box_session_id) │
|
||||||
|
│ ├─ 技能挂载组装 (build_skill_extra_mounts) │
|
||||||
|
│ ├─ 重连循环 (_reconnect_loop, 指数退避) │
|
||||||
|
│ └─ BoxRuntimeConnector │
|
||||||
|
│ ├─ 心跳 loop (20s ping) │
|
||||||
|
│ └─ ActionRPCBoxClient │
|
||||||
|
│ │ Action RPC (stdio 或 WebSocket) │
|
||||||
|
│ │
|
||||||
|
│ SkillManager (skill_mgr) │
|
||||||
|
│ └─ 从 Box runtime 拉取 skills, 不可用时回落 data/skills │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Box Runtime 进程 (SDK 侧) │
|
||||||
|
│ │
|
||||||
|
│ BoxServerHandler (Action RPC 处理, INIT 配置注入) │
|
||||||
|
│ │ │
|
||||||
|
│ BoxRuntime (session 管理 / 进程生命周期 / TTL reaper) │
|
||||||
|
│ │ └─ session.managed_processes: dict[pid, _ManagedProcess]
|
||||||
|
│ │ │
|
||||||
|
│ Backend (启动时根据 box.backend 配置选择): │
|
||||||
|
│ DockerBackend ──┐ │
|
||||||
|
│ PodmanBackend ──┤── CLISandboxBackend │
|
||||||
|
│ NsjailBackend ──┘ (本地 CLI 或 fallback 到容器内 CLI) │
|
||||||
|
│ E2BBackend (云沙箱, 需要 E2B_API_KEY) │
|
||||||
|
│ │
|
||||||
|
│ BoxSkillStore │
|
||||||
|
│ ├─ list / get / create / update / delete │
|
||||||
|
│ ├─ scan_skill_directory / read_skill_file / write_skill_file │
|
||||||
|
│ └─ preview_skill_zip / install_skill_zip (zip 或 GitHub) │
|
||||||
|
│ │
|
||||||
|
│ aiohttp 单端口服务 (默认 :5410): │
|
||||||
|
│ /rpc/ws — Action RPC │
|
||||||
|
│ /v1/sessions/{id}/managed-process/ws — 默认 process │
|
||||||
|
│ /v1/sessions/{id}/managed-process/{pid}/ws — 指定 process │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 容器 / 沙箱 (Docker/Podman 容器, nsjail sandbox, 或 E2B 远程沙箱) │
|
||||||
|
│ - 隔离文件系统 / 网络 / PID 命名空间 │
|
||||||
|
│ - 资源限制 (CPU, 内存, PID 数, 可选 workspace 配额) │
|
||||||
|
│ - 主挂载 (host_path → mount_path) + 任意条 extra_mounts │
|
||||||
|
│ └─ Skills 通过 extra_mounts 挂在 /workspace/.skills/<name> │
|
||||||
|
│ - exec: 用户命令在此执行 │
|
||||||
|
│ - managed process: 多个长驻进程并存 (MCP Server / 自定义服务) │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心设计原则**:
|
||||||
|
- Box Runtime 作为独立进程运行,通过 Action RPC 与 LangBot 主进程通信,两者复用 SDK 的 IO 层(Handler → Connection → Controller)
|
||||||
|
- 一个 session_id 对应一个容器/沙箱实例。同一 session 内可并存多条 mount 与多个 managed process
|
||||||
|
- Skill / 默认 exec / MCP Server 共享同一个 session 容器(详见 [box-session-scope.md](./box-session-scope.md))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. LangBot 侧模块
|
||||||
|
|
||||||
|
### 2.1 BoxService (`pkg/box/service.py`, 722 行)
|
||||||
|
|
||||||
|
应用层门面,协调 Profile、安全校验、配额、连接、Skill 挂载与 Session 模板:
|
||||||
|
|
||||||
|
主要公开方法(按定义顺序):
|
||||||
|
|
||||||
|
```
|
||||||
|
BoxService
|
||||||
|
├─ initialize() 连接 Box Runtime + 默认 workspace 准备
|
||||||
|
├─ _on_runtime_disconnect(connector) 触发重连
|
||||||
|
├─ _reconnect_loop(connector) 指数退避重连
|
||||||
|
├─ available (property) 连接状态
|
||||||
|
│
|
||||||
|
├─ resolve_box_session_id(query) 从 pipeline 模板解析 session_id
|
||||||
|
├─ build_skill_extra_mounts(query) 组装 pipeline-bound skill 的挂载列表
|
||||||
|
│
|
||||||
|
├─ execute_tool(parameters, query) Agent 调用 exec 时的入口
|
||||||
|
│ ├─ _apply_profile / build_spec
|
||||||
|
│ ├─ _validate_host_mount
|
||||||
|
│ ├─ _enforce_workspace_quota (phase=pre)
|
||||||
|
│ ├─ client.execute(spec)
|
||||||
|
│ ├─ _enforce_workspace_quota (phase=post)
|
||||||
|
│ └─ _truncate (stdout/stderr)
|
||||||
|
│
|
||||||
|
├─ execute_spec_payload(spec_payload, ...) 内部入口(其他 loader 调用)
|
||||||
|
├─ create_session(spec_payload, ...) 显式创建 session
|
||||||
|
├─ start_managed_process(session_id, ...) 启动 managed process
|
||||||
|
├─ get_managed_process(session_id, pid) 查询进程状态(pid 默认 'default')
|
||||||
|
├─ stop_managed_process(session_id, pid) 单独停止某个 managed process
|
||||||
|
├─ get_managed_process_websocket_url(...) 返回 WS attach URL
|
||||||
|
│
|
||||||
|
├─ list_skills() / get_skill(name) Skill 元数据
|
||||||
|
├─ create_skill / update_skill / delete_skill Skill CRUD
|
||||||
|
├─ scan_skill_directory(path) 扫描目录
|
||||||
|
├─ list_skill_files / read_skill_file / write_skill_file
|
||||||
|
├─ preview_skill_zip / install_skill_zip zip / GitHub 安装
|
||||||
|
│
|
||||||
|
├─ shutdown() / dispose() 清理:RPC SHUTDOWN + 进程终止
|
||||||
|
├─ get_status() / get_sessions() / get_recent_errors()
|
||||||
|
└─ get_system_guidance() LLM 系统提示
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profile 系统**: 4 个内置 Profile(`default` / `offline_readonly` / `network_basic` / `network_extended`),`locked` frozenset 字段不可被 LLM 覆盖。参数合并顺序:Profile defaults → LLM 请求参数 → locked 强制值。
|
||||||
|
|
||||||
|
**输出截断**: 默认 4000 字符上限,保留前 60% + 后 40%,中间插入 `[...truncated...]`。
|
||||||
|
|
||||||
|
**Skill 挂载合并**: `execute_tool()` 调用时,`build_skill_extra_mounts(query)` 会把当前 pipeline-bound 的所有 skill 的 `package_root` 作为 `extra_mounts` 加入 BoxSpec,挂在 `/workspace/.skills/<name>`。LLM 通过 `activate` 工具显式激活某个 skill 后,工具调用才允许引用这个 skill 的虚拟路径。
|
||||||
|
|
||||||
|
### 2.2 BoxRuntimeConnector (`pkg/box/connector.py`, 357 行)
|
||||||
|
|
||||||
|
管理与 Box Runtime 的通信连接:
|
||||||
|
|
||||||
|
- **本地 stdio**: Unix/macOS 默认路径,fork `python -m langbot_plugin.cli.__init__ box -s --ws-control-port {port}` 子进程(与 plugin runtime 统一走 `lbp` CLI 入口)
|
||||||
|
- **本地 subprocess + WS**: Windows 本地(asyncio ProactorEventLoop 不支持 stdio pipe)
|
||||||
|
- **远程 WebSocket**: Docker 部署 / `box.runtime.endpoint` 显式配置时,连接 `ws://{host}:{port}/rpc/ws`
|
||||||
|
- **同步等待**: `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接
|
||||||
|
- **心跳**: `_heartbeat_loop()` 每 20s 调用 `ping()`,失败仅 DEBUG 日志(断开检测靠 connection close)
|
||||||
|
- **重连**: `runtime_disconnect_callback` 由 BoxService 提供,触发 `_reconnect_loop`
|
||||||
|
- **INIT 注入**: 连接建立后立即下发当前 `box.*` 配置子树(剔除 `runtime` 私有字段),Runtime 据此初始化 backend
|
||||||
|
|
||||||
|
> **历史改进**: 2026-04-16 版本本文档曾列 P0 「Box 无心跳 / 无重连」,已修复(commit `2dfd9d5d`、`c6882cf`、`5029d9c` 等)。
|
||||||
|
|
||||||
|
### 2.3 BoxWorkspaceSession 工具 (`pkg/box/workspace.py`, 413 行)
|
||||||
|
|
||||||
|
此文件目前提供两类能力:
|
||||||
|
|
||||||
|
1. **路径与命令重写工具函数** — `normalize_host_path` / `rewrite_mounted_path` / `unwrap_venv_path` / `rewrite_venv_command` / `infer_workspace_host_path`,被 MCP loader 与 Skill 路径解析共用。
|
||||||
|
2. **`BoxWorkspaceSession`** — 围绕 BoxService 的轻量包装,专供 MCP-in-Box 场景使用(管理一个共享 session 的 session_id、构建挂载 payload、stage host 文件到共享 workspace)。
|
||||||
|
|
||||||
|
**变化点**: 早期 Skill exec 会为每个 skill 创建独立 BoxWorkspaceSession(独占 session);当前实现已转为 `extra_mounts` 模式,Skill 不再独占容器,只追加挂载。这部分 wrapping 逻辑已从 native loader 移除。
|
||||||
|
|
||||||
|
### 2.4 policy.py (`pkg/box/policy.py`, 98 行) — 仍是死代码
|
||||||
|
|
||||||
|
三层安全策略设计(`SandboxPolicy` / `ToolPolicy` / `ElevatedPolicy`),全项目无任何导入或调用。详见 [SaaS 阻塞项 S2](./box-issues.md)。
|
||||||
|
|
||||||
|
### 2.5 SkillManager (`pkg/skill/manager.py`, 186 行)
|
||||||
|
|
||||||
|
```
|
||||||
|
SkillManager
|
||||||
|
├─ initialize() 调用 reload_skills()
|
||||||
|
├─ reload_skills() 先从 Box runtime list_skills(),
|
||||||
|
│ 不可用则回落 data/skills/ 扫描
|
||||||
|
├─ refresh_skill_from_disk() 单 skill 重新加载
|
||||||
|
├─ get_skill_by_name(name)
|
||||||
|
└─ get_managed_skills_root() 返回 Box 视角的 skills_root 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
skill 元数据通过 `parse_frontmatter` 解析 `SKILL.md` 头部(`name` / `description` / `instructions`),不再做整体扫描的代价(典型 < 50 个)。
|
||||||
|
|
||||||
|
### 2.6 Skill activation (`pkg/skill/activation.py`, 33 行) + Skill loader 辅助
|
||||||
|
|
||||||
|
历史上 skill 通过 LLM 在文本中输出 `[ACTIVATE_SKILL:name]` 标记激活;当前已改为 **Tool Call 机制**:
|
||||||
|
|
||||||
|
- `SkillToolLoader` (`pkg/provider/tools/loaders/skill.py`, 157 行) 暴露 `activate` 工具,参数为 skill 名
|
||||||
|
- 工具实现调用 `register_activated_skill(query, skill_data)`,将激活态写入 `query.variables['_activated_skills']`
|
||||||
|
- 这种 KV-cache-friendly 模式对齐 Claude Code 设计;详见 [box-session-scope.md §4.3](./box-session-scope.md) 的 Tool Call 描述
|
||||||
|
|
||||||
|
`activation.py` 现仅保留对外辅助函数(pipeline 层调用 loader 的 `register_activated_skill`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SDK 侧模块
|
||||||
|
|
||||||
|
### 3.1 BoxRuntime (`box/runtime.py`, 599 行)
|
||||||
|
|
||||||
|
核心编排器,管理 session 生命周期与 backend 调度:
|
||||||
|
|
||||||
|
```
|
||||||
|
Session 生命周期:
|
||||||
|
|
||||||
|
Client EXEC / CREATE_SESSION
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_get_or_create_session(spec)
|
||||||
|
├─ _reap_expired_sessions_locked() 清理 TTL 过期 session
|
||||||
|
├─ 已存在? → _assert_session_compatible() → 复用
|
||||||
|
├─ Backend session 失踪? → 重建 (commit c6882cf)
|
||||||
|
└─ 新建? → backend.start_session(spec) → 创建容器
|
||||||
|
│ └─ 应用 spec.extra_mounts (多挂载)
|
||||||
|
▼
|
||||||
|
execute(spec)
|
||||||
|
├─ 获取 session lock (每 session 独立)
|
||||||
|
├─ backend.exec(session, spec) 在容器中执行命令
|
||||||
|
├─ 更新 last_used_at
|
||||||
|
└─ 超时? → 销毁 session
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Session 保持存活直到:
|
||||||
|
├─ TTL 过期 (默认 300s,下次操作时清理)
|
||||||
|
├─ 执行超时 (自动销毁)
|
||||||
|
├─ 客户端 DELETE_SESSION
|
||||||
|
└─ SHUTDOWN
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键设计**:
|
||||||
|
- 每 session 有独立 `asyncio.Lock`,同一 session 内的命令串行执行
|
||||||
|
- 每 session 维护 `managed_processes: dict[process_id, _ManagedProcess]`,支持多个长驻进程并存(MCP / 自定义)
|
||||||
|
- 全局 `_lock` 保护 `_sessions` dict 的读写
|
||||||
|
- 兼容性检查:比较核心 spec 字段,`image` 字段对不支持自定义镜像的 backend(nsjail/E2B)会跳过
|
||||||
|
|
||||||
|
**Backend 选择 (`_select_backend`)**: 优先级
|
||||||
|
1. 显式 `box.backend` 配置(`docker` / `nsjail` / `e2b`)
|
||||||
|
2. `local` (默认) → Docker / Podman / nsjail CLI 顺序探测
|
||||||
|
3. `get_status` 调用时若当前 backend 不可用,会尝试重新选择 (commit `e5617c7`)
|
||||||
|
|
||||||
|
### 3.2 Backend 系统
|
||||||
|
|
||||||
|
#### CLISandboxBackend (`box/backend.py`, 411 行)
|
||||||
|
|
||||||
|
Docker / Podman 公共基类:
|
||||||
|
|
||||||
|
```
|
||||||
|
start_session(spec):
|
||||||
|
1. validate_sandbox_security(spec)
|
||||||
|
2. docker/podman run -d --rm --name <name>
|
||||||
|
--network none (可选)
|
||||||
|
--cpus/--memory/--pids-limit
|
||||||
|
--read-only + --tmpfs /tmp
|
||||||
|
-v <host>:<mount>:<mode> 主挂载
|
||||||
|
-v <extra.host>:<extra.mount>:.. 额外挂载 (extra_mounts)
|
||||||
|
<image> sh -lc 'while true; do sleep 3600; done'
|
||||||
|
3. 返回 BoxSessionInfo
|
||||||
|
|
||||||
|
exec(session, spec):
|
||||||
|
docker/podman exec -e KEY=VAL <container>
|
||||||
|
sh -lc 'mkdir -p <workdir> && cd <workdir> && <cmd>'
|
||||||
|
|
||||||
|
start_managed_process(session, spec):
|
||||||
|
docker/podman exec -i <container>
|
||||||
|
sh -lc 'mkdir -p <cwd> && cd <cwd> && exec <command> <args>'
|
||||||
|
返回 asyncio.subprocess.Process (stdin/stdout PIPE)
|
||||||
|
```
|
||||||
|
|
||||||
|
容器以 idle 进程启动,实际命令通过 `docker exec` 执行。`--rm` 确保容器退出时自动清理。
|
||||||
|
|
||||||
|
**Windows 支持**: backend 内对 Windows 路径处理与 subprocess 调用做了适配(commit `120817a`)。
|
||||||
|
|
||||||
|
**孤儿清理**: 启动时枚举 `langbot.box=true` 标签的容器,instance_id 不匹配的强制删除。
|
||||||
|
|
||||||
|
#### NsjailBackend (`box/nsjail_backend.py`, 552 行)
|
||||||
|
|
||||||
|
轻量级 Linux 沙箱(无容器引擎依赖):
|
||||||
|
|
||||||
|
- 使用 namespace 隔离(user/mount/pid/ipc/uts/cgroup/net)
|
||||||
|
- 挂载宿主 `/usr`/`/lib`/`/bin`/`/sbin` 只读 + 选定 `/etc` 条目
|
||||||
|
- 每 session 创建独立目录(workspace/tmp/home)
|
||||||
|
- 资源限制: cgroup v2 优先,fallback 到 rlimit
|
||||||
|
- **CLI 兼容**: 通过 `shutil.which(self._nsjail_bin)` 检测系统安装版 nsjail;不存在时再尝试容器内 nsjail(commit `686fcc0`、`feed530`)
|
||||||
|
- **无自定义镜像**: 使用宿主 OS,`image` 字段固定为 `'host'`,兼容性检查跳过 image
|
||||||
|
|
||||||
|
#### E2BBackend (`box/e2b_backend.py`, 429 行)
|
||||||
|
|
||||||
|
云沙箱后端(commit `75b547f` 引入):
|
||||||
|
|
||||||
|
- 通过 `e2b` SDK 与 E2B 平台通信
|
||||||
|
- 配置:`box.e2b.api_key` / `api_url` / `template`
|
||||||
|
- 支持 `extra_mounts`(commit `0fea9b1` 同步上传文件)
|
||||||
|
- 无本地容器引擎依赖,适合无 Docker 的部署或 SaaS 多租户场景
|
||||||
|
- 不支持自定义 image 字段,由 template 控制
|
||||||
|
|
||||||
|
### 3.3 Server (`box/server.py`, 508 行)
|
||||||
|
|
||||||
|
单端口 aiohttp 服务(默认 5410),通过路径区分(commit `8c71ec5` 合并端口):
|
||||||
|
|
||||||
|
1. **Action RPC** (`/rpc/ws`): `BoxServerHandler` 处理所有 action,包括 `INIT` 配置注入、skill store 操作等
|
||||||
|
2. **WS Relay** (`/v1/sessions/{id}/managed-process/ws` 与 `/v1/sessions/{id}/managed-process/{pid}/ws`): 双向桥接 WebSocket ↔ 指定 managed process stdin/stdout
|
||||||
|
|
||||||
|
stdio 模式同样会在 5410 启动 aiohttp,专门承担 managed process attach;Action RPC 走 stdin/stdout。
|
||||||
|
|
||||||
|
### 3.4 Client (`box/client.py`, 377 行)
|
||||||
|
|
||||||
|
`ActionRPCBoxClient` 封装 `Handler.call_action()` 调用:
|
||||||
|
|
||||||
|
- 25+ 方法对应 25+ 个 RPC action(exec / session / managed-process / skill / status / shutdown)
|
||||||
|
- 错误还原: `_translate_action_error()` 通过字符串前缀匹配还原 SDK 侧异常类型
|
||||||
|
- `execute()` timeout = 300s,其他默认 15s
|
||||||
|
- `BoxRuntimeClient` 是 ABC,供后续可能的非 RPC 实现复用
|
||||||
|
|
||||||
|
包级别 `__init__.py` 显式导出:`BoxRuntimeClient`、`ActionRPCBoxClient`(commit `df9c722`)。
|
||||||
|
|
||||||
|
### 3.5 Actions (`box/actions.py`, 34 行)
|
||||||
|
|
||||||
|
`LangBotToBoxAction` 枚举共定义 **25 个** action:
|
||||||
|
|
||||||
|
| 类别 | Actions |
|
||||||
|
|------|---------|
|
||||||
|
| 控制 | `INIT`、`HEALTH`、`STATUS`、`GET_BACKEND_INFO`、`SHUTDOWN` |
|
||||||
|
| 执行 | `EXEC` |
|
||||||
|
| Session | `CREATE_SESSION` / `GET_SESSION` / `GET_SESSIONS` / `DELETE_SESSION` |
|
||||||
|
| Managed Process | `START_MANAGED_PROCESS` / `GET_MANAGED_PROCESS` / `STOP_MANAGED_PROCESS` |
|
||||||
|
| Skill | `LIST_SKILLS` / `GET_SKILL` / `CREATE_SKILL` / `UPDATE_SKILL` / `DELETE_SKILL` / `SCAN_SKILL_DIRECTORY` / `LIST_SKILL_FILES` / `READ_SKILL_FILE` / `WRITE_SKILL_FILE` / `PREVIEW_SKILL_ZIP` / `INSTALL_SKILL_ZIP` |
|
||||||
|
|
||||||
|
### 3.6 Models (`box/models.py`, 331 行)
|
||||||
|
|
||||||
|
核心数据模型:
|
||||||
|
|
||||||
|
| 模型 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `BoxNetworkMode` | `OFF` / `ON` |
|
||||||
|
| `BoxExecutionStatus` | `COMPLETED` / `TIMED_OUT` |
|
||||||
|
| `BoxHostMountMode` | `NONE` / `READ_ONLY` / `READ_WRITE` |
|
||||||
|
| `BoxManagedProcessStatus` | `RUNNING` / `EXITED` |
|
||||||
|
| `BoxMountSpec` | 单条挂载(host_path/mount_path/mode)— **新增** |
|
||||||
|
| `BoxSpec` | 执行请求;新增 `extra_mounts: list[BoxMountSpec]`、`persistent`、`workspace_quota_mb` |
|
||||||
|
| `BoxProfile` | 4 个内置 Profile + `locked` frozenset |
|
||||||
|
| `BoxSessionInfo` | Session 状态(含 backend_name/created_at/last_used_at) |
|
||||||
|
| `BoxManagedProcessSpec` | 长驻进程参数(process_id/command/args/env/cwd) |
|
||||||
|
| `BoxManagedProcessInfo` | 进程状态(status/exit_code/stderr_preview/attached) |
|
||||||
|
| `BoxExecutionResult` | 执行结果(status/exit_code/stdout/stderr/duration_ms) |
|
||||||
|
|
||||||
|
`BoxSpec` 校验器: `workdir` 默认继承 `mount_path`;`host_path` 支持 POSIX 和 Windows 路径;设置 `host_path` 时 `workdir` 必须在 `mount_path` 下。
|
||||||
|
|
||||||
|
### 3.7 BoxSkillStore (`box/skill_store.py`, 647 行)
|
||||||
|
|
||||||
|
新增模块(commit `4ab3502`),把 skill 持久化收归 Box runtime:
|
||||||
|
|
||||||
|
```
|
||||||
|
BoxSkillStore
|
||||||
|
├─ list_skills() / get_skill(name)
|
||||||
|
├─ create_skill(data) / update_skill(name, data) / delete_skill(name)
|
||||||
|
├─ scan_skill_directory(path) 扫描目录返回候选 skill 包列表
|
||||||
|
├─ list_skill_files(name, path) 浏览 skill 内文件树
|
||||||
|
├─ read_skill_file(name, path) / write_skill_file(name, path, content)
|
||||||
|
├─ preview_skill_zip(zip_bytes, ...) 不落盘预览 zip 内容
|
||||||
|
└─ install_skill_zip(zip_bytes, ...) 解压、校验、复制到 skills_root
|
||||||
|
└─ 支持 source_subdir / target_suffix(commit 1aa043f)
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub 安装路径:HTTP 层(`api/http/service/skill.py`)先 `git clone` 拉取,再走 `install_skill_zip` 或 directory 路径。Skill 文件存放于 `box.local.skills_root`(默认 `skills`,相对 `host_root`),容器内对应 `/workspace/.skills/`。
|
||||||
|
|
||||||
|
### 3.8 Security (`box/security.py`, 52 行)
|
||||||
|
|
||||||
|
`validate_sandbox_security()`: 黑名单校验 host_path,阻止挂载 `/etc`/`/proc`/`/sys`/`/dev`/`/root`/`/boot` 及 Docker/Podman socket。
|
||||||
|
|
||||||
|
**已知缺陷**: 根路径 `/` 未拦截,用户 home 目录未拦截,是 denylist 而非 allowlist 策略。详见 [SaaS 阻塞项 S5](./box-issues.md)。
|
||||||
|
|
||||||
|
### 3.9 Errors (`box/errors.py`, 33 行)
|
||||||
|
|
||||||
|
| 异常类型 | 含义 |
|
||||||
|
|----------|------|
|
||||||
|
| `BoxError` | 基类 |
|
||||||
|
| `BoxValidationError` | spec/参数校验失败 |
|
||||||
|
| `BoxBackendUnavailableError` | 无可用 backend |
|
||||||
|
| `BoxRuntimeUnavailableError` | Runtime 服务不可用 |
|
||||||
|
| `BoxSessionConflictError` | session 已存在但 spec 不兼容 |
|
||||||
|
| `BoxSessionNotFoundError` | session 不存在 |
|
||||||
|
| `BoxManagedProcessConflictError` | session 已有同名 process |
|
||||||
|
| `BoxManagedProcessNotFoundError` | process 不存在 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 工具系统集成
|
||||||
|
|
||||||
|
### 4.1 ToolManager 编排 (`toolmgr.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
ToolManager.initialize()
|
||||||
|
├─ NativeToolLoader (exec / read / write / edit / glob / grep)
|
||||||
|
├─ PluginToolLoader (插件工具)
|
||||||
|
├─ MCPLoader (MCP Server 工具)
|
||||||
|
├─ SkillToolLoader (activate 工具 — Tool Call 激活)
|
||||||
|
└─ SkillAuthoringToolLoader (Skill CRUD)
|
||||||
|
|
||||||
|
工具调用优先级: native → plugin → mcp → skill → skill_authoring
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Native Tools (`native.py`, 846 行)
|
||||||
|
|
||||||
|
| 工具 | 是否在 Box 中执行 | 是否访问宿主文件系统 |
|
||||||
|
|------|:---:|:---:|
|
||||||
|
| `exec` | 是 | 否 |
|
||||||
|
| `read` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `write` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `edit` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `glob` | **否** | **是** — 直接遍历宿主目录 |
|
||||||
|
| `grep` | **否** | **是** — 直接读宿主文件 |
|
||||||
|
|
||||||
|
**沙箱边界不对称**: 这是刻意的设计权衡 — `read`/`write`/`edit`/`glob`/`grep` 绕过沙箱以获得性能(避免容器 I/O 开销与跨进程拷贝),但意味着 LLM 可以直接读写 `allowed_mount_roots` 下任何文件。Skill 路径经 `_resolve_host_path()` 重写,禁止穿越 `package_root`。
|
||||||
|
|
||||||
|
**exec 的 Skill 分支**: 命令中引用 `/workspace/.skills/<name>` 的 skill 时:
|
||||||
|
1. 验证 skill 已激活
|
||||||
|
2. 单次 exec 只能引用一个 skill 包
|
||||||
|
3. 若 skill 是 Python 项目(有 `requirements.txt` 或 `pyproject.toml`),命令会被 venv bootstrap 包裹(在 skill 挂载点内创建 `.venv`)
|
||||||
|
4. 调用 `box_service.execute_tool()` → 走默认 session_id 与已组装好的 `extra_mounts`,**不再为每 skill 起独立 session**
|
||||||
|
|
||||||
|
### 4.3 MCP-in-Box (`mcp_stdio.py`, 354 行)
|
||||||
|
|
||||||
|
`BoxStdioSessionRuntime` 让 MCP stdio 服务器在 Box 容器中运行,**共享 session、多 process**模式(commit `529088e`):
|
||||||
|
|
||||||
|
```
|
||||||
|
initialize()
|
||||||
|
1. 复用/创建共享 session (session_id = _build_box_session_id())
|
||||||
|
- persistent=True,长期保持
|
||||||
|
2. workspace.execute_raw(install_cmd) 安装依赖 (可选)
|
||||||
|
3. 将每个 MCP server 文件 stage 到 /workspace/.mcp/<process_id>/
|
||||||
|
4. workspace.start_managed_process(process_id=<server>)
|
||||||
|
5. websocket_client(ws_url) 通过 WS relay 连接
|
||||||
|
6. ClientSession.initialize() MCP 协议握手
|
||||||
|
```
|
||||||
|
|
||||||
|
配置 (`MCPServerBoxConfig`): `network='on'` (MCP 服务器通常需要网络),`host_path_mode='ro'` (默认只读),`startup_timeout_sec=120` (留时间给 pip install)。
|
||||||
|
|
||||||
|
每条 MCP server 是同一 session 中的一个 managed process,独立的 `process_id`、独立 attach URL,互不阻塞。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 启动与生命周期
|
||||||
|
|
||||||
|
### 5.1 启动顺序 (`build_app.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
BuildAppStage.run(ap)
|
||||||
|
├─ ... (persistence, models, sessions) ...
|
||||||
|
│
|
||||||
|
├─ BoxService(ap)
|
||||||
|
├─ box_service.initialize()
|
||||||
|
│ └─ connector.initialize()
|
||||||
|
│ ├─ [stdio] fork box subprocess
|
||||||
|
│ ├─ [subprocess+WS] Windows 本地
|
||||||
|
│ └─ [remote WS] connect URL
|
||||||
|
│ └─ 启动心跳 _heartbeat_task
|
||||||
|
├─ ap.box_service = box_service
|
||||||
|
│
|
||||||
|
├─ ToolManager(ap)
|
||||||
|
├─ tool_mgr.initialize()
|
||||||
|
│ ├─ NativeToolLoader (检查 box_service.available)
|
||||||
|
│ ├─ PluginToolLoader
|
||||||
|
│ ├─ MCPLoader (Box 可用时,stdio MCP 走沙箱)
|
||||||
|
│ └─ SkillAuthoringToolLoader
|
||||||
|
├─ ap.tool_mgr = tool_mgr
|
||||||
|
│
|
||||||
|
├─ ... (platform, pipeline) ...
|
||||||
|
├─ SkillManager.initialize() (从 Box runtime 加载 skill 列表)
|
||||||
|
└─ ... (RAG, HTTP, plugins) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
BoxService 在 ToolManager **之前**初始化。ToolManager 创建 loader 时检查 `box_service.available`。
|
||||||
|
|
||||||
|
### 5.2 初始化失败处理
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
await self._runtime_connector.initialize()
|
||||||
|
self._available = True
|
||||||
|
except Exception as e:
|
||||||
|
self._available = False
|
||||||
|
logger.warning(f"Box runtime unavailable: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**静默降级**: Box 初始化失败不会阻止应用启动,仅导致 6 个 native tool、所有 Skill 工具和 MCP-in-Box 工具不暴露给 LLM。与 Plugin 的行为不同(Plugin 失败会抛异常)。
|
||||||
|
|
||||||
|
### 5.3 销毁流程
|
||||||
|
|
||||||
|
```
|
||||||
|
app.dispose()
|
||||||
|
└─ box_service.dispose()
|
||||||
|
├─ connector.dispose()
|
||||||
|
│ ├─ cancel _heartbeat_task
|
||||||
|
│ ├─ cancel _handler_task / _ctrl_task
|
||||||
|
│ └─ terminate subprocess (SIGTERM)
|
||||||
|
└─ loop.create_task(client.shutdown())
|
||||||
|
└─ RPC SHUTDOWN → Box Runtime 清理所有容器
|
||||||
|
```
|
||||||
|
|
||||||
|
Box 额外做了 RPC SHUTDOWN 通知 Runtime 主动清理容器,比 Plugin 的直接杀进程更安全。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置
|
||||||
|
|
||||||
|
### config.yaml (重构后)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
box:
|
||||||
|
enabled: true # 整个 Box 子系统的总开关。设为 false 时:
|
||||||
|
# - 不连接远程 Box runtime,不 fork 本地 stdio 子进程
|
||||||
|
# - sandbox 工具 (exec/read/write/edit/glob/grep) 不暴露给 LLM
|
||||||
|
# - skill 添加/编辑 / GitHub 安装 / 文件写入全部拒绝
|
||||||
|
# - stdio 模式的 MCP server 启动时报错(http/sse 模式不受影响)
|
||||||
|
# - skill 列表/读取保持只读可用
|
||||||
|
# BOX__ENABLED 环境变量可覆盖(统一约定)
|
||||||
|
backend: 'local' # 'local' (探测) / 'docker' / 'nsjail' / 'e2b'
|
||||||
|
# 由 box.backend / BOX__BACKEND 选择后端
|
||||||
|
runtime:
|
||||||
|
endpoint: '' # 外部 Runtime 的 WS 基地址 'ws://host:5410'
|
||||||
|
# 留空 = 本地自管 Runtime
|
||||||
|
local:
|
||||||
|
profile: 'default'
|
||||||
|
image: '' # 覆盖 profile 默认 image
|
||||||
|
host_root: './data/box' # 工作区挂载根,Docker 部署需绝对路径
|
||||||
|
default_workspace: '' # 默认 '<host_root>/default'
|
||||||
|
skills_root: 'skills' # Box 管理的 skill 包目录(相对 host_root)
|
||||||
|
allowed_mount_roots: # 默认 ['<host_root>']
|
||||||
|
- './data/box'
|
||||||
|
- '/tmp'
|
||||||
|
workspace_quota_mb: null # 配额覆盖,null = 走 profile
|
||||||
|
e2b:
|
||||||
|
api_key: '' # 也可走 E2B_API_KEY 环境变量
|
||||||
|
api_url: '' # 自托管 E2B 时填写
|
||||||
|
template: '' # 默认 template ID
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重大变更**: 较 2026-04-16 文档,配置结构完全重组(commit `eefdea4`)。原字段 `box.profile` / `box.runtime_url` / `box.shared_host_root` / `box.allowed_host_mount_roots` 全部迁入 `box.local.*` 子表,新增 `box.backend` 与 `box.e2b.*` 配置组。
|
||||||
|
|
||||||
|
### docker-compose.yaml
|
||||||
|
|
||||||
|
`langbot_box` 服务受 compose profile 控制,默认 `docker compose up` **不会**启动它。需要 sandbox 时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile box up # 启动 langbot + langbot_box + plugin runtime
|
||||||
|
docker compose --profile all up # 同上
|
||||||
|
docker compose up # 只起 langbot + plugin runtime (box 关闭)
|
||||||
|
```
|
||||||
|
|
||||||
|
若不起 `langbot_box`,需要同步在 `data/config.yaml` 中设 `box.enabled: false`(或 langbot 容器 env 加 `BOX__ENABLED=false`),否则 LangBot 会一直尝试连接不存在的 Box runtime 并报错。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# langbot_box 的关键 volume
|
||||||
|
volumes:
|
||||||
|
- ${LANGBOT_BOX_ROOT}:${LANGBOT_BOX_ROOT} # 工作区挂载(源/目标同路径)
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock # Docker backend 复用宿主 docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关闭/连接失败时的行为矩阵
|
||||||
|
|
||||||
|
`box.enabled = false` 与"启用但连接失败"在用户可观察行为上**完全一致**——都通过 `BoxService.available = False` 表达,只是 `get_status` 多返回 `enabled` 字段供前端区分文案。
|
||||||
|
|
||||||
|
| 消费方 | Box 可用 | Box 不可用(disabled 或 failed) |
|
||||||
|
|---|---|---|
|
||||||
|
| native exec/read/write/edit/glob/grep 工具 | 暴露给 LLM | **不暴露** |
|
||||||
|
| `activate` / `register_skill` 工具 | 暴露给 LLM | **不暴露** |
|
||||||
|
| stdio MCP server | 在 Box 内启动 | **`_init_stdio_python_server` 抛 RuntimeError** 拒绝;不退化到宿主 stdio |
|
||||||
|
| http/sse MCP server | 正常 | 正常(不依赖 Box) |
|
||||||
|
| Skill 列表/读取 (`list_skills`/`get_skill`/`read_skill_file`) | 走 Box runtime | 走 LangBot 本地 `data/skills/` 只读 fallback |
|
||||||
|
| Skill 创建/编辑/安装/写文件 | 走 Box runtime | **HTTP 400** + 明确错误信息(`_require_box_for_write`) |
|
||||||
|
| Pipeline AI 配置中 `box-session-id-template` | 正常生效 | **前端 banner** 提示字段无效 |
|
||||||
|
| Pipeline 扩展页 `enable_all_skills` / 绑定 skill | 可编辑 | **前端禁用** + banner |
|
||||||
|
| 仪表盘 Box 状态卡片 | 绿点 / "已连接" | 灰点 / "已禁用"(disabled) 或 红点 / "已断开"(failed) |
|
||||||
|
|
||||||
|
> 后端拒写的边界条件:如果 `ap.box_service` **完全没装**(老式 dev mode,没经过 BuildAppStage),`_require_box_for_write` 视作 no-op,保留 `data/skills/` 本地路径——以兼容历史测试与最小化设置。生产环境总会装 `ap.box_service`,因此该 fallback 不会被触发。
|
||||||
|
|
||||||
|
### Pipeline 配置 (templates/metadata/pipeline/ai.yaml)
|
||||||
|
|
||||||
|
`local-agent.config.box-session-id-template` 控制 session 作用域,预设:
|
||||||
|
|
||||||
|
- `{launcher_type}_{launcher_id}` — 每个会话 (推荐,默认)
|
||||||
|
- `{launcher_type}_{launcher_id}_{sender_id}` — 群聊每个用户
|
||||||
|
- `{launcher_type}_{launcher_id}_{conversation_id}` — 每个对话上下文
|
||||||
|
- `{query_id}` — 每条消息(完全隔离)
|
||||||
|
|
||||||
|
详见 [box-session-scope.md](./box-session-scope.md)。
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 | 前端 |
|
||||||
|
|------|------|------|:---:|
|
||||||
|
| `/api/v1/box/status` | GET | 可用性、Profile、后端信息 | ✅ 监控页 |
|
||||||
|
| `/api/v1/box/sessions` | GET | 活跃 session 列表 | ❌ |
|
||||||
|
| `/api/v1/box/errors` | GET | 最近 50 条错误 | ❌ |
|
||||||
|
| `/api/v1/skills` 等 | GET/POST/PUT/DELETE | Skill CRUD、文件浏览、zip/GitHub 安装、preview | ✅ Skill 管理页 |
|
||||||
|
|
||||||
|
前端 `web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx` 已接入 `/api/v1/box/status`,展示 backend 名称、profile 与活跃 session 数。Sessions 与 errors API 仍未接入。
|
||||||
76
docs/review/box-issues.md
Normal file
76
docs/review/box-issues.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Box 系统 — SaaS 发布前阻塞项
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> 相关文档: [架构分析](./box-architecture.md) | [Session 作用域](./box-session-scope.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md)
|
||||||
|
|
||||||
|
## 范围说明
|
||||||
|
|
||||||
|
**自部署社区版已具备发布条件**:默认 stdio 模式、box 为可选项;box 关闭 / 不可用时后端、前端、工具、skill、stdio-MCP 均能干净降级(清晰报错、不崩溃);配置向后兼容(旧 `data/config.yaml` 可直接启动);无新增 ORM 模型、无迁移欠债;市场安装失败不会破坏实例。CI 全绿。
|
||||||
|
|
||||||
|
本清单**只保留发布 SaaS / 多租户 / 公网暴露前必须处理的阻塞项**。社区版(可信、单运营者、内网)不受这些项阻塞——它们的风险面在"不可信调用方能直接触达 Box 控制面"或"多租户共享资源"的场景才成立。
|
||||||
|
|
||||||
|
## 已解决(社区版发布前)
|
||||||
|
|
||||||
|
| 项 | 处理 |
|
||||||
|
|----|------|
|
||||||
|
| 工具调用循环无上限 (原 #13) | `localagent.py` 增加 `MAX_TOOL_CALL_ROUNDS=128`,超限优雅终止(`cafef1a3`) |
|
||||||
|
| 配额校验同步遍历阻塞事件循环 (原 #10) | `_enforce_workspace_quota` 改 async,工作区遍历走 `asyncio.to_thread`(`cafef1a3`) |
|
||||||
|
| `host_path` 挂载白名单 (原 #3 的 LangBot 侧) | `pkg/box/service.py` `allowed_mount_roots` 白名单,空列表时拒绝一切宿主挂载 |
|
||||||
|
| 重复的 `_is_path_under` (原 #12) | 已去重,仅保留一处定义 |
|
||||||
|
| 重连 / 心跳 / Windows 兼容 / nsjail image 字段 / 前端 Box 状态接入 | 见上一轮 review 记录,均已合入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SaaS 阻塞项
|
||||||
|
|
||||||
|
### S1. Box 控制面无认证 — Critical
|
||||||
|
|
||||||
|
- **位置**: SDK `box/server.py` — Action RPC WS (`/rpc/ws`) 与 managed-process relay (`/v1/sessions/{id}/managed-process/{pid}/ws`)
|
||||||
|
- **现状**: 两个 WS handler 在 `ws.prepare` 后直接服务,无任何 token / 鉴权;box 默认绑定 `0.0.0.0:5410`。任何能触达该端口者可发起 `EXEC`、创建 session、attach 任意 session 的 managed-process stdin/stdout、甚至 `SHUTDOWN`。LangBot→box 的 INIT 也未下发任何凭证。
|
||||||
|
- **缓解现状**: 默认 `docker-compose.yaml` 的 `langbot_box` 未把 5410 发布到宿主(爆炸半径限于内网 bridge);但 box 挂载了 `/var/run/docker.sock`,同网络的任意服务(含被攻破的插件)→ 宿主 root。若运营者把 5410 发布到宿主或独立以 `0.0.0.0` 起 box,则完全裸奔。
|
||||||
|
- **要求**: INIT 时下发 token,两个 WS 路由按连接校验(query/header)。这是 SaaS 的**头号**阻塞项。
|
||||||
|
|
||||||
|
### S2. 无 exec 授权模型(policy.py 死代码) — High
|
||||||
|
|
||||||
|
- **位置**: LangBot `pkg/box/policy.py`(`SandboxPolicy` / `ToolPolicy` / `ElevatedPolicy` 全项目无引用);`pkg/provider/tools/loaders/native.py`;`pkg/provider/tools/toolmgr.py`
|
||||||
|
- **现状**: 原生工具(`exec/read/write/edit/glob/grep`)按"box 是否可用"全有或全无地暴露,**无 per-pipeline 的 exec 网关 / 工具白名单 / 沙箱模式 / 权限提升控制**。只要 box 可用,任何使用 local-agent + 函数调用模型的 pipeline 都能跑任意 shell。
|
||||||
|
- **要求**: 接入 policy.py(或等价机制),按 pipeline 控制是否暴露 `exec`、可用工具白名单、沙箱网络/只读模式。
|
||||||
|
|
||||||
|
### S3. 会话资源无界(DoS) — High
|
||||||
|
|
||||||
|
- **#5 session 数量无上限**: SDK `box/runtime.py` `_get_or_create_session` 的 `_sessions` dict 无容量限制——可变 `session_id` 的恶意调用可无限创建容器,耗尽宿主 CPU/内存/PID/磁盘。
|
||||||
|
- **#8 无定时回收**: 过期 session 仅在 `_get_or_create_session` 时机会性清理,无独立周期任务;一波创建后转静默会永久泄漏容器。
|
||||||
|
- **要求**: `max_sessions` 上限(拒绝或 LRU),加独立周期 reaper(如 60s)。
|
||||||
|
|
||||||
|
### S4. 工作区配额无内核级限制(TOCTOU) — Med-High
|
||||||
|
|
||||||
|
- **位置**: LangBot `pkg/box/service.py` `_enforce_workspace_quota`(应用层 read-then-check);SDK 侧 `workspace_quota_mb` 仅记录/透传,无 `--storage-opt size=` 等内核/FS 限额
|
||||||
|
- **现状**: 执行前后两次检查之间存在竞态窗口;单条命令(`dd`/`fallocate`)可在检查间隙撑爆磁盘,事后检查只能补救。
|
||||||
|
- **要求**: Docker `--storage-opt size=` 做内核级限制,或 Redis 原子计数预留式配额。
|
||||||
|
|
||||||
|
### S5. 挂载校验缺口 — Med-High
|
||||||
|
|
||||||
|
- **位置**: SDK `box/security.py` `_BLOCKED_HOST_PATHS_POSIX`;`box/backend.py` 的 `extra_mounts` 处理
|
||||||
|
- **现状**: ① SDK 黑名单仍不含 `/`(前缀匹配,`host_path="/"` 可通过,挂载整个宿主 fs);用户 home、`/usr`、`/opt`、`/tmp` 也未拦截。② `validate_sandbox_security` 只校验 `spec.host_path`,**从不遍历 `spec.extra_mounts`**——LangBot 侧 `allowed_mount_roots` 也只校验 `host_path`。当前 `extra_mounts` 仅由 `build_skill_extra_mounts` 内部填充(agent 不可达),但缺乏纵深防御:一旦 S1 的无认证 RPC 被触达,extra_mounts 可挂任意宿主路径,两层都不拦。
|
||||||
|
- **要求**: SDK 黑名单加入 `/`(或改白名单);`extra_mounts` 在 SDK 与 LangBot 两侧都纳入挂载校验。
|
||||||
|
|
||||||
|
### S6. 容器加固缺失 — Med
|
||||||
|
|
||||||
|
- **位置**: SDK `box/backend.py` 的 `docker run` 组装
|
||||||
|
- **现状**: 未设置 `--cap-drop=ALL`、`--security-opt=no-new-privileges`、非 root `--user`;叠加挂载 docker.sock,逃逸面偏大。
|
||||||
|
- **要求**: 默认加上上述加固 flag(需回归常用 skill 不被破坏)。
|
||||||
|
|
||||||
|
### S7. 全局锁内执行慢操作(扩展性) — Med
|
||||||
|
|
||||||
|
- **位置**: SDK `box/runtime.py` `_get_or_create_session`:`self._lock` 持有期间调用 `backend.start_session()`(`docker run` / nsjail 启动 / E2B `Sandbox.create`)
|
||||||
|
- **影响**: 冷启动(镜像拉取数秒、E2B >1s)期间串行阻塞所有并发请求——多租户负载下整个 Box runtime 停顿。降级表现是延迟而非失败。
|
||||||
|
- **要求**: 锁内只做状态检查与注册,容器创建移到锁外。
|
||||||
|
|
||||||
|
### S8. 其他硬化 / 跟进 — Low
|
||||||
|
|
||||||
|
- **#9** SDK `box/server.py` 直接读 `runtime._sessions` 私有字段、绕过锁,并发下可能读到不一致状态——应加公共访问方法。
|
||||||
|
- **#16** `pkg/provider/tools/toolmgr.py` `execute_func_call` 按优先级分发,plugin/MCP 若有同名 `exec/read/write/...` 工具会被静默遮蔽——应加命名空间或冲突告警。
|
||||||
|
- **#4** SDK `box/runtime.py` INIT/handshake 与 backend 实例化的残留竞态(仅"纯远程 WS box 先启动、LangBot 后连"场景成立;stdio/compose 路径下 config 经 env 在 spawn 时已就位,无竞态)——应在 INIT 完成前拒绝业务 action。
|
||||||
|
- **#11** `extra_mounts` 在容器创建时固定(SDK `runtime.py` 兼容性检查不含 extra_mounts);长生命周期共享 session 后续新激活的 skill 不会挂上(当前缓解:创建时挂上 pipeline 绑定的全部 skill)——动态绑定场景需销毁重建或文档说明。
|
||||||
|
- **#21** 集成测试未进 CI:容器实际执行、E2B 真机、managed-process WS attach 仅本地可跑。安全关键路径缺自动化覆盖——SaaS 前建议加 Docker-in-Docker CI stage 或合并前手动 checklist。
|
||||||
402
docs/review/box-session-scope.md
Normal file
402
docs/review/box-session-scope.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# Box Session Scope Design
|
||||||
|
|
||||||
|
> Date: 2026-04-18 (last reviewed 2026-06-02)
|
||||||
|
> Status (2026-06-02): the self-hosted community edition is release-ready (box optional, clean degradation, no migration debt). Tool-call loop cap, async quota scan, and the host_path mount allowlist have landed. Remaining multi-tenant / security hardening is tracked in [box-issues.md](./box-issues.md).
|
||||||
|
> Branch: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> Related: [Box Architecture](./box-architecture.md) | [Box vs Plugin Runtime](./box-vs-plugin-runtime.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Implementation Status (2026-05-19)
|
||||||
|
|
||||||
|
This document was authored as a design proposal. The current `feat/sandbox` branch
|
||||||
|
has shipped the design largely as written:
|
||||||
|
|
||||||
|
| Item | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `BoxMountSpec` + `BoxSpec.extra_mounts` | ✅ Shipped | SDK `box/models.py` |
|
||||||
|
| Docker / nsjail / E2B backends apply extra mounts | ✅ Shipped | Last gap closed by SDK commit `0fea9b1` (E2B) |
|
||||||
|
| `box-session-id-template` in `local-agent` pipeline config | ✅ Shipped | `templates/metadata/pipeline/ai.yaml`, default `{launcher_type}_{launcher_id}` |
|
||||||
|
| `BoxService.resolve_box_session_id(query)` | ✅ Shipped | `pkg/box/service.py:166` |
|
||||||
|
| `BoxService.build_skill_extra_mounts(query)` | ✅ Shipped | `pkg/box/service.py:189` |
|
||||||
|
| Skill exec uses unified container + extra mounts | ✅ Shipped | `pkg/provider/tools/loaders/native.py` skill branch |
|
||||||
|
| MCP-in-Box uses shared persistent session, multi-process | ✅ Shipped (earlier than originally scoped) | SDK commit `529088e`, LangBot `mcp_stdio.py:_build_box_session_id` |
|
||||||
|
| `BoxManagedProcessSpec.process_id` + multi-process per session | ✅ Shipped | `BoxRuntime` keeps `managed_processes: dict[pid, _ManagedProcess]` |
|
||||||
|
| Per-tenant / quota integration with templates | ❌ Not started | See [box-tob-analysis.md](./box-tob-analysis.md) |
|
||||||
|
|
||||||
|
The "Phase 2 deferred" note in §10 is **out of date** — MCP unification went in on
|
||||||
|
the same line. Pipeline-scoped (not user-scoped) MCP container is the realized
|
||||||
|
behavior: each pipeline's MCP servers share one `mcp-<pipeline>` session, and
|
||||||
|
user exec sessions use the template-derived id.
|
||||||
|
|
||||||
|
The remaining open work is multi-tenant overlays (tenant_id in session_id,
|
||||||
|
quota counters keyed by tenant), tracked in the toB analysis doc rather than here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problems
|
||||||
|
|
||||||
|
### 1.1 Default exec: per-message containers
|
||||||
|
|
||||||
|
Currently, `BoxService.execute_tool()` sets `session_id = str(query.query_id)` — an
|
||||||
|
auto-incrementing integer per incoming message. Every user message creates a new sandbox
|
||||||
|
container. Dependencies installed and in-container state are lost between messages.
|
||||||
|
|
||||||
|
### 1.2 Three isolated container pools
|
||||||
|
|
||||||
|
Default exec, skills, and MCP servers each manage their own containers with
|
||||||
|
independent session IDs:
|
||||||
|
|
||||||
|
| Path | Session ID | Container |
|
||||||
|
|--------------|-----------------------------------------------|-------------|
|
||||||
|
| Default exec | `str(query_id)` (per message) | Ephemeral |
|
||||||
|
| Skill exec | `skill-{launcher}_{id}-{skill_name}` | Per skill |
|
||||||
|
| MCP stdio | `mcp-{server_uuid}` | Per server |
|
||||||
|
|
||||||
|
This means a single logical user interaction can spawn 3+ containers that cannot
|
||||||
|
share state, see each other's files, or reuse installed dependencies.
|
||||||
|
|
||||||
|
### 1.3 Single bind mount limitation
|
||||||
|
|
||||||
|
`BoxSpec` currently supports only **one** `host_path` → `mount_path` bind mount.
|
||||||
|
This prevents mounting both a default workspace and skill directories into the
|
||||||
|
same container.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Concept Model
|
||||||
|
|
||||||
|
```
|
||||||
|
Platform Message
|
||||||
|
→ Query (query_id: int, auto-increment, per message)
|
||||||
|
→ Session (launcher_type + launcher_id, per chat window)
|
||||||
|
→ Conversation (uuid, per dialogue context within a Session)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Concept | Key | Example | Scope |
|
||||||
|
|---------------|-------------------------------------|----------------------------|------------------------------|
|
||||||
|
| Query | `query_id` | `42` | Single message |
|
||||||
|
| Session | `launcher_type` + `launcher_id` | `group_123456` | Chat window (group or PM) |
|
||||||
|
| Conversation | `conversation_id` (UUID) | `a1b2c3d4-...` | Dialogue context within a Session |
|
||||||
|
| Sender | `sender_id` | `789` | Individual user |
|
||||||
|
|
||||||
|
Note: in a **group chat**, all users share the same Session (keyed by `group_id`). The
|
||||||
|
individual sender is tracked as `sender_id` but does not affect Session/Conversation routing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Target Scenarios
|
||||||
|
|
||||||
|
| # | Scenario | Box Granularity | Desired `session_id` |
|
||||||
|
|----|--------------------------------|------------------------------------------|---------------------------------------------------------|
|
||||||
|
| 1 | Personal assistant | 1 Box per user, long-lived | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 2 | Customer service | 1 Box per customer, cross-pipeline | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 3 | Internal employee tool | 1 Box per employee | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 4 | Group chat shared assistant | 1 Box per group | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 5 | Group chat isolated per user | 1 Box per user within a group | `{launcher_type}_{launcher_id}_{sender_id}` |
|
||||||
|
| 6 | Teaching (cross-channel) | 1 Box per student across groups/PMs | `{sender_id}` |
|
||||||
|
| 7 | One-off execution | 1 Box per message (current behavior) | `{query_id}` |
|
||||||
|
| 8 | Multi-project development | 1 Box per conversation context | `{launcher_type}_{launcher_id}_{conversation_id}` |
|
||||||
|
|
||||||
|
No single fixed granularity covers all scenarios. A template-based approach is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Design Overview
|
||||||
|
|
||||||
|
Two key changes:
|
||||||
|
|
||||||
|
1. **Unified container**: exec, skills, and MCP all share the same container per
|
||||||
|
session scope. No more separate container pools.
|
||||||
|
2. **Configurable session scope**: `session_id` is generated from a template with
|
||||||
|
pipeline variables, configurable per pipeline.
|
||||||
|
|
||||||
|
### 4.1 Unified Container with Multiple Mounts
|
||||||
|
|
||||||
|
A single container per session scope is created on first use. It has:
|
||||||
|
|
||||||
|
- **Primary mount**: default workspace at `/workspace` (from `default_host_workspace`)
|
||||||
|
- **Skill mounts**: each pipeline-bound skill's `package_root` mounted at
|
||||||
|
`/workspace/.skills/{skill_name}/`
|
||||||
|
- **MCP servers**: run as managed processes inside the same container
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id = "group_123456")
|
||||||
|
/workspace/ ← default workspace (bind mount, rw)
|
||||||
|
/workspace/.skills/web-search/ ← skill package (bind mount, rw)
|
||||||
|
/workspace/.skills/data-analysis/ ← skill package (bind mount, rw)
|
||||||
|
[managed process: mcp-server-a] ← MCP server running inside
|
||||||
|
[managed process: mcp-server-b] ← MCP server running inside
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires extending `BoxSpec` to support multiple mounts (see §5).
|
||||||
|
|
||||||
|
### 4.2 Session ID Template
|
||||||
|
|
||||||
|
A new field `box-session-id-template` in the `local-agent` pipeline runner config
|
||||||
|
controls the session scope:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# templates/metadata/pipeline/ai.yaml (under local-agent.config)
|
||||||
|
- name: box-session-id-template
|
||||||
|
label:
|
||||||
|
en_US: Sandbox Scope
|
||||||
|
zh_Hans: 沙箱作用域
|
||||||
|
description:
|
||||||
|
en_US: >-
|
||||||
|
Determines how sandbox environments are shared. Use variables to
|
||||||
|
control isolation granularity.
|
||||||
|
zh_Hans: >-
|
||||||
|
决定沙箱环境的共享方式。使用变量控制隔离粒度。
|
||||||
|
type: select
|
||||||
|
required: false
|
||||||
|
default: "{launcher_type}_{launcher_id}"
|
||||||
|
options:
|
||||||
|
- value: "{launcher_type}_{launcher_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per chat (Recommended)
|
||||||
|
zh_Hans: 每个会话(推荐)
|
||||||
|
- value: "{launcher_type}_{launcher_id}_{sender_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per user in chat
|
||||||
|
zh_Hans: 会话中每个用户
|
||||||
|
- value: "{launcher_type}_{launcher_id}_{conversation_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per conversation context
|
||||||
|
zh_Hans: 每个对话上下文
|
||||||
|
- value: "{query_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per message (isolated)
|
||||||
|
zh_Hans: 每条消息(完全隔离)
|
||||||
|
```
|
||||||
|
|
||||||
|
Available template variables (populated by PreProcessor in `query.variables`):
|
||||||
|
|
||||||
|
| Variable | Source | Example |
|
||||||
|
|---------------------|---------------------------------|----------------------|
|
||||||
|
| `{launcher_type}` | `query.session.launcher_type` | `person` / `group` |
|
||||||
|
| `{launcher_id}` | `query.session.launcher_id` | `123456` |
|
||||||
|
| `{sender_id}` | `query.sender_id` | `789` |
|
||||||
|
| `{conversation_id}` | `conversation.uuid` | `a1b2c3d4-...` |
|
||||||
|
| `{query_id}` | `query.query_id` | `42` |
|
||||||
|
|
||||||
|
Default `{launcher_type}_{launcher_id}` covers scenarios 1–4 out of the box.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. SDK Changes: Multi-Mount BoxSpec
|
||||||
|
|
||||||
|
### 5.1 Model Extension
|
||||||
|
|
||||||
|
```python
|
||||||
|
# box/models.py
|
||||||
|
|
||||||
|
class BoxMountSpec(pydantic.BaseModel):
|
||||||
|
"""A single bind mount specification."""
|
||||||
|
host_path: str
|
||||||
|
mount_path: str
|
||||||
|
mode: BoxHostMountMode = BoxHostMountMode.READ_WRITE
|
||||||
|
|
||||||
|
class BoxSpec(pydantic.BaseModel):
|
||||||
|
# ... existing fields ...
|
||||||
|
host_path: str | None = None # Primary mount (backward compat)
|
||||||
|
host_path_mode: BoxHostMountMode = BoxHostMountMode.READ_WRITE
|
||||||
|
mount_path: str = DEFAULT_BOX_MOUNT_PATH
|
||||||
|
extra_mounts: list[BoxMountSpec] = [] # NEW: additional mounts
|
||||||
|
```
|
||||||
|
|
||||||
|
`extra_mounts` is additive — the existing `host_path` / `mount_path` pair remains
|
||||||
|
the primary mount for backward compatibility.
|
||||||
|
|
||||||
|
### 5.2 Backend: Apply Extra Mounts
|
||||||
|
|
||||||
|
```python
|
||||||
|
# box/backend.py — CLISandboxBackend.start_session()
|
||||||
|
|
||||||
|
# Primary mount (unchanged)
|
||||||
|
if spec.host_path is not None and spec.host_path_mode != BoxHostMountMode.NONE:
|
||||||
|
args.extend(['-v', f'{spec.host_path}:{spec.mount_path}:{spec.host_path_mode.value}'])
|
||||||
|
|
||||||
|
# Extra mounts (NEW)
|
||||||
|
for mount in spec.extra_mounts:
|
||||||
|
if mount.mode != BoxHostMountMode.NONE:
|
||||||
|
args.extend(['-v', f'{mount.host_path}:{mount.mount_path}:{mount.mode.value}'])
|
||||||
|
```
|
||||||
|
|
||||||
|
Same pattern for nsjail backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. LangBot Changes
|
||||||
|
|
||||||
|
### 6.1 Session ID Resolution
|
||||||
|
|
||||||
|
In `BoxService.execute_tool()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before:
|
||||||
|
spec_payload.setdefault('session_id', str(query.query_id))
|
||||||
|
|
||||||
|
# After:
|
||||||
|
template = (query.pipeline_config or {}).get('ai', {}) \
|
||||||
|
.get('local-agent', {}).get('box-session-id-template',
|
||||||
|
'{launcher_type}_{launcher_id}')
|
||||||
|
variables = query.variables or {}
|
||||||
|
session_id = template.format_map(collections.defaultdict(
|
||||||
|
lambda: 'unknown', variables
|
||||||
|
))
|
||||||
|
spec_payload.setdefault('session_id', session_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Skill Exec: Use Same Container
|
||||||
|
|
||||||
|
Currently `native.py:_invoke_exec` creates a separate `BoxWorkspaceSession` per
|
||||||
|
skill with `host_path=package_root`. Instead:
|
||||||
|
|
||||||
|
1. Use the **same session_id** as default exec (from the template).
|
||||||
|
2. Pass the skill's `package_root` as an **extra mount** at
|
||||||
|
`/workspace/.skills/{skill_name}/` instead of replacing `/workspace`.
|
||||||
|
3. The container already has the default workspace at `/workspace`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# native.py — _invoke_exec, skill branch (REVISED)
|
||||||
|
|
||||||
|
# Same session_id as default exec
|
||||||
|
session_id = resolve_box_session_id(query)
|
||||||
|
|
||||||
|
spec_payload = {
|
||||||
|
'cmd': rewritten_command,
|
||||||
|
'workdir': rewritten_workdir,
|
||||||
|
'session_id': session_id,
|
||||||
|
'extra_mounts': [{
|
||||||
|
'host_path': package_root,
|
||||||
|
'mount_path': f'/workspace/.skills/{selected_skill_name}',
|
||||||
|
'mode': 'rw',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
result = await self.ap.box_service.execute_spec_payload(spec_payload, query)
|
||||||
|
```
|
||||||
|
|
||||||
|
The virtual path `/workspace/.skills/{name}` no longer needs rewriting at the
|
||||||
|
command level — it maps directly to the bind mount path inside the container.
|
||||||
|
|
||||||
|
### 6.3 MCP: Use Same Container
|
||||||
|
|
||||||
|
MCP servers should run inside the same container as exec and skills. Changes:
|
||||||
|
|
||||||
|
1. `BoxStdioSessionRuntime` uses the pipeline's session_id template instead of
|
||||||
|
`mcp-{server_uuid}`.
|
||||||
|
2. MCP server's working directory is a subdirectory (e.g. `/workspace/.mcp/{name}/`).
|
||||||
|
3. MCP server's dependencies are mounted or installed into that subdirectory.
|
||||||
|
4. The MCP server runs as a managed process inside the shared container.
|
||||||
|
|
||||||
|
Since MCP servers start at LangBot boot (not per-query), the session must be
|
||||||
|
created eagerly. The container will be kept alive by the managed process
|
||||||
|
exemption in TTL reaping (`runtime.py:259`).
|
||||||
|
|
||||||
|
**Note**: MCP sessions are pipeline-scoped (not per-launcher), so their session_id
|
||||||
|
should be a **fixed identifier per pipeline** rather than the user-facing template.
|
||||||
|
This means one shared MCP container per pipeline, with user exec sessions separate.
|
||||||
|
|
||||||
|
Alternatively, in a future iteration, MCP managed processes could be launched
|
||||||
|
lazily into the user's container on first MCP tool call. This is more complex
|
||||||
|
but maximizes sharing. For V1, keeping MCP containers at pipeline scope is
|
||||||
|
simpler and more predictable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Mount Layout Summary
|
||||||
|
|
||||||
|
### Default exec (no skills activated)
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id from template)
|
||||||
|
/workspace/ ← default_host_workspace (rw)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exec with activated skills
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (same session_id)
|
||||||
|
/workspace/ ← default_host_workspace (rw)
|
||||||
|
/workspace/.skills/web-search/ ← skill package_root (rw)
|
||||||
|
/workspace/.skills/data-analysis/ ← skill package_root (rw)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extra mounts are **additive** — they are added when the container is first
|
||||||
|
created (or on the first exec that references a skill). Since Docker bind
|
||||||
|
mounts are specified at container creation time, skills must be known at
|
||||||
|
creation time.
|
||||||
|
|
||||||
|
**Resolution**: When creating a container, inject `extra_mounts` for **all
|
||||||
|
pipeline-bound skills** (from `extensions_preferences`), not just the
|
||||||
|
currently activated one. This way any skill can be activated later without
|
||||||
|
recreating the container.
|
||||||
|
|
||||||
|
### MCP servers (V1: pipeline-scoped)
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id = "mcp-pipeline-{pipeline_uuid}")
|
||||||
|
/workspace/ ← MCP shared workspace
|
||||||
|
/workspace/.mcp/server-a/ ← MCP server A files
|
||||||
|
/workspace/.mcp/server-b/ ← MCP server B files
|
||||||
|
[managed process: server-a]
|
||||||
|
[managed process: server-b]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Data Migration
|
||||||
|
|
||||||
|
Existing pipelines do not have `box-session-id-template`. The backend uses
|
||||||
|
`.get(..., default)` so missing keys fall back to `{launcher_type}_{launcher_id}`.
|
||||||
|
This changes behavior from per-message to per-launcher for existing pipelines.
|
||||||
|
|
||||||
|
Recommendation: **accept the behavior change** — per-launcher is the more
|
||||||
|
intuitive default, and the old per-message behavior was rarely desired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Cloud Quota Implications
|
||||||
|
|
||||||
|
| Scope | Typical concurrent containers |
|
||||||
|
|-----------------------------------------------|-------------------------------|
|
||||||
|
| `{query_id}` (per message) | Many, short-lived |
|
||||||
|
| `{launcher_type}_{launcher_id}` (per chat) | = active chat count |
|
||||||
|
| `{sender_id}` (per user) | = active user count |
|
||||||
|
| `{conversation_id}` (per conversation) | Between per-chat and per-msg |
|
||||||
|
|
||||||
|
With the unified container model, each scope value maps to exactly **one**
|
||||||
|
container (instead of potentially 3+ per-message). This significantly reduces
|
||||||
|
resource usage.
|
||||||
|
|
||||||
|
Quota enforcement point: `BoxRuntime._get_or_create_session()` in the SDK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Session scope + skill unification (this PR)
|
||||||
|
|
||||||
|
1. **SDK**: Extend `BoxSpec` with `extra_mounts: list[BoxMountSpec]`.
|
||||||
|
2. **SDK**: Update Docker/nsjail backends to apply extra mounts.
|
||||||
|
3. **LangBot**: Add `box-session-id-template` to `local-agent` YAML metadata
|
||||||
|
and default pipeline config JSON.
|
||||||
|
4. **LangBot**: Update `BoxService.execute_tool()` to use template interpolation.
|
||||||
|
5. **LangBot**: Update `native.py:_invoke_exec` skill branch to use same
|
||||||
|
session_id + extra mounts instead of separate `BoxWorkspaceSession`.
|
||||||
|
6. **LangBot**: On container creation, inject extra mounts for all
|
||||||
|
pipeline-bound skills.
|
||||||
|
7. **Frontend**: No code change — `DynamicFormComponent` renders `select` fields.
|
||||||
|
8. **Tests**: Unit tests for template interpolation and multi-mount specs.
|
||||||
|
|
||||||
|
### Phase 2: MCP unification (future)
|
||||||
|
|
||||||
|
1. Refactor `BoxStdioSessionRuntime` to use pipeline-scoped shared container.
|
||||||
|
2. MCP servers become managed processes in the shared container.
|
||||||
|
3. Support multiple concurrent managed processes per container.
|
||||||
|
|
||||||
|
MCP unification is deferred because it requires changes to the managed process
|
||||||
|
model (currently 1 managed process per session) and has startup ordering
|
||||||
|
concerns (MCP servers start at boot, before any user query determines
|
||||||
|
a session_id).
|
||||||
122
docs/review/box-test-coverage.md
Normal file
122
docs/review/box-test-coverage.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Box 系统测试覆盖分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 测试文件清单
|
||||||
|
|
||||||
|
### LangBot 仓库
|
||||||
|
|
||||||
|
| 文件 | 行数 | CI 运行 | 覆盖范围 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| `tests/unit_tests/box/test_box_connector.py` | 106 | 是 | Connector 传输决策、WS relay URL、dispose、心跳/重连 |
|
||||||
|
| `tests/unit_tests/box/test_box_service.py` | 1224 | 是 | Service 核心逻辑(最全面) |
|
||||||
|
| `tests/unit_tests/box/test_workspace.py` | 147 | 是 | WorkspaceSession 路径重写、payload 构建 |
|
||||||
|
| `tests/unit_tests/provider/test_mcp_box_integration.py` | 707 | 是 | MCP Box 配置、路径重写、payload、shared-session/multi-process、runtime info |
|
||||||
|
| `tests/unit_tests/provider/test_localagent_sandbox_exec.py` | 444 | 是 | LocalAgent exec 流程、流式、Skill 激活 (Tool Call) |
|
||||||
|
| `tests/unit_tests/provider/test_tool_manager_native.py` | 249 | 是 | ToolManager 路由、native tool CRUD、路径穿越、6 工具暴露 |
|
||||||
|
| `tests/unit_tests/provider/test_skill_tools.py` | 582 | 是 | Skill 管理、Tool Call 激活、路径、authoring CRUD |
|
||||||
|
| `tests/unit_tests/test_skill_service.py` | 396 | 是 | HTTP service:skill CRUD、zip/GitHub install、文件浏览 |
|
||||||
|
| `tests/unit_tests/test_paths.py` | 23 | 是 | paths 工具 |
|
||||||
|
| `tests/unit_tests/test_preproc.py` | 134 | 是 | PreProcessor 注入 session 变量、bound skill 解析 |
|
||||||
|
| `tests/unit_tests/pipeline/test_chat_handler_logging.py` | 78 | 是 | Chat handler 日志相关回归 |
|
||||||
|
| `tests/integration_tests/box/test_box_integration.py` | 329 | **否** | 真实容器执行、超时、网络隔离 |
|
||||||
|
| `tests/integration_tests/box/test_box_mcp_integration.py` | 368 | **否** | Managed process、WS attach、shared-session 清理 |
|
||||||
|
|
||||||
|
### SDK 仓库
|
||||||
|
|
||||||
|
| 文件 | 行数 | CI 运行 | 覆盖范围 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| `tests/box/test_backend_selection.py` | 255 | 是 | 显式 backend / local 模式探测顺序 / 配置变更触发 reselect |
|
||||||
|
| `tests/box/test_nsjail_backend.py` | 452 | 是 | nsjail 可用性、安装版 CLI vs 容器内 CLI、session、arg 构建、资源限制 |
|
||||||
|
| `tests/box/test_e2b_backend.py` | 482 | 是 | E2B SDK mock、session 生命周期、extra_mounts 同步 |
|
||||||
|
| `tests/box/test_skill_store.py` | 88 | 是 | zip preview/install、基础 file CRUD |
|
||||||
|
|
||||||
|
**总计**: 17 个测试文件, ~6,500 行测试代码; 其中 2 个集成测试(约 700 行)在 CI 中不运行。
|
||||||
|
|
||||||
|
> 较 2026-04-16 版增加:`test_skill_service.py`、`test_paths.py`、`test_preproc.py`、`test_chat_handler_logging.py` (LangBot),`test_backend_selection.py`、`test_e2b_backend.py`、`test_skill_store.py` (SDK)。`test_nsjail_backend.py` 增加 CLI 兼容性 case (commit `feed530`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 覆盖良好的区域
|
||||||
|
|
||||||
|
| 区域 | 质量 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| BoxRuntime session 管理 | 优秀 | session 复用、冲突检测、TTL 配置、消失 session 重建 |
|
||||||
|
| BoxService Profile 系统 | 优秀 | 4 个内置 Profile、locked/unlocked 字段、timeout clamp |
|
||||||
|
| BoxService host mount 安全 | 优秀 | allowed_mount_roots、disallowed_roots、shared host root |
|
||||||
|
| BoxService workspace quota | 优秀 | 前置/后置配额检查、超额清理 |
|
||||||
|
| BoxService 输出截断 | 优秀 | 短/精确边界/长输出、独立 stderr |
|
||||||
|
| BoxService 可观测性 | 优秀 | 状态报告、error ring buffer、buffer 上限 |
|
||||||
|
| BoxService session 模板 | 良好 | `resolve_box_session_id` + `build_skill_extra_mounts` 在 service / native / mcp 三处都有覆盖 |
|
||||||
|
| RPC client/server 协议 | 优秀 | execute/get_sessions/delete/create/conflict error |
|
||||||
|
| BoxRuntimeConnector | 良好 | local/remote 模式、Docker 平台、relay URL、心跳与重连回调 |
|
||||||
|
| BoxWorkspaceSession | 良好 | payload 构建、managed process 路径重写、stage host file |
|
||||||
|
| BoxHostMountMode.NONE | 良好 | 枚举校验、workdir 约束 |
|
||||||
|
| NsjailBackend | 良好 | 可用性、安装版 vs 容器内、session 生命周期、arg 构建、资源限制 |
|
||||||
|
| E2BBackend | 良好 | mock SDK、session/extra_mounts 同步 |
|
||||||
|
| Backend selection | 良好 | 显式 backend 优先级、local 探测顺序、配置变更触发 reselect |
|
||||||
|
| MCP Box 集成 | 良好 | config model、路径重写、payload、shared-session 多 process |
|
||||||
|
| Native tool loader | 良好 | 6 工具(exec/read/write/edit/glob/grep)、路径穿越拦截 |
|
||||||
|
| LocalAgent exec 流程 | 良好 | 完整 tool call 循环、流式、system prompt 注入、Tool Call 激活 |
|
||||||
|
| Skill 系统 | 良好 | 加载、Tool Call 激活、marker、路径解析、authoring CRUD、HTTP service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 覆盖缺失的区域
|
||||||
|
|
||||||
|
### 3.1 零测试 / 严重不足
|
||||||
|
|
||||||
|
| 区域 | 源文件 | 影响 |
|
||||||
|
|------|--------|------|
|
||||||
|
| **`security.py`** | SDK `box/security.py` (52 行) | `validate_sandbox_security()` 无任何测试。阻止 `/etc`/`/proc`/Docker socket 等危险挂载的安全函数从未被验证 |
|
||||||
|
| **`policy.py`** | `pkg/box/policy.py` (98 行) | 三层安全策略无测试(也是死代码) |
|
||||||
|
| **`skill_store.py` 边缘场景** | SDK `box/skill_store.py` (647 行) vs 测试 88 行 | GitHub 安装路径、`source_subdir` / `target_suffix` 组合、损坏 zip、文件冲突等场景未覆盖 |
|
||||||
|
|
||||||
|
### 3.2 未测试的关键路径
|
||||||
|
|
||||||
|
| 区域 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **Session TTL 过期** | 测试配置了 `session_ttl_sec` 但从未推进时间验证过期清理 |
|
||||||
|
| **并发 session 访问** | 无并发 exec / 并发创建 / race condition 测试 |
|
||||||
|
| **Container backend (Docker)** | 仅通过集成测试覆盖(CI 不运行),单元测试全用 FakeBackend |
|
||||||
|
| **E2B 真实 sandbox** | 单测全是 mock,未对接真实 E2B API |
|
||||||
|
| **BoxRuntime shutdown()** | 在 test cleanup 中调用但未验证行为 |
|
||||||
|
| **BoxServerHandler 错误路径** | 畸形请求、未知 action 类型 |
|
||||||
|
| **WS relay** | 仅在集成测试中覆盖(CI 不运行) |
|
||||||
|
| **NsjailBackend managed process** | 完全未测试 |
|
||||||
|
| **MCP stdio 完整生命周期** | 依赖安装 → 进程启动 → 健康检查 → 多 process 并发 → 重试 |
|
||||||
|
| **BoxService start/stop_managed_process** | 单 process 流转有单测,多 process 互不阻塞主要靠集成测试 |
|
||||||
|
| **重连指数退避** | connector 单测覆盖回调接线,未实际跑完整重连周期 |
|
||||||
|
|
||||||
|
### 3.3 边缘情况缺失
|
||||||
|
|
||||||
|
| 区域 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| BoxSpec 校验 | 无效 session_id 格式、超长命令、env 特殊字符 |
|
||||||
|
| BoxSpec.extra_mounts | 重复 mount_path、与 host_path 冲突、绝对 vs 相对路径 |
|
||||||
|
| BoxExecutionResult | 仅 COMPLETED 和 TIMED_OUT,无 ERROR 状态测试 |
|
||||||
|
| 多后端 fallback | local 模式探测顺序仅靠 mock,无真实 Docker 不可用 → nsjail 真机 fallback 测试 |
|
||||||
|
| Profile YAML 加载 | 测试用硬编码字符串,未从真实 config.yaml 加载 |
|
||||||
|
| INIT 配置变更触发 backend 重建 | 单测仅在初始化场景验证 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 集成测试 vs CI 的差距
|
||||||
|
|
||||||
|
CI 仅运行 `tests/unit_tests/`,以下场景**从未在自动化中验证**:
|
||||||
|
|
||||||
|
- 真实容器的创建/执行/销毁
|
||||||
|
- 容器网络隔离(`--network none`)
|
||||||
|
- 容器资源限制生效(cpus/memory/pids_limit)
|
||||||
|
- Managed process 的 WS 双向 I/O
|
||||||
|
- 多 process 同 session 并发 I/O
|
||||||
|
- 孤儿容器清理
|
||||||
|
- Session 删除清理容器
|
||||||
|
- 进程退出检测
|
||||||
|
- E2B 真实 sandbox 行为
|
||||||
|
|
||||||
|
**建议**: 在 CI 中加一个可选的 Docker-in-Docker 集成测试 stage,至少覆盖核心执行路径(exec / MCP attach / session 销毁)。
|
||||||
167
docs/review/box-tob-analysis.md
Normal file
167
docs/review/box-tob-analysis.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Box 系统 toB 商业化分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现有优势
|
||||||
|
|
||||||
|
| 能力 | toB 价值 | 代码位置 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| **沙箱隔离执行** | 企业安全运行不受信代码的基础能力 | SDK `box/backend.py` |
|
||||||
|
| **多后端支持** | 适配不同企业容器基础设施 (Podman/Docker/nsjail/E2B) | SDK `box/runtime.py` `_select_backend()` |
|
||||||
|
| **E2B 云沙箱** | SaaS / 无 Docker 部署的兜底执行环境 | SDK `box/e2b_backend.py` |
|
||||||
|
| **连接自愈** | 心跳 + 自动重连,单点 Box runtime 故障可恢复 | `pkg/box/connector.py` `_heartbeat_loop`, `pkg/box/service.py` `_reconnect_loop` |
|
||||||
|
| **Profile + locked 字段** | 运维锁定安全边界,LLM/用户无法绕过 | `pkg/box/service.py`, SDK `box/models.py` |
|
||||||
|
| **资源限制** | CPU/内存/PID 数限制防止资源滥用 | SDK `backend.py` `--cpus/--memory/--pids-limit` |
|
||||||
|
| **Workspace quota** | 磁盘用量控制 | `pkg/box/service.py` `_enforce_workspace_quota` |
|
||||||
|
| **静默降级** | Box 不可用不影响其他功能,降低部署门槛 | `pkg/box/service.py:78` `_available=False` |
|
||||||
|
| **孤儿容器清理** | 防止泄漏的容器持续占用资源 | SDK `backend.py` `cleanup_orphaned_containers` |
|
||||||
|
| **网络隔离** | `--network none` 防止数据外泄 | SDK `backend.py` start_session |
|
||||||
|
| **只读根文件系统** | `--read-only` 防止容器被持久篡改 | SDK `backend.py` start_session |
|
||||||
|
| **Host path 白名单** | `allowed_host_mount_roots` 限制可挂载目录 | `pkg/box/service.py` `_validate_host_mount` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. toB 差距分析
|
||||||
|
|
||||||
|
### 2.1 安全与合规
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **WS relay 认证** | 无认证,任何人可 attach | 至少 token 认证 | **P0** |
|
||||||
|
| **安全策略** | policy.py 是死代码,实际无细粒度控制 | 工具级 allow/deny、沙箱模式控制 | **P0** |
|
||||||
|
| **审计日志** | 仅内存中 50 条 `_recent_errors` | 持久化审计:谁何时执行了什么、结果如何 | **P0** |
|
||||||
|
| **Host path 校验** | 黑名单策略,`/` 未拦截 | 白名单策略,默认拒绝 | **P1** |
|
||||||
|
| **数据驻留** | 无控制 | GDPR / 等保要求的数据隔离 | **P2** |
|
||||||
|
|
||||||
|
### 2.2 多租户
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **租户隔离** | 无租户概念 | BoxSpec/Profile 绑定 tenant_id | **P0** |
|
||||||
|
| **RBAC** | 仅 token 认证 | admin/operator/viewer 角色权限 | **P0** |
|
||||||
|
| **资源配额** | 单一 workspace quota | 每租户 CPU 时间/内存/并发/执行次数配额 | **P1** |
|
||||||
|
| **Session 隔离** | 所有 session 共享 dict | 按租户分区,互不可见 | **P1** |
|
||||||
|
|
||||||
|
### 2.3 可靠性
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **连接恢复** | 已实现:20s 心跳 + `_reconnect_loop` 指数退避 | 已满足基本要求 | 已有 |
|
||||||
|
| **Session 清理** | 机会性(仅新建时触发) | 定时清理 + 独立 reaper | **P1** |
|
||||||
|
| **水平扩展** | 单 Box Runtime 实例 | 多实例负载均衡(按 tenant 路由) | **P1** |
|
||||||
|
| **优雅降级** | 已有(_available=False) | 已满足基本要求 | 已有 |
|
||||||
|
| **Backend 自愈** | 已实现:`get_status` 时若 backend 不可用会重新选择 | 已满足基本要求 | 已有 |
|
||||||
|
|
||||||
|
### 2.4 可观测性
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **监控指标** | 无 Prometheus metrics | session 数/执行延迟/资源用量/错误率 | **P1** |
|
||||||
|
| **结构化日志** | Python logging, 无结构化 | JSON 格式日志,含 trace_id/tenant_id | **P1** |
|
||||||
|
| **前端面板** | 监控页接入 `/api/v1/box/status`(backend 名 + 活跃 session 数);`sessions` / `errors` 仍未接入 | 完整状态面板 + 历史错误/审计列表 | **P2** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SaaS 部署架构建议
|
||||||
|
|
||||||
|
### 3.1 方案 A: 共享 Box Runtime Pool (快速上线)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot Instance ──> Box Runtime (共享)
|
||||||
|
├─ tenant_id 标签隔离
|
||||||
|
├─ Redis 配额计数器
|
||||||
|
└─ Container labels: langbot.tenant_id=xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 改动最小,加 tenant_id 到 BoxSpec/labels 即可
|
||||||
|
- **缺点**: 容器引擎共享,安全隔离弱
|
||||||
|
|
||||||
|
### 3.2 方案 B: 每租户 K8s Namespace + gVisor (推荐中期)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot ──> K8s API
|
||||||
|
├─ namespace: tenant-xxx
|
||||||
|
│ ├─ RuntimeClass: gVisor (runsc)
|
||||||
|
│ ├─ ResourceQuota
|
||||||
|
│ └─ NetworkPolicy
|
||||||
|
└─ namespace: tenant-yyy
|
||||||
|
└─ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 强隔离(namespace + gVisor),原生 K8s 配额
|
||||||
|
- **缺点**: 需要重写 backend 为 K8s Job,部署复杂度高
|
||||||
|
|
||||||
|
### 3.3 方案 C: K8s Job 直接编排 (长期)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot ──> K8s Job per execution
|
||||||
|
├─ 每次执行创建 Job
|
||||||
|
├─ Pod Security Standards
|
||||||
|
├─ 自动调度和资源分配
|
||||||
|
└─ Job TTL Controller 自动清理
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 最强隔离,天然水平扩展
|
||||||
|
- **缺点**: 冷启动延迟,架构重写
|
||||||
|
|
||||||
|
**推荐演进路径**: A → B → C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配额体系建议
|
||||||
|
|
||||||
|
### 三层配额
|
||||||
|
|
||||||
|
| 层 | 实现 | 作用 |
|
||||||
|
|----|------|------|
|
||||||
|
| **内核层** | Docker `--cpus`/`--memory`/`--storage-opt` | 硬性资源上限,不可绕过 |
|
||||||
|
| **应用层** | Redis 原子计数器 | 并发 session 数/执行次数/CPU 时间预算 |
|
||||||
|
| **计费层** | 月度聚合 | 按租户计费(session-hours/execution-count) |
|
||||||
|
|
||||||
|
### Profile 与套餐映射
|
||||||
|
|
||||||
|
| 套餐 | Profile | locked 字段 | 配额 |
|
||||||
|
|------|---------|------------|------|
|
||||||
|
| Free | `offline_readonly` | network, host_path_mode, rootfs | 10 exec/天, 0.5 CPU, 256MB |
|
||||||
|
| Pro | `default` | (无) | 100 exec/天, 1 CPU, 512MB |
|
||||||
|
| Enterprise | `network_extended` | (按需) | 无限, 2 CPU, 1GB, 自定义镜像 |
|
||||||
|
|
||||||
|
### TOCTOU 配额修复
|
||||||
|
|
||||||
|
当前 `_enforce_workspace_quota` 的 TOCTOU 问题可通过两种方式解决:
|
||||||
|
|
||||||
|
1. **预留式配额** (应用层): Redis `INCRBY` 预扣额度 → 执行 → 成功则扣减,失败则回滚
|
||||||
|
2. **内核级限制** (Docker): `--storage-opt size=500m` 直接限制容器可写层大小
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 优先实施路线
|
||||||
|
|
||||||
|
### Phase 1 (2-4 周): 安全基线
|
||||||
|
|
||||||
|
- [ ] WS relay 加 token 认证
|
||||||
|
- [ ] 接入或删除 policy.py
|
||||||
|
- [x] ~~Box 加重连和心跳~~(已完成,见 [box-issues.md 已解决](./box-issues.md))
|
||||||
|
- [ ] 审计日志持久化(至少写文件/数据库)
|
||||||
|
- [ ] `security.py` 加 `/` 拦截,考虑白名单
|
||||||
|
- [ ] INIT 与 backend 初始化顺序整理(避免 backend 在配置到达前实例化)
|
||||||
|
|
||||||
|
### Phase 2 (4-8 周): 多租户基础
|
||||||
|
|
||||||
|
- [ ] BoxSpec 加 `tenant_id` 字段
|
||||||
|
- [ ] 容器 labels 加 tenant 标识
|
||||||
|
- [ ] Redis 配额计数器(并发/执行次数/时间)
|
||||||
|
- [ ] RBAC 基础框架
|
||||||
|
- [ ] 定时 session reaper
|
||||||
|
|
||||||
|
### Phase 3 (8-16 周): 生产就绪
|
||||||
|
|
||||||
|
- [ ] Prometheus metrics exporter
|
||||||
|
- [ ] 前端 Box 状态面板
|
||||||
|
- [ ] K8s backend 支持 (方案 B)
|
||||||
|
- [ ] 结构化日志 (JSON, trace_id)
|
||||||
|
- [ ] 水平扩展支持
|
||||||
222
docs/review/box-vs-plugin-runtime.md
Normal file
222
docs/review/box-vs-plugin-runtime.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# Box Runtime vs Plugin Runtime: 连接架构对比
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 总体差异
|
||||||
|
|
||||||
|
| 维度 | Plugin Runtime | Box Runtime |
|
||||||
|
|------|---------------|-------------|
|
||||||
|
| **继承关系** | `PluginRuntimeConnector(ManagedRuntimeConnector)` | `BoxRuntimeConnector`(独立类) |
|
||||||
|
| **传输分支** | 3 条 (Docker/WS, Win32/subprocess+WS, Unix/stdio) | 3 条 (本地 stdio, Win32/subprocess+WS, 远程 WS) |
|
||||||
|
| **心跳** | 20s ping loop | 20s ping loop(`_heartbeat_loop`) |
|
||||||
|
| **重连** | WS 模式: sleep 3s → re-initialize | 由 BoxService `_reconnect_loop` 处理,指数退避 |
|
||||||
|
| **Handler 类型** | `RuntimeConnectionHandler` (1132 行, 25+ action) | 基础 `Handler` + `BoxServerHandler`(SDK 端 25 action) |
|
||||||
|
| **Client 抽象** | Handler 即 API | 独立 `ActionRPCBoxClient` 封装 Handler |
|
||||||
|
| **启用/禁用** | `is_enable_plugin` 开关 | 无开关(可用/不可用由初始化结果决定) |
|
||||||
|
| **初始化失败** | 异常上抛 | 静默降级 `_available=False` |
|
||||||
|
| **Shutdown** | 直接杀进程 | RPC SHUTDOWN → 清理容器 → 再杀进程 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 传输决策
|
||||||
|
|
||||||
|
### Plugin: 3-路决策
|
||||||
|
|
||||||
|
```python
|
||||||
|
# pkg/plugin/connector.py:106-165
|
||||||
|
if get_platform() == 'docker' or use_websocket_to_connect_plugin_runtime():
|
||||||
|
# Docker/WS → ws://langbot_plugin_runtime:5400/control/ws
|
||||||
|
elif get_platform() == 'win32':
|
||||||
|
# Windows → 起子进程(无 pipe) + ws://localhost:5400/control/ws
|
||||||
|
else:
|
||||||
|
# Unix/Mac → StdioClientController(python -m langbot_plugin.cli rt -s)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Box: 3-路决策
|
||||||
|
|
||||||
|
```python
|
||||||
|
# pkg/box/connector.py
|
||||||
|
if self._uses_websocket():
|
||||||
|
if platform.get_platform() == 'win32' and not self.configured_runtime_url:
|
||||||
|
await self._start_subprocess_then_ws() # subprocess + ws://localhost:5410/rpc/ws
|
||||||
|
else:
|
||||||
|
await self._connect_remote_ws() # ws://{host}:5410/rpc/ws
|
||||||
|
else:
|
||||||
|
await self._start_local_stdio() # StdioClientController
|
||||||
|
```
|
||||||
|
|
||||||
|
> 历史:2026-04-16 版本本文档曾把 Box 描述为 2 路决策(缺 Windows 分支)。现已对齐 Plugin 的 3 路设计。
|
||||||
|
|
||||||
|
### 决策矩阵
|
||||||
|
|
||||||
|
| 环境 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Docker | WS → `:5400` | WS → `:5410/rpc/ws` |
|
||||||
|
| `--standalone-box` | N/A | WS → `localhost:5410/rpc/ws` |
|
||||||
|
| Windows 非 Docker | subprocess + WS (`:5400`) | subprocess + WS (`localhost:5410/rpc/ws`) |
|
||||||
|
| Unix/Mac 非 Docker | stdio | stdio |
|
||||||
|
| 手动配置 URL | 通过配置项 | WS → 用户配置的 URL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 连接建立
|
||||||
|
|
||||||
|
### 同步模式差异
|
||||||
|
|
||||||
|
**Plugin**: `new_connection_callback` 内直接 ping + await handler_task,`initialize()` 通过 `create_task()` 异步启动,不阻塞等待连接。
|
||||||
|
|
||||||
|
**Box**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式,`initialize()` 同步等待连接成功或超时。
|
||||||
|
|
||||||
|
### Box stdio 路径
|
||||||
|
|
||||||
|
```
|
||||||
|
connector._start_local_stdio()
|
||||||
|
├─ connected = asyncio.Event()
|
||||||
|
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.cli.__init__', 'box', '-s', '--ws-control-port', N])
|
||||||
|
├─ _ctrl_task = create_task(ctrl.run(callback))
|
||||||
|
│ callback:
|
||||||
|
│ handler = Handler(connection) ← 基础 Handler, 无 disconnect_callback
|
||||||
|
│ client.set_handler(handler)
|
||||||
|
│ _handler_task = create_task(handler.run())
|
||||||
|
│ call_action(PING, {}) ← 握手, timeout=15s
|
||||||
|
│ connected.set() ← 通知外层
|
||||||
|
│ await _handler_task ← 阻塞直到断开
|
||||||
|
└─ await wait_for(connected.wait(), 30s) ← 同步等待
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin stdio 路径
|
||||||
|
|
||||||
|
```
|
||||||
|
connector.initialize()
|
||||||
|
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.cli', 'rt', '-s'])
|
||||||
|
├─ task = ctrl.run(callback)
|
||||||
|
│ callback:
|
||||||
|
│ disconnect_callback:
|
||||||
|
│ [WS] → runtime_disconnect_callback → 重连
|
||||||
|
│ [stdio] → 仅日志, 不重连
|
||||||
|
│ handler = RuntimeConnectionHandler(conn, disconnect_cb, ap)
|
||||||
|
│ create_task(handler.run())
|
||||||
|
│ handler.ping() ← 握手, timeout=10s
|
||||||
|
│ await handler_task ← 阻塞直到断开
|
||||||
|
├─ create_task(heartbeat_loop()) ← 20s ping loop
|
||||||
|
└─ create_task(task) ← 不等待连接
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 心跳与重连
|
||||||
|
|
||||||
|
### 心跳
|
||||||
|
|
||||||
|
| 维度 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| 有心跳? | 是 | 是(`connector.py` `_heartbeat_loop`) |
|
||||||
|
| 间隔 | 20s | 20s |
|
||||||
|
| 失败处理 | 仅 DEBUG 日志,不触发重连 | 仅 DEBUG 日志,依赖 connection close 触发重连 |
|
||||||
|
| 生命周期 | 整个应用生命周期 | 连接建立后启动;`dispose()` 时 cancel |
|
||||||
|
|
||||||
|
### 重连
|
||||||
|
|
||||||
|
| 维度 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Docker/WS 断开 | `runtime_disconnect_callback` → sleep 3s → re-initialize | `runtime_disconnect_callback` → `BoxService._reconnect_loop()`(指数退避) |
|
||||||
|
| WS 连接失败 | 同上 | 同上;初次失败时 `_available=False`,重连成功后恢复 |
|
||||||
|
| stdio 断开 | 仅日志,不重连 | 接同样回调;stdio 重连需重新 fork 子进程 |
|
||||||
|
| 重连退避 | 固定 3s,无 backoff | 指数退避 |
|
||||||
|
|
||||||
|
> 历史:2026-04-16 版本本文档曾把心跳与重连标记为 Box 缺失。这两项已在 commit `2dfd9d5d` / `c6882cf` / `5029d9c` 等修复(详见 [box-issues.md 已解决](./box-issues.md))。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 共享 IO 层
|
||||||
|
|
||||||
|
两者复用同一套 SDK IO 基础设施:
|
||||||
|
|
||||||
|
```
|
||||||
|
Handler ← ABC (runtime/io/handler.py)
|
||||||
|
├── RuntimeConnectionHandler (Plugin 用, LangBot 侧)
|
||||||
|
├── ControlConnectionHandler (Plugin 用, SDK 侧)
|
||||||
|
├── BoxServerHandler (Box 用, SDK 侧)
|
||||||
|
└── 匿名 Handler 实例 (Box 用, LangBot 侧)
|
||||||
|
|
||||||
|
Connection ← ABC
|
||||||
|
├── StdioConnection (stdio: 16KB chunks, 应用层分帧协议)
|
||||||
|
└── WebSocketConnection (WS: 64KB chunks, 原生 WS 分帧)
|
||||||
|
|
||||||
|
Controller ← ABC
|
||||||
|
├── StdioClientController (fork 子进程, pipe stdin/stdout)
|
||||||
|
├── StdioServerController (接管当前进程 stdin/stdout)
|
||||||
|
├── WebSocketClientController (连接 WS 服务端)
|
||||||
|
└── WebSocketServerController (监听 WS 端口)
|
||||||
|
```
|
||||||
|
|
||||||
|
共享的核心机制:
|
||||||
|
- `call_action()` / `call_action_generator()` — RPC 调用/流式调用
|
||||||
|
- `ActionRequest` / `ActionResponse` — 请求/响应协议
|
||||||
|
- `seq_id` 关联 — 并发请求复用单连接
|
||||||
|
- `CommonAction.PING` — 两者都用于初始握手
|
||||||
|
- 文件传输 (`send_file`) — Plugin 用,Box 不用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 端口方案
|
||||||
|
|
||||||
|
| 服务 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Action RPC (stdio) | stdin/stdout | stdin/stdout |
|
||||||
|
| Action RPC (WS) | `:5400` | `:5410/rpc/ws` |
|
||||||
|
| 辅助服务 | debug WS `:5401` | managed process WS relay `:5410/v1/sessions/{id}/managed-process/ws` |
|
||||||
|
|
||||||
|
**Box 特点**: 单端口 aiohttp 服务(默认 5410),通过路径区分 Action RPC 和 managed process relay。即使在 stdio 模式,也在 `:5410` 启动 aiohttp 用于 managed process attach。Plugin 在 stdio 模式不开额外端口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 销毁对比
|
||||||
|
|
||||||
|
### Plugin
|
||||||
|
|
||||||
|
```python
|
||||||
|
dispose():
|
||||||
|
if stdio: ctrl.process.terminate()
|
||||||
|
_dispose_subprocess() # Windows 子进程
|
||||||
|
heartbeat_task.cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Box
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector.dispose():
|
||||||
|
_handler_task.cancel()
|
||||||
|
_ctrl_task.cancel()
|
||||||
|
_subprocess.terminate()
|
||||||
|
|
||||||
|
service.dispose():
|
||||||
|
connector.dispose()
|
||||||
|
loop.create_task(client.shutdown()) # RPC SHUTDOWN → 清理所有容器
|
||||||
|
```
|
||||||
|
|
||||||
|
Box 的 RPC SHUTDOWN 确保容器被正确停止,不会成为孤儿。Plugin 直接杀进程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 改进建议
|
||||||
|
|
||||||
|
### P0
|
||||||
|
|
||||||
|
1. **两者都加 WS 认证**: 至少 token 认证(INIT 时下发,连接时校验)
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
2. **考虑 Box 继承 ManagedRuntimeConnector**: 复用 `_start_runtime_subprocess` / `_wait_until_ready` / `_dispose_subprocess`,减少重复代码
|
||||||
|
3. **Plugin 重连加退避**: 固定 3s 无 backoff 可能造成日志洪水,建议向 Box 的指数退避看齐
|
||||||
|
4. **统一连接管理模式**: Event-based (Box) vs direct-await (Plugin),考虑收敛为一种
|
||||||
|
|
||||||
|
### 已完成(自上一轮)
|
||||||
|
|
||||||
|
- ~~Box 加重连~~(commit `2dfd9d5d`)
|
||||||
|
- ~~Box 加心跳~~(20s loop 与 Plugin 一致)
|
||||||
|
- ~~Box 加 Windows 支持~~(commit `120817a` / `fafb7a4`)
|
||||||
1944
docs/service-api-openapi.json
Normal file
1944
docs/service-api-openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,45 +0,0 @@
|
|||||||
from v1 import client # type: ignore
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class TestDifyClient:
|
|
||||||
async def test_chat_messages(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
|
|
||||||
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_upload_file(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
file_bytes = open('img.png', 'rb').read()
|
|
||||||
|
|
||||||
print(type(file_bytes))
|
|
||||||
|
|
||||||
file = ('img2.png', file_bytes, 'image/png')
|
|
||||||
|
|
||||||
resp = await cln.upload_file(file=file, user='test')
|
|
||||||
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_workflow_run(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
# resp = await cln.workflow_run(inputs={}, user="test")
|
|
||||||
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
# print(resp)
|
|
||||||
chunks = []
|
|
||||||
|
|
||||||
ignored_events = ['text_chunk']
|
|
||||||
async for chunk in cln.workflow_run(inputs={}, user='test'):
|
|
||||||
if chunk['event'] in ignored_events:
|
|
||||||
continue
|
|
||||||
chunks.append(chunk)
|
|
||||||
print(json.dumps(chunks, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(TestDifyClient().test_chat_messages())
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import time
|
|
||||||
from quart import request
|
|
||||||
import httpx
|
|
||||||
from quart import Quart
|
|
||||||
from typing import Callable, Dict, Any
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
from .qqofficialevent import QQOfficialEvent
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
||||||
|
|
||||||
|
|
||||||
def handle_validation(body: dict, bot_secret: str):
|
|
||||||
# bot正确的secert是32位的,此处仅为了适配演示demo
|
|
||||||
while len(bot_secret) < 32:
|
|
||||||
bot_secret = bot_secret * 2
|
|
||||||
bot_secret = bot_secret[:32]
|
|
||||||
# 实际使用场景中以上三行内容可清除
|
|
||||||
|
|
||||||
seed_bytes = bot_secret.encode()
|
|
||||||
|
|
||||||
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
|
|
||||||
|
|
||||||
msg = body['d']['event_ts'] + body['d']['plain_token']
|
|
||||||
msg_bytes = msg.encode()
|
|
||||||
|
|
||||||
signature = signing_key.sign(msg_bytes)
|
|
||||||
|
|
||||||
signature_hex = signature.hex()
|
|
||||||
|
|
||||||
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
|
||||||
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
self.secret = secret
|
|
||||||
self.token = token
|
|
||||||
self.app_id = app_id
|
|
||||||
self._message_handlers = {}
|
|
||||||
self.base_url = 'https://api.sgroup.qq.com'
|
|
||||||
self.access_token = ''
|
|
||||||
self.access_token_expiry_time = None
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def check_access_token(self):
|
|
||||||
"""检查access_token是否存在"""
|
|
||||||
if not self.access_token or await self.is_token_expired():
|
|
||||||
return False
|
|
||||||
return bool(self.access_token and self.access_token.strip())
|
|
||||||
|
|
||||||
async def get_access_token(self):
|
|
||||||
"""获取access_token"""
|
|
||||||
url = 'https://bots.qq.com/app/getAppAccessToken'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
params = {
|
|
||||||
'appId': self.app_id,
|
|
||||||
'clientSecret': self.secret,
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = await client.post(url, json=params, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
response_data = response.json()
|
|
||||||
access_token = response_data.get('access_token')
|
|
||||||
expires_in = int(response_data.get('expires_in', 7200))
|
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
|
||||||
if access_token:
|
|
||||||
self.access_token = access_token
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'获取access_token失败: {response_data}')
|
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
"""处理回调请求"""
|
|
||||||
try:
|
|
||||||
# 读取请求数据
|
|
||||||
body = await request.get_data()
|
|
||||||
payload = json.loads(body)
|
|
||||||
|
|
||||||
# 验证是否为回调验证请求
|
|
||||||
if payload.get('op') == 13:
|
|
||||||
# 生成签名
|
|
||||||
response = handle_validation(payload, self.secret)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
if payload.get('op') == 0:
|
|
||||||
message_data = await self.get_message(payload)
|
|
||||||
if message_data:
|
|
||||||
event = QQOfficialEvent.from_payload(message_data)
|
|
||||||
await self._handle_message(event)
|
|
||||||
|
|
||||||
return {'code': 0, 'message': 'success'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
|
||||||
return {'error': str(e)}, 400
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""启动 Quart 应用"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
"""注册消息类型处理器"""
|
|
||||||
|
|
||||||
def decorator(func: Callable[[platform_events.Event], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def _handle_message(self, event: QQOfficialEvent):
|
|
||||||
"""处理消息事件"""
|
|
||||||
msg_type = event.t
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
|
|
||||||
async def get_message(self, msg: dict) -> Dict[str, Any]:
|
|
||||||
"""获取消息"""
|
|
||||||
message_data = {
|
|
||||||
't': msg.get('t', {}),
|
|
||||||
'user_openid': msg.get('d', {}).get('author', {}).get('user_openid', {}),
|
|
||||||
'timestamp': msg.get('d', {}).get('timestamp', {}),
|
|
||||||
'd_author_id': msg.get('d', {}).get('author', {}).get('id', {}),
|
|
||||||
'content': msg.get('d', {}).get('content', {}),
|
|
||||||
'd_id': msg.get('d', {}).get('id', {}),
|
|
||||||
'id': msg.get('id', {}),
|
|
||||||
'channel_id': msg.get('d', {}).get('channel_id', {}),
|
|
||||||
'username': msg.get('d', {}).get('author', {}).get('username', {}),
|
|
||||||
'guild_id': msg.get('d', {}).get('guild_id', {}),
|
|
||||||
'member_openid': msg.get('d', {}).get('author', {}).get('openid', {}),
|
|
||||||
'group_openid': msg.get('d', {}).get('group_openid', {}),
|
|
||||||
}
|
|
||||||
attachments = msg.get('d', {}).get('attachments', [])
|
|
||||||
image_attachments = [attachment['url'] for attachment in attachments if await self.is_image(attachment)]
|
|
||||||
image_attachments_type = [
|
|
||||||
attachment['content_type'] for attachment in attachments if await self.is_image(attachment)
|
|
||||||
]
|
|
||||||
if image_attachments:
|
|
||||||
message_data['image_attachments'] = image_attachments[0]
|
|
||||||
message_data['content_type'] = image_attachments_type[0]
|
|
||||||
else:
|
|
||||||
message_data['image_attachments'] = None
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def is_image(self, attachment: dict) -> bool:
|
|
||||||
"""判断是否为图片附件"""
|
|
||||||
content_type = attachment.get('content_type', '')
|
|
||||||
return content_type.startswith('image/')
|
|
||||||
|
|
||||||
async def send_private_text_msg(self, user_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
response_data = response.json()
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
|
||||||
raise ValueError(response)
|
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送群聊消息失败:{response.json()}')
|
|
||||||
raise Exception(response.read().decode())
|
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def is_token_expired(self):
|
|
||||||
"""检查token是否过期"""
|
|
||||||
if self.access_token_expiry_time is None:
|
|
||||||
return True
|
|
||||||
return time.time() > self.access_token_expiry_time
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import json
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from urllib.parse import unquote
|
|
||||||
import hashlib
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
|
||||||
from quart import Quart, request, Response, jsonify
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import asyncio
|
|
||||||
from libs.wecom_ai_bot_api import wecombotevent
|
|
||||||
from typing import Callable
|
|
||||||
import base64
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from pkg.platform.logger import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotClient:
|
|
||||||
def __init__(self,Token:str,EnCodingAESKey:str,Corpid:str,logger:EventLogger):
|
|
||||||
self.Token=Token
|
|
||||||
self.EnCodingAESKey=EnCodingAESKey
|
|
||||||
self.Corpid=Corpid
|
|
||||||
self.ReceiveId = ''
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['POST','GET']
|
|
||||||
)
|
|
||||||
self._message_handlers = {
|
|
||||||
'example': [],
|
|
||||||
}
|
|
||||||
self.user_stream_map = {}
|
|
||||||
self.logger = logger
|
|
||||||
self.generated_content = {}
|
|
||||||
self.msg_id_map = {}
|
|
||||||
|
|
||||||
async def sha1_signature(token: str, timestamp: str, nonce: str, encrypt: str) -> str:
|
|
||||||
raw = "".join(sorted([token, timestamp, nonce, encrypt]))
|
|
||||||
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
try:
|
|
||||||
self.wxcpt=WXBizMsgCrypt(self.Token,self.EnCodingAESKey,'')
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
|
|
||||||
msg_signature = unquote(request.args.get("msg_signature", ""))
|
|
||||||
timestamp = unquote(request.args.get("timestamp", ""))
|
|
||||||
nonce = unquote(request.args.get("nonce", ""))
|
|
||||||
echostr = unquote(request.args.get("echostr", ""))
|
|
||||||
|
|
||||||
if not all([msg_signature, timestamp, nonce, echostr]):
|
|
||||||
await self.logger.error("请求参数缺失")
|
|
||||||
return Response("缺少参数", status=400)
|
|
||||||
|
|
||||||
ret, decrypted_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
|
||||||
if ret != 0:
|
|
||||||
|
|
||||||
await self.logger.error("验证URL失败")
|
|
||||||
return Response("验证失败", status=403)
|
|
||||||
|
|
||||||
return Response(decrypted_str, mimetype="text/plain")
|
|
||||||
|
|
||||||
elif request.method == "POST":
|
|
||||||
msg_signature = unquote(request.args.get("msg_signature", ""))
|
|
||||||
timestamp = unquote(request.args.get("timestamp", ""))
|
|
||||||
nonce = unquote(request.args.get("nonce", ""))
|
|
||||||
|
|
||||||
try:
|
|
||||||
timeout = 3
|
|
||||||
interval = 0.1
|
|
||||||
start_time = time.monotonic()
|
|
||||||
encrypted_json = await request.get_json()
|
|
||||||
encrypted_msg = encrypted_json.get("encrypt", "")
|
|
||||||
if not encrypted_msg:
|
|
||||||
await self.logger.error("请求体中缺少 'encrypt' 字段")
|
|
||||||
|
|
||||||
xml_post_data = f"<xml><Encrypt><![CDATA[{encrypted_msg}]]></Encrypt></xml>"
|
|
||||||
ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce)
|
|
||||||
if ret != 0:
|
|
||||||
await self.logger.error("解密失败")
|
|
||||||
|
|
||||||
|
|
||||||
msg_json = json.loads(decrypted_xml)
|
|
||||||
|
|
||||||
from_user_id = msg_json.get("from", {}).get("userid")
|
|
||||||
chatid = msg_json.get("chatid", "")
|
|
||||||
|
|
||||||
message_data = await self.get_message(msg_json)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if message_data:
|
|
||||||
try:
|
|
||||||
event = wecombotevent.WecomBotEvent(message_data)
|
|
||||||
if event:
|
|
||||||
await self._handle_message(event)
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
if msg_json.get('chattype','') == 'single':
|
|
||||||
if from_user_id in self.user_stream_map:
|
|
||||||
stream_id = self.user_stream_map[from_user_id]
|
|
||||||
else:
|
|
||||||
stream_id =str(uuid.uuid4())
|
|
||||||
self.user_stream_map[from_user_id] = stream_id
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
if chatid in self.user_stream_map:
|
|
||||||
stream_id = self.user_stream_map[chatid]
|
|
||||||
else:
|
|
||||||
stream_id = str(uuid.uuid4())
|
|
||||||
self.user_stream_map[chatid] = stream_id
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
while True:
|
|
||||||
content = self.generated_content.pop(msg_json['msgid'],None)
|
|
||||||
if content:
|
|
||||||
reply_plain = {
|
|
||||||
"msgtype": "stream",
|
|
||||||
"stream": {
|
|
||||||
"id": stream_id,
|
|
||||||
"finish": True,
|
|
||||||
"content": content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reply_plain_str = json.dumps(reply_plain, ensure_ascii=False)
|
|
||||||
|
|
||||||
reply_timestamp = str(int(time.time()))
|
|
||||||
ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp)
|
|
||||||
if ret != 0:
|
|
||||||
|
|
||||||
await self.logger.error("加密失败"+str(ret))
|
|
||||||
|
|
||||||
|
|
||||||
root = ET.fromstring(encrypt_text)
|
|
||||||
encrypt = root.find("Encrypt").text
|
|
||||||
resp = {
|
|
||||||
"encrypt": encrypt,
|
|
||||||
}
|
|
||||||
return jsonify(resp), 200
|
|
||||||
|
|
||||||
if time.time() - start_time > timeout:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|
||||||
if self.msg_id_map.get(message_data['msgid'], 1) == 3:
|
|
||||||
await self.logger.error('请求失效:暂不支持智能机器人超过7秒的请求,如有需求,请联系 LangBot 团队。')
|
|
||||||
return ''
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
|
|
||||||
async def get_message(self,msg_json):
|
|
||||||
message_data = {}
|
|
||||||
|
|
||||||
if msg_json.get('chattype','') == 'single':
|
|
||||||
message_data['type'] = 'single'
|
|
||||||
elif msg_json.get('chattype','') == 'group':
|
|
||||||
message_data['type'] = 'group'
|
|
||||||
|
|
||||||
if msg_json.get('msgtype') == 'text':
|
|
||||||
message_data['content'] = msg_json.get('text',{}).get('content')
|
|
||||||
elif msg_json.get('msgtype') == 'image':
|
|
||||||
picurl = msg_json.get('image', {}).get('url','')
|
|
||||||
base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64
|
|
||||||
elif msg_json.get('msgtype') == 'mixed':
|
|
||||||
items = msg_json.get('mixed', {}).get('msg_item', [])
|
|
||||||
texts = []
|
|
||||||
picurl = None
|
|
||||||
for item in items:
|
|
||||||
if item.get('msgtype') == 'text':
|
|
||||||
texts.append(item.get('text', {}).get('content', ''))
|
|
||||||
elif item.get('msgtype') == 'image' and picurl is None:
|
|
||||||
picurl = item.get('image', {}).get('url')
|
|
||||||
|
|
||||||
if texts:
|
|
||||||
message_data['content'] = "".join(texts) # 拼接所有 text
|
|
||||||
if picurl:
|
|
||||||
base64 = await self.download_url_to_base64(picurl,self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64 # 只保留第一个 image
|
|
||||||
|
|
||||||
message_data['userid'] = msg_json.get('from', {}).get('userid', '')
|
|
||||||
message_data['msgid'] = msg_json.get('msgid', '')
|
|
||||||
|
|
||||||
if msg_json.get('aibotid'):
|
|
||||||
message_data['aibotid'] = msg_json.get('aibotid', '')
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def _handle_message(self, event: wecombotevent.WecomBotEvent):
|
|
||||||
"""
|
|
||||||
处理消息事件。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
message_id = event.message_id
|
|
||||||
if message_id in self.msg_id_map.keys():
|
|
||||||
self.msg_id_map[message_id] += 1
|
|
||||||
return
|
|
||||||
self.msg_id_map[message_id] = 1
|
|
||||||
msg_type = event.type
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
async def set_message(self, msg_id: str, content: str):
|
|
||||||
self.generated_content[msg_id] = content
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
def decorator(func: Callable[[wecombotevent.WecomBotEvent], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
async def download_url_to_base64(self, download_url, encoding_aes_key):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(download_url)
|
|
||||||
if response.status_code != 200:
|
|
||||||
await self.logger.error(f'failed to get file: {response.text}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
encrypted_bytes = response.content
|
|
||||||
|
|
||||||
|
|
||||||
aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐
|
|
||||||
iv = aes_key[:16]
|
|
||||||
|
|
||||||
|
|
||||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
|
|
||||||
decrypted = cipher.decrypt(encrypted_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
pad_len = decrypted[-1]
|
|
||||||
decrypted = decrypted[:-pad_len]
|
|
||||||
|
|
||||||
|
|
||||||
if decrypted.startswith(b"\xff\xd8"): # JPEG
|
|
||||||
mime_type = "image/jpeg"
|
|
||||||
elif decrypted.startswith(b"\x89PNG"): # PNG
|
|
||||||
mime_type = "image/png"
|
|
||||||
elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF
|
|
||||||
mime_type = "image/gif"
|
|
||||||
elif decrypted.startswith(b"BM"): # BMP
|
|
||||||
mime_type = "image/bmp"
|
|
||||||
elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF
|
|
||||||
mime_type = "image/tiff"
|
|
||||||
else:
|
|
||||||
mime_type = "application/octet-stream"
|
|
||||||
|
|
||||||
# 转 base64
|
|
||||||
base64_str = base64.b64encode(decrypted).decode("utf-8")
|
|
||||||
return f"data:{mime_type};base64,{base64_str}"
|
|
||||||
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
启动 Quart 应用。
|
|
||||||
"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
from typing import Dict, Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotEvent(dict):
|
|
||||||
@staticmethod
|
|
||||||
def from_payload(payload: Dict[str, Any]) -> Optional['WecomBotEvent']:
|
|
||||||
try:
|
|
||||||
event = WecomBotEvent(payload)
|
|
||||||
return event
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self) -> str:
|
|
||||||
"""
|
|
||||||
事件类型
|
|
||||||
"""
|
|
||||||
return self.get('type', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def userid(self) -> str:
|
|
||||||
"""
|
|
||||||
用户id
|
|
||||||
"""
|
|
||||||
return self.get('from', {}).get('userid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self) -> str:
|
|
||||||
"""
|
|
||||||
内容
|
|
||||||
"""
|
|
||||||
return self.get('content', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def picurl(self) -> str:
|
|
||||||
"""
|
|
||||||
图片url
|
|
||||||
"""
|
|
||||||
return self.get('picurl', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chatid(self) -> str:
|
|
||||||
"""
|
|
||||||
群组id
|
|
||||||
"""
|
|
||||||
return self.get('chatid', {})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message_id(self) -> str:
|
|
||||||
"""
|
|
||||||
消息id
|
|
||||||
"""
|
|
||||||
return self.get('msgid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ai_bot_id(self) -> str:
|
|
||||||
"""
|
|
||||||
AI Bot ID
|
|
||||||
"""
|
|
||||||
return self.get('aibotid', '')
|
|
||||||
118
main.py
118
main.py
@@ -1,117 +1,3 @@
|
|||||||
import asyncio
|
import langbot.__main__
|
||||||
import argparse
|
|
||||||
# LangBot 终端启动入口
|
|
||||||
# 在此层级解决依赖项检查。
|
|
||||||
# LangBot/main.py
|
|
||||||
|
|
||||||
asciiart = r"""
|
langbot.__main__.main()
|
||||||
_ ___ _
|
|
||||||
| | __ _ _ _ __ _| _ ) ___| |_
|
|
||||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
|
||||||
|____\__,_|_||_\__, |___/\___/\__|
|
|
||||||
|___/
|
|
||||||
|
|
||||||
⭐️ Open Source 开源地址: https://github.com/langbot-app/LangBot
|
|
||||||
📖 Documentation 文档地址: https://docs.langbot.app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
|
||||||
parser = argparse.ArgumentParser(description='LangBot')
|
|
||||||
parser.add_argument(
|
|
||||||
'--standalone-runtime',
|
|
||||||
action='store_true',
|
|
||||||
help='Use standalone plugin runtime / 使用独立插件运行时',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
parser.add_argument('--debug', action='store_true', help='Debug mode / 调试模式', default=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.standalone_runtime:
|
|
||||||
from pkg.utils import platform
|
|
||||||
|
|
||||||
platform.standalone_runtime = True
|
|
||||||
|
|
||||||
if args.debug:
|
|
||||||
from pkg.utils import constants
|
|
||||||
|
|
||||||
constants.debug_mode = True
|
|
||||||
|
|
||||||
print(asciiart)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
|
|
||||||
from pkg.core.bootutils import deps
|
|
||||||
|
|
||||||
missing_deps = await deps.check_deps()
|
|
||||||
|
|
||||||
if missing_deps:
|
|
||||||
print('以下依赖包未安装,将自动安装,请完成后重启程序:')
|
|
||||||
print(
|
|
||||||
'These dependencies are missing, they will be installed automatically, please restart the program after completion:'
|
|
||||||
)
|
|
||||||
for dep in missing_deps:
|
|
||||||
print('-', dep)
|
|
||||||
await deps.install_deps(missing_deps)
|
|
||||||
print('已自动安装缺失的依赖包,请重启程序。')
|
|
||||||
print('The missing dependencies have been installed automatically, please restart the program.')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
|
||||||
# import pydantic.version
|
|
||||||
|
|
||||||
# if pydantic.version.VERSION < '2.0':
|
|
||||||
# import pydantic
|
|
||||||
|
|
||||||
# sys.modules['pydantic.v1'] = pydantic
|
|
||||||
|
|
||||||
# 检查配置文件
|
|
||||||
|
|
||||||
from pkg.core.bootutils import files
|
|
||||||
|
|
||||||
generated_files = await files.generate_files()
|
|
||||||
|
|
||||||
if generated_files:
|
|
||||||
print('以下文件不存在,已自动生成:')
|
|
||||||
print('Following files do not exist and have been automatically generated:')
|
|
||||||
for file in generated_files:
|
|
||||||
print('-', file)
|
|
||||||
|
|
||||||
from pkg.core import boot
|
|
||||||
|
|
||||||
await boot.main(loop)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 必须大于 3.10.1
|
|
||||||
if sys.version_info < (3, 10, 1):
|
|
||||||
print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version)
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Your Python version is not supported. Please exit the program by pressing any key.')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Check if the current directory is the LangBot project root directory
|
|
||||||
invalid_pwd = False
|
|
||||||
|
|
||||||
if not os.path.exists('main.py'):
|
|
||||||
invalid_pwd = True
|
|
||||||
else:
|
|
||||||
with open('main.py', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
if 'LangBot/main.py' not in content:
|
|
||||||
invalid_pwd = True
|
|
||||||
if invalid_pwd:
|
|
||||||
print('请在 LangBot 项目根目录下以命令形式运行此程序。')
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Please run this program in the LangBot project root directory in command form.')
|
|
||||||
print('Press any key to exit...')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
|
|
||||||
loop.run_until_complete(main_entry(loop))
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
import mimetypes
|
|
||||||
import uuid
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import quart.datastructures
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('files', '/api/v1/files')
|
|
||||||
class FilesRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/image/<image_key>', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _(image_key: str) -> quart.Response:
|
|
||||||
if '/' in image_key or '\\' in image_key:
|
|
||||||
return quart.Response(status=404)
|
|
||||||
|
|
||||||
if not await self.ap.storage_mgr.storage_provider.exists(image_key):
|
|
||||||
return quart.Response(status=404)
|
|
||||||
|
|
||||||
image_bytes = await self.ap.storage_mgr.storage_provider.load(image_key)
|
|
||||||
mime_type = mimetypes.guess_type(image_key)[0]
|
|
||||||
if mime_type is None:
|
|
||||||
mime_type = 'image/jpeg'
|
|
||||||
|
|
||||||
return quart.Response(image_bytes, mimetype=mime_type)
|
|
||||||
|
|
||||||
@self.route('/documents', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> quart.Response:
|
|
||||||
request = quart.request
|
|
||||||
# get file bytes from 'file'
|
|
||||||
file = (await request.files)['file']
|
|
||||||
assert isinstance(file, quart.datastructures.FileStorage)
|
|
||||||
|
|
||||||
file_bytes = await asyncio.to_thread(file.stream.read)
|
|
||||||
extension = file.filename.split('.')[-1]
|
|
||||||
file_name = file.filename.split('.')[0]
|
|
||||||
|
|
||||||
# check if file name contains '/' or '\'
|
|
||||||
if '/' in file_name or '\\' in file_name:
|
|
||||||
return self.fail(400, 'File name contains invalid characters')
|
|
||||||
|
|
||||||
file_key = file_name + '_' + str(uuid.uuid4())[:8] + '.' + extension
|
|
||||||
# save file to storage
|
|
||||||
await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes)
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'file_id': file_key,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('pipelines', '/api/v1/pipelines')
|
|
||||||
class PipelinesRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
sort_by = quart.request.args.get('sort_by', 'created_at')
|
|
||||||
sort_order = quart.request.args.get('sort_order', 'DESC')
|
|
||||||
return self.success(
|
|
||||||
data={'pipelines': await self.ap.pipeline_service.get_pipelines(sort_by, sort_order)}
|
|
||||||
)
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
pipeline_uuid = await self.ap.pipeline_service.create_pipeline(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': pipeline_uuid})
|
|
||||||
|
|
||||||
@self.route('/_/metadata', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()})
|
|
||||||
|
|
||||||
@self.route('/<pipeline_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(pipeline_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return self.http_status(404, -1, 'pipeline not found')
|
|
||||||
|
|
||||||
return self.success(data={'pipeline': pipeline})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.pipeline_service.delete_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
|
|
||||||
class WebChatDebugRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/send', methods=['POST'])
|
|
||||||
async def send_message(pipeline_uuid: str) -> str:
|
|
||||||
"""Send a message to the pipeline for debugging"""
|
|
||||||
|
|
||||||
async def stream_generator(generator):
|
|
||||||
yield 'data: {"type": "start"}\n\n'
|
|
||||||
async for message in generator:
|
|
||||||
yield f'data: {json.dumps({"message": message})}\n\n'
|
|
||||||
yield 'data: {"type": "end"}\n\n'
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = await quart.request.get_json()
|
|
||||||
session_type = data.get('session_type', 'person')
|
|
||||||
message_chain_obj = data.get('message', [])
|
|
||||||
is_stream = data.get('is_stream', False)
|
|
||||||
|
|
||||||
if not message_chain_obj:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
if is_stream:
|
|
||||||
generator = webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj, is_stream
|
|
||||||
)
|
|
||||||
# 设置正确的响应头
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Transfer-Encoding': 'chunked',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
}
|
|
||||||
return quart.Response(stream_generator(generator), mimetype='text/event-stream', headers=headers)
|
|
||||||
|
|
||||||
else: # non-stream
|
|
||||||
result = None
|
|
||||||
async for message in webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj
|
|
||||||
):
|
|
||||||
result = message
|
|
||||||
if result is not None:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'message': result,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/messages/<session_type>', methods=['GET'])
|
|
||||||
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
|
|
||||||
"""Get the message history of the pipeline for debugging"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
|
|
||||||
|
|
||||||
return self.success(data={'messages': messages})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/reset/<session_type>', methods=['POST'])
|
|
||||||
async def reset_session(session_type: str) -> str:
|
|
||||||
"""Reset the debug session"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = None
|
|
||||||
for bot in self.ap.platform_mgr.bots:
|
|
||||||
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
|
|
||||||
webchat_adapter = bot.adapter
|
|
||||||
break
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
webchat_adapter.reset_debug_session(session_type)
|
|
||||||
|
|
||||||
return self.success(data={'message': 'Session reset successfully'})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('adapters', '/api/v1/platform/adapters')
|
|
||||||
class AdaptersRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'adapters': self.ap.platform_mgr.get_available_adapters_info()})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>', methods=['GET'])
|
|
||||||
async def _(adapter_name: str) -> str:
|
|
||||||
adapter_info = self.ap.platform_mgr.get_available_adapter_info_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_info is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
return self.success(data={'adapter': adapter_info})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>/icon', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _(adapter_name: str) -> quart.Response:
|
|
||||||
adapter_manifest = self.ap.platform_mgr.get_available_adapter_manifest_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_manifest is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
icon_path = adapter_manifest.icon_rel_path
|
|
||||||
|
|
||||||
if icon_path is None:
|
|
||||||
return self.http_status(404, -1, 'icon not found')
|
|
||||||
|
|
||||||
return await quart.send_file(icon_path)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('bots', '/api/v1/platform/bots')
|
|
||||||
class BotsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'bots': await self.ap.bot_service.get_bots()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
bot_uuid = await self.ap.bot_service.create_bot(json_data)
|
|
||||||
return self.success(data={'uuid': bot_uuid})
|
|
||||||
|
|
||||||
@self.route('/<bot_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(bot_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
bot = await self.ap.bot_service.get_bot(bot_uuid)
|
|
||||||
if bot is None:
|
|
||||||
return self.http_status(404, -1, 'bot not found')
|
|
||||||
return self.success(data={'bot': bot})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
await self.ap.bot_service.update_bot(bot_uuid, json_data)
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.bot_service.delete_bot(bot_uuid)
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<bot_uuid>/logs', methods=['POST'])
|
|
||||||
async def _(bot_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
from_index = json_data.get('from_index', -1)
|
|
||||||
max_count = json_data.get('max_count', 10)
|
|
||||||
logs, total_count = await self.ap.bot_service.list_event_logs(bot_uuid, from_index, max_count)
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'logs': logs,
|
|
||||||
'total_count': total_count,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from .....core import taskmgr
|
|
||||||
from .. import group
|
|
||||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('plugins', '/api/v1/plugins')
|
|
||||||
class PluginsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
plugins = await self.ap.plugin_connector.list_plugins()
|
|
||||||
|
|
||||||
return self.success(data={'plugins': plugins})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/upgrade',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.upgrade_plugin(author, plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-upgrade-{plugin_name}',
|
|
||||||
label=f'Upgrading plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>',
|
|
||||||
methods=['GET', 'DELETE'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
return self.success(data={'plugin': plugin})
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.delete_plugin(author, plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-remove-{plugin_name}',
|
|
||||||
label=f'Removing plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/config',
|
|
||||||
methods=['GET', 'PUT'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'config': plugin['plugin_config']})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.plugin_connector.set_plugin_config(author, plugin_name, data)
|
|
||||||
|
|
||||||
return self.success(data={})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/icon',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.NONE,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
|
||||||
icon_base64 = icon_data['plugin_icon_base64']
|
|
||||||
mime_type = icon_data['mime_type']
|
|
||||||
|
|
||||||
icon_data = base64.b64decode(icon_base64)
|
|
||||||
|
|
||||||
return quart.Response(icon_data, mimetype=mime_type)
|
|
||||||
|
|
||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
short_source_str = data['source'][-8:]
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-github',
|
|
||||||
label=f'Installing plugin from github ...{short_source_str}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/install/marketplace', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-marketplace',
|
|
||||||
label=f'Installing plugin from marketplace ...{data}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
file = (await quart.request.files).get('file')
|
|
||||||
if file is None:
|
|
||||||
return self.http_status(400, -1, 'file is required')
|
|
||||||
|
|
||||||
file_bytes = file.read()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'plugin_file': file_bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-local',
|
|
||||||
label=f'Installing plugin from local ...{file.filename}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
|
||||||
class LLMModelsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'models': await self.ap.llm_model_service.get_llm_models()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
model_uuid = await self.ap.llm_model_service.create_llm_model(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': model_uuid})
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
model = await self.ap.llm_model_service.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return self.http_status(404, -1, 'model not found')
|
|
||||||
|
|
||||||
return self.success(data={'model': model})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.llm_model_service.update_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.llm_model_service.delete_llm_model(model_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>/test', methods=['POST'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.llm_model_service.test_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/embedding', '/api/v1/provider/models/embedding')
|
|
||||||
class EmbeddingModelsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'models': await self.ap.embedding_models_service.get_embedding_models()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
model_uuid = await self.ap.embedding_models_service.create_embedding_model(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': model_uuid})
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
model = await self.ap.embedding_models_service.get_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return self.http_status(404, -1, 'model not found')
|
|
||||||
|
|
||||||
return self.success(data={'model': model})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.embedding_models_service.update_embedding_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.embedding_models_service.delete_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>/test', methods=['POST'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.embedding_models_service.test_embedding_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
from .....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('system', '/api/v1/system')
|
|
||||||
class SystemRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'version': constants.semantic_version,
|
|
||||||
'debug': constants.debug_mode,
|
|
||||||
'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()),
|
|
||||||
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'enable_marketplace', True
|
|
||||||
),
|
|
||||||
'cloud_service_url': (
|
|
||||||
self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'cloud_service_url', 'https://space.langbot.app'
|
|
||||||
)
|
|
||||||
if 'cloud_service_url' in self.ap.instance_config.data.get('plugin', {})
|
|
||||||
else 'https://space.langbot.app'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
task_type = quart.request.args.get('type')
|
|
||||||
|
|
||||||
if task_type == '':
|
|
||||||
task_type = None
|
|
||||||
|
|
||||||
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type))
|
|
||||||
|
|
||||||
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(task_id: str) -> str:
|
|
||||||
task = self.ap.task_mgr.get_task_by_id(int(task_id))
|
|
||||||
|
|
||||||
if task is None:
|
|
||||||
return self.http_status(404, 404, 'Task not found')
|
|
||||||
|
|
||||||
return self.success(data=task.to_dict())
|
|
||||||
|
|
||||||
@self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
py_code = await quart.request.data
|
|
||||||
|
|
||||||
ap = self.ap
|
|
||||||
|
|
||||||
return self.success(data=exec(py_code, {'ap': ap}))
|
|
||||||
|
|
||||||
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/debug/plugin/action',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
class AnoymousAction:
|
|
||||||
value = 'anonymous_action'
|
|
||||||
|
|
||||||
def __init__(self, value: str):
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
resp = await self.ap.plugin_connector.handler.call_action(
|
|
||||||
AnoymousAction(data['action']),
|
|
||||||
data['data'],
|
|
||||||
timeout=data.get('timeout', 10),
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data=resp)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/status/plugin-system',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
plugin_connector_error = 'ok'
|
|
||||||
is_connected = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.plugin_connector.ping_plugin_runtime()
|
|
||||||
except Exception as e:
|
|
||||||
plugin_connector_error = str(e)
|
|
||||||
is_connected = False
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'is_enable': self.ap.plugin_connector.is_enable_plugin,
|
|
||||||
'is_connected': is_connected,
|
|
||||||
'plugin_connector_error': plugin_connector_error,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import quart
|
|
||||||
import argon2
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('user', '/api/v1/user')
|
|
||||||
class UserRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
|
|
||||||
|
|
||||||
if await self.ap.user_service.is_initialized():
|
|
||||||
return self.fail(1, 'System already initialized')
|
|
||||||
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
password = json_data['password']
|
|
||||||
|
|
||||||
await self.ap.user_service.create_user(user_email, password)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.fail(1, 'Invalid username or password')
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
token = await self.ap.user_service.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/reset-password', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
recovery_key = json_data['recovery_key']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
# hard sleep 3s for security
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
if not await self.ap.user_service.is_initialized():
|
|
||||||
return self.http_status(400, -1, 'System not initialized')
|
|
||||||
|
|
||||||
user_obj = await self.ap.user_service.get_user_by_email(user_email)
|
|
||||||
|
|
||||||
if user_obj is None:
|
|
||||||
return self.http_status(400, -1, 'User not found')
|
|
||||||
|
|
||||||
if recovery_key != self.ap.instance_config.data['system']['recovery_key']:
|
|
||||||
return self.http_status(403, -1, 'Invalid recovery key')
|
|
||||||
|
|
||||||
await self.ap.user_service.reset_password(user_email, new_password)
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
|
|
||||||
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
current_password = json_data['current_password']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.user_service.change_password(user_email, current_password, new_password)
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.http_status(400, -1, 'Current password is incorrect')
|
|
||||||
except ValueError as e:
|
|
||||||
return self.http_status(400, -1, str(e))
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import rag as persistence_rag
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeService:
|
|
||||||
"""知识库服务"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_knowledge_bases(self) -> list[dict]:
|
|
||||||
"""获取所有知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
|
||||||
knowledge_bases = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
for knowledge_base in knowledge_bases
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
|
|
||||||
"""获取知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
knowledge_base = result.first()
|
|
||||||
if knowledge_base is None:
|
|
||||||
return None
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
|
|
||||||
async def create_knowledge_base(self, kb_data: dict) -> str:
|
|
||||||
"""创建知识库"""
|
|
||||||
kb_data['uuid'] = str(uuid.uuid4())
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
return kb_data['uuid']
|
|
||||||
|
|
||||||
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
|
||||||
"""更新知识库"""
|
|
||||||
if 'uuid' in kb_data:
|
|
||||||
del kb_data['uuid']
|
|
||||||
|
|
||||||
if 'embedding_model_uuid' in kb_data:
|
|
||||||
del kb_data['embedding_model_uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
|
||||||
.values(kb_data)
|
|
||||||
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
async def store_file(self, kb_uuid: str, file_id: str) -> int:
|
|
||||||
"""存储文件"""
|
|
||||||
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
|
|
||||||
# await self.ap.rag_mgr.store_file(file_id)
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return await runtime_kb.store_file(file_id)
|
|
||||||
|
|
||||||
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
|
||||||
"""检索知识库"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return [
|
|
||||||
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
|
||||||
"""获取知识库文件"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
files = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_rag.File, file) for file in files]
|
|
||||||
|
|
||||||
async def delete_file(self, kb_uuid: str, file_id: str) -> None:
|
|
||||||
"""删除文件"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
await runtime_kb.delete_file(file_id)
|
|
||||||
|
|
||||||
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
|
||||||
"""删除知识库"""
|
|
||||||
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
# delete files
|
|
||||||
files = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
for file in files:
|
|
||||||
# delete chunks
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.Chunk).where(persistence_rag.Chunk.file_id == file.uuid)
|
|
||||||
)
|
|
||||||
# delete file
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
|
|
||||||
)
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import model as persistence_model
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
from ....provider.modelmgr import requester as model_requester
|
|
||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
||||||
|
|
||||||
|
|
||||||
class LLMModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
|
|
||||||
masked_columns = []
|
|
||||||
if not include_secret:
|
|
||||||
masked_columns = ['api_keys']
|
|
||||||
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns)
|
|
||||||
for model in models
|
|
||||||
]
|
|
||||||
|
|
||||||
async def create_llm_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_model.LLMModel).values(**model_data))
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
# check if default pipeline has no model bound
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
|
||||||
pipeline_config = pipeline.config
|
|
||||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
|
||||||
pipeline_data = {'config': pipeline_config}
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_llm_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
|
|
||||||
|
|
||||||
async def update_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.LLMModel)
|
|
||||||
.where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
async def delete_llm_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.llm_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_llm_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_llm_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
|
|
||||||
|
|
||||||
await runtime_llm_model.requester.invoke_llm(
|
|
||||||
query=None,
|
|
||||||
model=runtime_llm_model,
|
|
||||||
messages=[provider_message.Message(role='user', content='Hello, world!')],
|
|
||||||
funcs=[],
|
|
||||||
extra_args=model_data.get('extra_args', {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_embedding_models(self) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model) for model in models]
|
|
||||||
|
|
||||||
async def create_embedding_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_model.EmbeddingModel).values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_embedding_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
|
|
||||||
|
|
||||||
async def update_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.EmbeddingModel)
|
|
||||||
.where(persistence_model.EmbeddingModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
async def delete_embedding_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_embedding_model: model_requester.RuntimeEmbeddingModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.embedding_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_embedding_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_embedding_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_embedding_model = await self.ap.model_mgr.init_runtime_embedding_model(model_data)
|
|
||||||
|
|
||||||
await runtime_embedding_model.requester.invoke_embedding(
|
|
||||||
model=runtime_embedding_model,
|
|
||||||
input_text=['Hello, world!'],
|
|
||||||
extra_args={},
|
|
||||||
)
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
default_stage_order = [
|
|
||||||
'GroupRespondRuleCheckStage', # 群响应规则检查
|
|
||||||
'BanSessionCheckStage', # 封禁会话检查
|
|
||||||
'PreContentFilterStage', # 内容过滤前置阶段
|
|
||||||
'PreProcessor', # 预处理器
|
|
||||||
'ConversationMessageTruncator', # 会话消息截断器
|
|
||||||
'RequireRateLimitOccupancy', # 请求速率限制占用
|
|
||||||
'MessageProcessor', # 处理器
|
|
||||||
'ReleaseRateLimitOccupancy', # 释放速率限制占用
|
|
||||||
'PostContentFilterStage', # 内容过滤后置阶段
|
|
||||||
'ResponseWrapper', # 响应包装器
|
|
||||||
'LongTextProcessStage', # 长文本处理
|
|
||||||
'SendResponseBackStage', # 发送响应
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_pipeline_metadata(self) -> dict:
|
|
||||||
return [
|
|
||||||
self.ap.pipeline_config_meta_trigger.data,
|
|
||||||
self.ap.pipeline_config_meta_safety.data,
|
|
||||||
self.ap.pipeline_config_meta_ai.data,
|
|
||||||
self.ap.pipeline_config_meta_output.data,
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipelines(self, sort_by: str = 'created_at', sort_order: str = 'DESC') -> list[dict]:
|
|
||||||
query = sqlalchemy.select(persistence_pipeline.LegacyPipeline)
|
|
||||||
|
|
||||||
if sort_by == 'created_at':
|
|
||||||
if sort_order == 'DESC':
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.desc())
|
|
||||||
else:
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.created_at.asc())
|
|
||||||
elif sort_by == 'updated_at':
|
|
||||||
if sort_order == 'DESC':
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.desc())
|
|
||||||
else:
|
|
||||||
query = query.order_by(persistence_pipeline.LegacyPipeline.updated_at.asc())
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(query)
|
|
||||||
pipelines = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
for pipeline in pipelines
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipeline(self, pipeline_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = result.first()
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
|
||||||
pipeline_data['uuid'] = str(uuid.uuid4())
|
|
||||||
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
|
||||||
pipeline_data['stages'] = default_stage_order.copy()
|
|
||||||
pipeline_data['is_default'] = default
|
|
||||||
pipeline_data['config'] = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
return pipeline_data['uuid']
|
|
||||||
|
|
||||||
async def update_pipeline(self, pipeline_uuid: str, pipeline_data: dict) -> None:
|
|
||||||
if 'uuid' in pipeline_data:
|
|
||||||
del pipeline_data['uuid']
|
|
||||||
if 'for_version' in pipeline_data:
|
|
||||||
del pipeline_data['for_version']
|
|
||||||
if 'stages' in pipeline_data:
|
|
||||||
del pipeline_data['stages']
|
|
||||||
if 'is_default' in pipeline_data:
|
|
||||||
del pipeline_data['is_default']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid)
|
|
||||||
.values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if 'name' in pipeline_data:
|
|
||||||
from ....entity.persistence import bot as persistence_bot
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
for bot in bots:
|
|
||||||
bot_data = {'use_pipeline_name': pipeline_data['name']}
|
|
||||||
await self.ap.bot_service.update_bot(bot.uuid, bot_data)
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
# update all conversation that use this pipeline
|
|
||||||
for session in self.ap.sess_mgr.session_list:
|
|
||||||
if session.using_conversation is not None and session.using_conversation.pipeline_uuid == pipeline_uuid:
|
|
||||||
session.using_conversation = None
|
|
||||||
|
|
||||||
async def delete_pipeline(self, pipeline_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
import argon2
|
|
||||||
import jwt
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import user
|
|
||||||
from ....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def is_initialized(self) -> bool:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list is not None and len(result_list) > 0
|
|
||||||
|
|
||||||
async def create_user(self, user_email: str, password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_user_by_email(self, user_email: str) -> user.User | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list[0] if result_list is not None and len(result_list) > 0 else None
|
|
||||||
|
|
||||||
async def authenticate(self, user_email: str, password: str) -> str | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
|
|
||||||
if result_list is None or len(result_list) == 0:
|
|
||||||
raise ValueError('用户不存在')
|
|
||||||
|
|
||||||
user_obj = result_list[0]
|
|
||||||
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, password)
|
|
||||||
|
|
||||||
return await self.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
async def generate_jwt_token(self, user_email: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
jwt_expire = self.ap.instance_config.data['system']['jwt']['expire']
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
'user': user_email,
|
|
||||||
'iss': 'LangBot-' + constants.edition,
|
|
||||||
'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire),
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt.encode(payload, jwt_secret, algorithm='HS256')
|
|
||||||
|
|
||||||
async def verify_jwt_token(self, token: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
|
|
||||||
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
|
|
||||||
|
|
||||||
async def reset_password(self, user_email: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
user_obj = await self.get_user_by_email(user_email)
|
|
||||||
if user_obj is None:
|
|
||||||
raise ValueError('User not found')
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, current_password)
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
199
pkg/core/app.py
199
pkg/core/app.py
@@ -1,199 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..platform import botmgr as im_mgr
|
|
||||||
from ..provider.session import sessionmgr as llm_session_mgr
|
|
||||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
|
||||||
from ..provider.tools import toolmgr as llm_tool_mgr
|
|
||||||
from ..config import manager as config_mgr
|
|
||||||
from ..command import cmdmgr
|
|
||||||
from ..plugin import connector as plugin_connector
|
|
||||||
from ..pipeline import pool
|
|
||||||
from ..pipeline import controller, pipelinemgr
|
|
||||||
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
|
|
||||||
from ..persistence import mgr as persistencemgr
|
|
||||||
from ..api.http.controller import main as http_controller
|
|
||||||
from ..api.http.service import user as user_service
|
|
||||||
from ..api.http.service import model as model_service
|
|
||||||
from ..api.http.service import pipeline as pipeline_service
|
|
||||||
from ..api.http.service import bot as bot_service
|
|
||||||
from ..api.http.service import knowledge as knowledge_service
|
|
||||||
from ..discover import engine as discover_engine
|
|
||||||
from ..storage import mgr as storagemgr
|
|
||||||
from ..utils import logcache
|
|
||||||
from . import taskmgr
|
|
||||||
from . import entities as core_entities
|
|
||||||
from ..rag.knowledge import kbmgr as rag_mgr
|
|
||||||
from ..vector import mgr as vectordb_mgr
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
"""Runtime application object and context"""
|
|
||||||
|
|
||||||
event_loop: asyncio.AbstractEventLoop = None
|
|
||||||
|
|
||||||
# asyncio_tasks: list[asyncio.Task] = []
|
|
||||||
task_mgr: taskmgr.AsyncTaskManager = None
|
|
||||||
|
|
||||||
discover: discover_engine.ComponentDiscoveryEngine = None
|
|
||||||
|
|
||||||
platform_mgr: im_mgr.PlatformManager = None
|
|
||||||
|
|
||||||
cmd_mgr: cmdmgr.CommandManager = None
|
|
||||||
|
|
||||||
sess_mgr: llm_session_mgr.SessionManager = None
|
|
||||||
|
|
||||||
model_mgr: llm_model_mgr.ModelManager = None
|
|
||||||
|
|
||||||
rag_mgr: rag_mgr.RAGManager = None
|
|
||||||
|
|
||||||
# TODO move to pipeline
|
|
||||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
|
||||||
|
|
||||||
# ======= Config manager =======
|
|
||||||
|
|
||||||
command_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
pipeline_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
platform_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
provider_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
system_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
instance_config: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# ======= Metadata config manager =======
|
|
||||||
|
|
||||||
sensitive_meta: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
pipeline_config_meta_trigger: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_safety: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_ai: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_output: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
plugin_connector: plugin_connector.PluginRuntimeConnector = None
|
|
||||||
|
|
||||||
query_pool: pool.QueryPool = None
|
|
||||||
|
|
||||||
ctrl: controller.Controller = None
|
|
||||||
|
|
||||||
pipeline_mgr: pipelinemgr.PipelineManager = None
|
|
||||||
|
|
||||||
ver_mgr: version_mgr.VersionManager = None
|
|
||||||
|
|
||||||
ann_mgr: announce_mgr.AnnouncementManager = None
|
|
||||||
|
|
||||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
|
||||||
|
|
||||||
logger: logging.Logger = None
|
|
||||||
|
|
||||||
persistence_mgr: persistencemgr.PersistenceManager = None
|
|
||||||
|
|
||||||
vector_db_mgr: vectordb_mgr.VectorDBManager = None
|
|
||||||
|
|
||||||
http_ctrl: http_controller.HTTPController = None
|
|
||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
|
||||||
|
|
||||||
storage_mgr: storagemgr.StorageMgr = None
|
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
|
||||||
|
|
||||||
llm_model_service: model_service.LLMModelsService = None
|
|
||||||
|
|
||||||
embedding_models_service: model_service.EmbeddingModelsService = None
|
|
||||||
|
|
||||||
pipeline_service: pipeline_service.PipelineService = None
|
|
||||||
|
|
||||||
bot_service: bot_service.BotService = None
|
|
||||||
|
|
||||||
knowledge_service: knowledge_service.KnowledgeService = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
try:
|
|
||||||
await self.plugin_connector.initialize_plugins()
|
|
||||||
|
|
||||||
# 后续可能会允许动态重启其他任务
|
|
||||||
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
|
|
||||||
async def never_ending():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.platform_mgr.run(),
|
|
||||||
name='platform-manager',
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.ctrl.run(),
|
|
||||||
name='query-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.http_ctrl.run(),
|
|
||||||
name='http-api-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
never_ending(),
|
|
||||||
name='never-ending-task',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.print_web_access_info()
|
|
||||||
await self.task_mgr.wait_all()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'Application runtime fatal exception: {e}')
|
|
||||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
def dispose(self):
|
|
||||||
self.plugin_connector.dispose()
|
|
||||||
|
|
||||||
async def print_web_access_info(self):
|
|
||||||
"""Print access webui tips"""
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join('.', 'web/out')):
|
|
||||||
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh')
|
|
||||||
self.logger.warning(
|
|
||||||
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
host_ip = '127.0.0.1'
|
|
||||||
|
|
||||||
port = self.instance_config.data['api']['port']
|
|
||||||
|
|
||||||
tips = f"""
|
|
||||||
=======================================
|
|
||||||
✨ Access WebUI / 访问管理面板
|
|
||||||
|
|
||||||
🏠 Local Address: http://{host_ip}:{port}/
|
|
||||||
🌐 Public Address: http://<Your Public IP>:{port}/
|
|
||||||
|
|
||||||
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
|
||||||
=======================================
|
|
||||||
""".strip()
|
|
||||||
for line in tips.split('\n'):
|
|
||||||
self.logger.info(line)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('msg-truncator-cfg-migration', 9)
|
|
||||||
class MsgTruncatorConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'msg-truncate' not in self.ap.pipeline_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.pipeline_cfg.data['msg-truncate'] = {
|
|
||||||
'method': 'round',
|
|
||||||
'round': {'max-round': 10},
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.pipeline_cfg.dump_config()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .. import stage, app
|
|
||||||
from ..bootutils import config
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('LoadConfigStage')
|
|
||||||
class LoadConfigStage(stage.BootingStage):
|
|
||||||
"""Load config file stage"""
|
|
||||||
|
|
||||||
async def run(self, ap: app.Application):
|
|
||||||
"""Load config file"""
|
|
||||||
|
|
||||||
# ======= deprecated =======
|
|
||||||
if os.path.exists('data/config/command.json'):
|
|
||||||
ap.command_cfg = await config.load_json_config(
|
|
||||||
'data/config/command.json',
|
|
||||||
'templates/legacy/command.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/pipeline.json'):
|
|
||||||
ap.pipeline_cfg = await config.load_json_config(
|
|
||||||
'data/config/pipeline.json',
|
|
||||||
'templates/legacy/pipeline.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/platform.json'):
|
|
||||||
ap.platform_cfg = await config.load_json_config(
|
|
||||||
'data/config/platform.json',
|
|
||||||
'templates/legacy/platform.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/provider.json'):
|
|
||||||
ap.provider_cfg = await config.load_json_config(
|
|
||||||
'data/config/provider.json',
|
|
||||||
'templates/legacy/provider.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists('data/config/system.json'):
|
|
||||||
ap.system_cfg = await config.load_json_config(
|
|
||||||
'data/config/system.json',
|
|
||||||
'templates/legacy/system.json',
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ======= deprecated =======
|
|
||||||
|
|
||||||
ap.instance_config = await config.load_yaml_config(
|
|
||||||
'data/config.yaml', 'templates/config.yaml', completion=False
|
|
||||||
)
|
|
||||||
await ap.instance_config.dump_config()
|
|
||||||
|
|
||||||
ap.sensitive_meta = await config.load_json_config(
|
|
||||||
'data/metadata/sensitive-words.json',
|
|
||||||
'templates/metadata/sensitive-words.json',
|
|
||||||
)
|
|
||||||
await ap.sensitive_meta.dump_config()
|
|
||||||
|
|
||||||
ap.pipeline_config_meta_trigger = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/trigger.yaml',
|
|
||||||
'templates/metadata/pipeline/trigger.yaml',
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_safety = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/safety.yaml',
|
|
||||||
'templates/metadata/pipeline/safety.yaml',
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_ai = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/ai.yaml', 'templates/metadata/pipeline/ai.yaml'
|
|
||||||
)
|
|
||||||
ap.pipeline_config_meta_output = await config.load_yaml_config(
|
|
||||||
'templates/metadata/pipeline/output.yaml',
|
|
||||||
'templates/metadata/pipeline/output.yaml',
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from .base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class LLMModel(Base):
|
|
||||||
"""LLM model"""
|
|
||||||
|
|
||||||
__tablename__ = 'llm_models'
|
|
||||||
|
|
||||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
|
||||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
|
|
||||||
abilities = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[])
|
|
||||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModel(Base):
|
|
||||||
"""Embedding 模型"""
|
|
||||||
|
|
||||||
__tablename__ = 'embedding_models'
|
|
||||||
|
|
||||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
|
||||||
name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
description = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
requester_config = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
api_keys = sqlalchemy.Column(sqlalchemy.JSON, nullable=False)
|
|
||||||
extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={})
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from .base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
__tablename__ = 'users'
|
|
||||||
|
|
||||||
id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
|
|
||||||
user = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
password = sqlalchemy.Column(sqlalchemy.String(255), nullable=False)
|
|
||||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now())
|
|
||||||
updated_at = sqlalchemy.Column(
|
|
||||||
sqlalchemy.DateTime,
|
|
||||||
nullable=False,
|
|
||||||
server_default=sqlalchemy.func.now(),
|
|
||||||
onupdate=sqlalchemy.func.now(),
|
|
||||||
)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveResultEntry(pydantic.BaseModel):
|
|
||||||
id: str
|
|
||||||
|
|
||||||
metadata: dict[str, Any]
|
|
||||||
|
|
||||||
distance: float
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(2)
|
|
||||||
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
|
||||||
"""Combine quote message config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'misc' not in config['trigger']:
|
|
||||||
config['trigger']['misc'] = {}
|
|
||||||
|
|
||||||
if 'combine-quote-message' not in config['trigger']['misc']:
|
|
||||||
config['trigger']['misc']['combine-quote-message'] = False
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(3)
|
|
||||||
class DBMigrateN8nConfig(migration.DBMigration):
|
|
||||||
"""N8n config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'n8n-service-api' not in config['ai']:
|
|
||||||
config['ai']['n8n-service-api'] = {
|
|
||||||
'webhook-url': 'http://your-n8n-webhook-url',
|
|
||||||
'auth-type': 'none',
|
|
||||||
'basic-username': '',
|
|
||||||
'basic-password': '',
|
|
||||||
'jwt-secret': '',
|
|
||||||
'jwt-algorithm': 'HS256',
|
|
||||||
'header-name': '',
|
|
||||||
'header-value': '',
|
|
||||||
'timeout': 120,
|
|
||||||
'output-key': 'response',
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(4)
|
|
||||||
class DBMigrateRAGKBUUID(migration.DBMigration):
|
|
||||||
"""RAG知识库UUID"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""升级"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'knowledge-base' not in config['ai']['local-agent']:
|
|
||||||
config['ai']['local-agent']['knowledge-base'] = ''
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""降级"""
|
|
||||||
pass
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(5)
|
|
||||||
class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
|
|
||||||
"""Pipeline remove cot config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'remove-think' not in config['output']['misc']:
|
|
||||||
config['output']['misc']['remove-think'] = False
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(6)
|
|
||||||
class DBMigrateLangflowApiConfig(migration.DBMigration):
|
|
||||||
"""Langflow API config"""
|
|
||||||
|
|
||||||
async def upgrade(self):
|
|
||||||
"""Upgrade"""
|
|
||||||
# read all pipelines
|
|
||||||
pipelines = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
for pipeline in pipelines:
|
|
||||||
serialized_pipeline = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
config = serialized_pipeline['config']
|
|
||||||
|
|
||||||
if 'langflow-api' not in config['ai']:
|
|
||||||
config['ai']['langflow-api'] = {
|
|
||||||
'base-url': 'http://localhost:7860',
|
|
||||||
'api-key': 'your-api-key',
|
|
||||||
'flow-id': 'your-flow-id',
|
|
||||||
'input-type': 'chat',
|
|
||||||
'output-type': 'chat',
|
|
||||||
'tweaks': '{}',
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == serialized_pipeline['uuid'])
|
|
||||||
.values(
|
|
||||||
{
|
|
||||||
'config': config,
|
|
||||||
'for_version': self.ap.ver_mgr.get_current_version(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def downgrade(self):
|
|
||||||
"""Downgrade"""
|
|
||||||
pass
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from .. import entities
|
|
||||||
from .. import filter as filter_model
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
BAIDU_EXAMINE_URL = 'https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token={}'
|
|
||||||
BAIDU_EXAMINE_TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token'
|
|
||||||
|
|
||||||
|
|
||||||
@filter_model.filter_class('baidu-cloud-examine')
|
|
||||||
class BaiduCloudExamine(filter_model.ContentFilter):
|
|
||||||
"""百度云内容审核"""
|
|
||||||
|
|
||||||
async def _get_token(self) -> str:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(
|
|
||||||
BAIDU_EXAMINE_TOKEN_URL,
|
|
||||||
params={
|
|
||||||
'grant_type': 'client_credentials',
|
|
||||||
'client_id': self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-key'],
|
|
||||||
'client_secret': self.ap.pipeline_cfg.data['baidu-cloud-examine']['api-secret'],
|
|
||||||
},
|
|
||||||
) as resp:
|
|
||||||
return (await resp.json())['access_token']
|
|
||||||
|
|
||||||
async def process(self, query: pipeline_query.Query, message: str) -> entities.FilterResult:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.post(
|
|
||||||
BAIDU_EXAMINE_URL.format(await self._get_token()),
|
|
||||||
headers={
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
data=f'text={message}'.encode('utf-8'),
|
|
||||||
) as resp:
|
|
||||||
result = await resp.json()
|
|
||||||
|
|
||||||
if 'error_code' in result:
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.BLOCK,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='',
|
|
||||||
console_notice=f'百度云判定出错,错误信息:{result["error_msg"]}',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
conclusion = result['conclusion']
|
|
||||||
|
|
||||||
if conclusion in ('合规'):
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.PASS,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='',
|
|
||||||
console_notice=f'百度云判定结果:{conclusion}',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return entities.FilterResult(
|
|
||||||
level=entities.ResultLevel.BLOCK,
|
|
||||||
replacement=message,
|
|
||||||
user_notice='消息中存在不合适的内容, 请修改',
|
|
||||||
console_notice=f'百度云判定结果:{conclusion}',
|
|
||||||
)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import stage, entities
|
|
||||||
from . import truncator
|
|
||||||
from ...utils import importutil
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
from . import truncators
|
|
||||||
|
|
||||||
importutil.import_modules_in_pkg(truncators)
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('ConversationMessageTruncator')
|
|
||||||
class ConversationMessageTruncator(stage.PipelineStage):
|
|
||||||
"""Conversation message truncator
|
|
||||||
|
|
||||||
Used to truncate the conversation message chain to adapt to the LLM message length limit.
|
|
||||||
"""
|
|
||||||
|
|
||||||
trun: truncator.Truncator
|
|
||||||
|
|
||||||
async def initialize(self, pipeline_config: dict):
|
|
||||||
use_method = 'round'
|
|
||||||
|
|
||||||
for trun in truncator.preregistered_truncators:
|
|
||||||
if trun.name == use_method:
|
|
||||||
self.trun = trun(self.ap)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f'Unknown truncator: {use_method}')
|
|
||||||
|
|
||||||
async def process(self, query: pipeline_query.Query, stage_inst_name: str) -> entities.StageProcessResult:
|
|
||||||
"""处理"""
|
|
||||||
query = await self.trun.truncate(query)
|
|
||||||
|
|
||||||
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
import abc
|
|
||||||
|
|
||||||
from ...core import app
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
preregistered_truncators: list[typing.Type[Truncator]] = []
|
|
||||||
|
|
||||||
|
|
||||||
def truncator_class(
|
|
||||||
name: str,
|
|
||||||
) -> typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]:
|
|
||||||
"""截断器类装饰器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): 截断器名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
typing.Callable[[typing.Type[Truncator]], typing.Type[Truncator]]: 装饰器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(cls: typing.Type[Truncator]) -> typing.Type[Truncator]:
|
|
||||||
assert issubclass(cls, Truncator)
|
|
||||||
|
|
||||||
cls.name = name
|
|
||||||
|
|
||||||
preregistered_truncators.append(cls)
|
|
||||||
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class Truncator(abc.ABC):
|
|
||||||
"""消息截断器基类"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query:
|
|
||||||
"""截断
|
|
||||||
|
|
||||||
一般只需要操作query.messages,也可以扩展操作query.prompt, query.user_message。
|
|
||||||
请勿操作其他字段。
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import truncator
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
@truncator.truncator_class('round')
|
|
||||||
class RoundTruncator(truncator.Truncator):
|
|
||||||
"""Truncate the conversation message chain to adapt to the LLM message length limit."""
|
|
||||||
|
|
||||||
async def truncate(self, query: pipeline_query.Query) -> pipeline_query.Query:
|
|
||||||
"""截断"""
|
|
||||||
max_round = query.pipeline_config['ai']['local-agent']['max-round']
|
|
||||||
|
|
||||||
temp_messages = []
|
|
||||||
|
|
||||||
current_round = 0
|
|
||||||
|
|
||||||
# Traverse from back to front
|
|
||||||
for msg in query.messages[::-1]:
|
|
||||||
if current_round < max_round:
|
|
||||||
temp_messages.append(msg)
|
|
||||||
if msg.role == 'user':
|
|
||||||
current_round += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
query.messages = temp_messages[::-1]
|
|
||||||
|
|
||||||
return query
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from .. import stage, entities
|
|
||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
||||||
import langbot_plugin.api.entities.events as events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('PreProcessor')
|
|
||||||
class PreProcessor(stage.PipelineStage):
|
|
||||||
"""Request pre-processing stage
|
|
||||||
|
|
||||||
Check out session, prompt, context, model, and content functions.
|
|
||||||
|
|
||||||
Rewrite:
|
|
||||||
- session
|
|
||||||
- prompt
|
|
||||||
- messages
|
|
||||||
- user_message
|
|
||||||
- use_model
|
|
||||||
- use_funcs
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def process(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
stage_inst_name: str,
|
|
||||||
) -> entities.StageProcessResult:
|
|
||||||
"""Process"""
|
|
||||||
selected_runner = query.pipeline_config['ai']['runner']['runner']
|
|
||||||
|
|
||||||
session = await self.ap.sess_mgr.get_session(query)
|
|
||||||
|
|
||||||
# When not local-agent, llm_model is None
|
|
||||||
llm_model = (
|
|
||||||
await self.ap.model_mgr.get_model_by_uuid(query.pipeline_config['ai']['local-agent']['model'])
|
|
||||||
if selected_runner == 'local-agent'
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
conversation = await self.ap.sess_mgr.get_conversation(
|
|
||||||
query,
|
|
||||||
session,
|
|
||||||
query.pipeline_config['ai']['local-agent']['prompt'],
|
|
||||||
query.pipeline_uuid,
|
|
||||||
query.bot_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 设置query
|
|
||||||
query.session = session
|
|
||||||
query.prompt = conversation.prompt.copy()
|
|
||||||
query.messages = conversation.messages.copy()
|
|
||||||
|
|
||||||
if selected_runner == 'local-agent':
|
|
||||||
query.use_funcs = []
|
|
||||||
query.use_llm_model_uuid = llm_model.model_entity.uuid
|
|
||||||
|
|
||||||
if llm_model.model_entity.abilities.__contains__('func_call'):
|
|
||||||
query.use_funcs = await self.ap.tool_mgr.get_all_tools()
|
|
||||||
|
|
||||||
variables = {
|
|
||||||
'session_id': f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
|
||||||
'conversation_id': conversation.uuid,
|
|
||||||
'msg_create_time': (
|
|
||||||
int(query.message_event.time) if query.message_event.time else int(datetime.datetime.now().timestamp())
|
|
||||||
),
|
|
||||||
}
|
|
||||||
query.variables.update(variables)
|
|
||||||
|
|
||||||
# Check if this model supports vision, if not, remove all images
|
|
||||||
# TODO this checking should be performed in runner, and in this stage, the image should be reserved
|
|
||||||
if selected_runner == 'local-agent' and not llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
for msg in query.messages:
|
|
||||||
if isinstance(msg.content, list):
|
|
||||||
for me in msg.content:
|
|
||||||
if me.type == 'image_url':
|
|
||||||
msg.content.remove(me)
|
|
||||||
|
|
||||||
content_list: list[provider_message.ContentElement] = []
|
|
||||||
|
|
||||||
plain_text = ''
|
|
||||||
qoute_msg = query.pipeline_config['trigger'].get('misc', '').get('combine-quote-message')
|
|
||||||
|
|
||||||
for me in query.message_chain:
|
|
||||||
if isinstance(me, platform_message.Plain):
|
|
||||||
content_list.append(provider_message.ContentElement.from_text(me.text))
|
|
||||||
plain_text += me.text
|
|
||||||
elif isinstance(me, platform_message.Image):
|
|
||||||
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
if me.base64 is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_image_base64(me.base64))
|
|
||||||
elif isinstance(me, platform_message.File):
|
|
||||||
# if me.url is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_file_url(me.url, me.name))
|
|
||||||
elif isinstance(me, platform_message.Quote) and qoute_msg:
|
|
||||||
for msg in me.origin:
|
|
||||||
if isinstance(msg, platform_message.Plain):
|
|
||||||
content_list.append(provider_message.ContentElement.from_text(msg.text))
|
|
||||||
elif isinstance(msg, platform_message.Image):
|
|
||||||
if selected_runner != 'local-agent' or llm_model.model_entity.abilities.__contains__('vision'):
|
|
||||||
if msg.base64 is not None:
|
|
||||||
content_list.append(provider_message.ContentElement.from_image_base64(msg.base64))
|
|
||||||
|
|
||||||
query.variables['user_message_text'] = plain_text
|
|
||||||
|
|
||||||
query.user_message = provider_message.Message(role='user', content=content_list)
|
|
||||||
# =========== 触发事件 PromptPreProcessing
|
|
||||||
|
|
||||||
event = events.PromptPreProcessing(
|
|
||||||
session_name=f'{query.session.launcher_type.value}_{query.session.launcher_id}',
|
|
||||||
default_prompt=query.prompt.messages,
|
|
||||||
prompt=query.messages,
|
|
||||||
query=query,
|
|
||||||
)
|
|
||||||
|
|
||||||
event_ctx = await self.ap.plugin_connector.emit_event(event)
|
|
||||||
|
|
||||||
query.prompt.messages = event_ctx.event.default_prompt
|
|
||||||
query.messages = event_ctx.event.prompt
|
|
||||||
|
|
||||||
return entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import abc
|
|
||||||
|
|
||||||
from ...core import app
|
|
||||||
from .. import entities
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler(metaclass=abc.ABCMeta):
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
) -> entities.StageProcessResult:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def cut_str(self, s: str) -> str:
|
|
||||||
"""
|
|
||||||
Take the first line of the string, up to 20 characters, if there are multiple lines, or more than 20 characters, add an ellipsis
|
|
||||||
"""
|
|
||||||
s0 = s.split('\n')[0]
|
|
||||||
if len(s0) > 20 or '\n' in s:
|
|
||||||
s0 = s0[:20] + '...'
|
|
||||||
return s0
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import typing
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
from .. import handler
|
|
||||||
from ... import entities
|
|
||||||
from ....provider import runner as runner_module
|
|
||||||
|
|
||||||
import langbot_plugin.api.entities.events as events
|
|
||||||
from ....utils import importutil
|
|
||||||
from ....provider import runners
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
||||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
|
||||||
|
|
||||||
|
|
||||||
importutil.import_modules_in_pkg(runners)
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageHandler(handler.MessageHandler):
|
|
||||||
async def handle(
|
|
||||||
self,
|
|
||||||
query: pipeline_query.Query,
|
|
||||||
) -> typing.AsyncGenerator[entities.StageProcessResult, None]:
|
|
||||||
"""处理"""
|
|
||||||
# 调API
|
|
||||||
# 生成器
|
|
||||||
|
|
||||||
# 触发插件事件
|
|
||||||
event_class = (
|
|
||||||
events.PersonNormalMessageReceived
|
|
||||||
if query.launcher_type == provider_session.LauncherTypes.PERSON
|
|
||||||
else events.GroupNormalMessageReceived
|
|
||||||
)
|
|
||||||
|
|
||||||
event = event_class(
|
|
||||||
launcher_type=query.launcher_type.value,
|
|
||||||
launcher_id=query.launcher_id,
|
|
||||||
sender_id=query.sender_id,
|
|
||||||
text_message=str(query.message_chain),
|
|
||||||
query=query,
|
|
||||||
)
|
|
||||||
|
|
||||||
event_ctx = await self.ap.plugin_connector.emit_event(event)
|
|
||||||
|
|
||||||
is_create_card = False # 判断下是否需要创建流式卡片
|
|
||||||
|
|
||||||
if event_ctx.is_prevented_default():
|
|
||||||
if event_ctx.event.reply_message_chain is not None:
|
|
||||||
mc = event_ctx.event.reply_message_chain
|
|
||||||
query.resp_messages.append(mc)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
else:
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.INTERRUPT, new_query=query)
|
|
||||||
else:
|
|
||||||
if event_ctx.event.user_message_alter is not None:
|
|
||||||
# if isinstance(event_ctx.event, str): # 现在暂时不考虑多模态alter
|
|
||||||
query.user_message.content = event_ctx.event.user_message_alter
|
|
||||||
|
|
||||||
text_length = 0
|
|
||||||
try:
|
|
||||||
is_stream = await query.adapter.is_stream_output_supported()
|
|
||||||
except AttributeError:
|
|
||||||
is_stream = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
for r in runner_module.preregistered_runners:
|
|
||||||
if r.name == query.pipeline_config['ai']['runner']['runner']:
|
|
||||||
runner = r(self.ap, query.pipeline_config)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f'未找到请求运行器: {query.pipeline_config["ai"]["runner"]["runner"]}')
|
|
||||||
if is_stream:
|
|
||||||
resp_message_id = uuid.uuid4()
|
|
||||||
|
|
||||||
async for result in runner.run(query):
|
|
||||||
result.resp_message_id = str(resp_message_id)
|
|
||||||
if query.resp_messages:
|
|
||||||
query.resp_messages.pop()
|
|
||||||
if query.resp_message_chain:
|
|
||||||
query.resp_message_chain.pop()
|
|
||||||
# 此时连接外部 AI 服务正常,创建卡片
|
|
||||||
if not is_create_card: # 只有不是第一次才创建卡片
|
|
||||||
await query.adapter.create_message_card(str(resp_message_id), query.message_event)
|
|
||||||
is_create_card = True
|
|
||||||
query.resp_messages.append(result)
|
|
||||||
self.ap.logger.info(f'对话({query.query_id})流式响应: {self.cut_str(result.readable_str())}')
|
|
||||||
|
|
||||||
if result.content is not None:
|
|
||||||
text_length += len(result.content)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
|
|
||||||
else:
|
|
||||||
async for result in runner.run(query):
|
|
||||||
query.resp_messages.append(result)
|
|
||||||
|
|
||||||
self.ap.logger.info(f'对话({query.query_id})响应: {self.cut_str(result.readable_str())}')
|
|
||||||
|
|
||||||
if result.content is not None:
|
|
||||||
text_length += len(result.content)
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(result_type=entities.ResultType.CONTINUE, new_query=query)
|
|
||||||
|
|
||||||
query.session.using_conversation.messages.append(query.user_message)
|
|
||||||
|
|
||||||
query.session.using_conversation.messages.extend(query.resp_messages)
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'对话({query.query_id})请求失败: {type(e).__name__} {str(e)}')
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
hide_exception_info = query.pipeline_config['output']['misc']['hide-exception']
|
|
||||||
|
|
||||||
yield entities.StageProcessResult(
|
|
||||||
result_type=entities.ResultType.INTERRUPT,
|
|
||||||
new_query=query,
|
|
||||||
user_notice='请求失败' if hide_exception_info else f'{e}',
|
|
||||||
error_notice=f'{e}',
|
|
||||||
debug_notice=traceback.format_exc(),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
# TODO statistics
|
|
||||||
pass
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ..core import app, entities as core_entities, taskmgr
|
|
||||||
|
|
||||||
from ..discover import engine
|
|
||||||
|
|
||||||
from ..entity.persistence import bot as persistence_bot
|
|
||||||
|
|
||||||
from ..entity.errors import platform as platform_errors
|
|
||||||
|
|
||||||
from .logger import EventLogger
|
|
||||||
|
|
||||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeBot:
|
|
||||||
"""运行时机器人"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
bot_entity: persistence_bot.Bot
|
|
||||||
|
|
||||||
enable: bool
|
|
||||||
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter
|
|
||||||
|
|
||||||
task_wrapper: taskmgr.TaskWrapper
|
|
||||||
|
|
||||||
task_context: taskmgr.TaskContext
|
|
||||||
|
|
||||||
logger: EventLogger
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
ap: app.Application,
|
|
||||||
bot_entity: persistence_bot.Bot,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
logger: EventLogger,
|
|
||||||
):
|
|
||||||
self.ap = ap
|
|
||||||
self.bot_entity = bot_entity
|
|
||||||
self.enable = bot_entity.enable
|
|
||||||
self.adapter = adapter
|
|
||||||
self.task_context = taskmgr.TaskContext()
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
async def on_friend_message(
|
|
||||||
event: platform_events.FriendMessage,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
):
|
|
||||||
image_components = [
|
|
||||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
|
||||||
]
|
|
||||||
|
|
||||||
await self.logger.info(
|
|
||||||
f'{event.message_chain}',
|
|
||||||
images=image_components,
|
|
||||||
message_session_id=f'person_{event.sender.id}',
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
|
||||||
bot_uuid=self.bot_entity.uuid,
|
|
||||||
launcher_type=provider_session.LauncherTypes.PERSON,
|
|
||||||
launcher_id=event.sender.id,
|
|
||||||
sender_id=event.sender.id,
|
|
||||||
message_event=event,
|
|
||||||
message_chain=event.message_chain,
|
|
||||||
adapter=adapter,
|
|
||||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def on_group_message(
|
|
||||||
event: platform_events.GroupMessage,
|
|
||||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
|
||||||
):
|
|
||||||
image_components = [
|
|
||||||
component for component in event.message_chain if isinstance(component, platform_message.Image)
|
|
||||||
]
|
|
||||||
|
|
||||||
await self.logger.info(
|
|
||||||
f'{event.message_chain}',
|
|
||||||
images=image_components,
|
|
||||||
message_session_id=f'group_{event.group.id}',
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.query_pool.add_query(
|
|
||||||
bot_uuid=self.bot_entity.uuid,
|
|
||||||
launcher_type=provider_session.LauncherTypes.GROUP,
|
|
||||||
launcher_id=event.group.id,
|
|
||||||
sender_id=event.sender.id,
|
|
||||||
message_event=event,
|
|
||||||
message_chain=event.message_chain,
|
|
||||||
adapter=adapter,
|
|
||||||
pipeline_uuid=self.bot_entity.use_pipeline_uuid,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
|
||||||
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
async def exception_wrapper():
|
|
||||||
try:
|
|
||||||
self.task_context.set_current_action('Running...')
|
|
||||||
await self.adapter.run_async()
|
|
||||||
self.task_context.set_current_action('Exited.')
|
|
||||||
except Exception as e:
|
|
||||||
if isinstance(e, asyncio.CancelledError):
|
|
||||||
self.task_context.set_current_action('Exited.')
|
|
||||||
return
|
|
||||||
|
|
||||||
traceback_str = traceback.format_exc()
|
|
||||||
self.task_context.set_current_action('Exited with error.')
|
|
||||||
await self.logger.error(f'平台适配器运行出错:\n{e}\n{traceback_str}')
|
|
||||||
|
|
||||||
self.task_wrapper = self.ap.task_mgr.create_task(
|
|
||||||
exception_wrapper(),
|
|
||||||
kind='platform-adapter',
|
|
||||||
name=f'platform-adapter-{self.adapter.__class__.__name__}',
|
|
||||||
context=self.task_context,
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def shutdown(self):
|
|
||||||
await self.adapter.kill()
|
|
||||||
|
|
||||||
self.ap.task_mgr.cancel_task(self.task_wrapper.id)
|
|
||||||
|
|
||||||
|
|
||||||
# 控制QQ消息输入输出的类
|
|
||||||
class PlatformManager:
|
|
||||||
# ====== 4.0 ======
|
|
||||||
ap: app.Application = None
|
|
||||||
|
|
||||||
bots: list[RuntimeBot]
|
|
||||||
|
|
||||||
webchat_proxy_bot: RuntimeBot
|
|
||||||
|
|
||||||
adapter_components: list[engine.Component]
|
|
||||||
|
|
||||||
adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]]
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application = None):
|
|
||||||
self.ap = ap
|
|
||||||
self.bots = []
|
|
||||||
self.adapter_components = []
|
|
||||||
self.adapter_dict = {}
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
self.adapter_components = self.ap.discover.get_components_by_kind('MessagePlatformAdapter')
|
|
||||||
adapter_dict: dict[str, type[abstract_platform_adapter.AbstractMessagePlatformAdapter]] = {}
|
|
||||||
for component in self.adapter_components:
|
|
||||||
adapter_dict[component.metadata.name] = component.get_python_component_class()
|
|
||||||
self.adapter_dict = adapter_dict
|
|
||||||
|
|
||||||
webchat_adapter_class = self.adapter_dict['webchat']
|
|
||||||
|
|
||||||
# initialize webchat adapter
|
|
||||||
webchat_logger = EventLogger(name='webchat-adapter', ap=self.ap)
|
|
||||||
webchat_adapter_inst = webchat_adapter_class(
|
|
||||||
{},
|
|
||||||
webchat_logger,
|
|
||||||
ap=self.ap,
|
|
||||||
is_stream=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.webchat_proxy_bot = RuntimeBot(
|
|
||||||
ap=self.ap,
|
|
||||||
bot_entity=persistence_bot.Bot(
|
|
||||||
uuid='webchat-proxy-bot',
|
|
||||||
name='WebChat',
|
|
||||||
description='',
|
|
||||||
adapter='webchat',
|
|
||||||
adapter_config={},
|
|
||||||
enable=True,
|
|
||||||
),
|
|
||||||
adapter=webchat_adapter_inst,
|
|
||||||
logger=webchat_logger,
|
|
||||||
)
|
|
||||||
await self.webchat_proxy_bot.initialize()
|
|
||||||
|
|
||||||
await self.load_bots_from_db()
|
|
||||||
|
|
||||||
def get_running_adapters(self) -> list[abstract_platform_adapter.AbstractMessagePlatformAdapter]:
|
|
||||||
return [bot.adapter for bot in self.bots if bot.enable]
|
|
||||||
|
|
||||||
async def load_bots_from_db(self):
|
|
||||||
self.ap.logger.info('Loading bots from db...')
|
|
||||||
|
|
||||||
self.bots = []
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
for bot in bots:
|
|
||||||
# load all bots here, enable or disable will be handled in runtime
|
|
||||||
try:
|
|
||||||
await self.load_bot(bot)
|
|
||||||
except platform_errors.AdapterNotFoundError as e:
|
|
||||||
self.ap.logger.warning(f'Adapter {e.adapter_name} not found, skipping bot {bot.uuid}')
|
|
||||||
except Exception as e:
|
|
||||||
self.ap.logger.error(f'Failed to load bot {bot.uuid}: {e}\n{traceback.format_exc()}')
|
|
||||||
|
|
||||||
async def load_bot(
|
|
||||||
self,
|
|
||||||
bot_entity: persistence_bot.Bot | sqlalchemy.Row[persistence_bot.Bot] | dict,
|
|
||||||
) -> RuntimeBot:
|
|
||||||
"""加载机器人"""
|
|
||||||
if isinstance(bot_entity, sqlalchemy.Row):
|
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity._mapping)
|
|
||||||
elif isinstance(bot_entity, dict):
|
|
||||||
bot_entity = persistence_bot.Bot(**bot_entity)
|
|
||||||
|
|
||||||
logger = EventLogger(name=f'platform-adapter-{bot_entity.name}', ap=self.ap)
|
|
||||||
|
|
||||||
if bot_entity.adapter not in self.adapter_dict:
|
|
||||||
raise platform_errors.AdapterNotFoundError(bot_entity.adapter)
|
|
||||||
|
|
||||||
adapter_inst = self.adapter_dict[bot_entity.adapter](
|
|
||||||
bot_entity.adapter_config,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
runtime_bot = RuntimeBot(ap=self.ap, bot_entity=bot_entity, adapter=adapter_inst, logger=logger)
|
|
||||||
|
|
||||||
await runtime_bot.initialize()
|
|
||||||
|
|
||||||
self.bots.append(runtime_bot)
|
|
||||||
|
|
||||||
return runtime_bot
|
|
||||||
|
|
||||||
async def get_bot_by_uuid(self, bot_uuid: str) -> RuntimeBot | None:
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.bot_entity.uuid == bot_uuid:
|
|
||||||
return bot
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def remove_bot(self, bot_uuid: str):
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.bot_entity.uuid == bot_uuid:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.shutdown()
|
|
||||||
self.bots.remove(bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_available_adapters_info(self) -> list[dict]:
|
|
||||||
return [
|
|
||||||
component.to_plain_dict() for component in self.adapter_components if component.metadata.name != 'webchat'
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_available_adapter_info_by_name(self, name: str) -> dict | None:
|
|
||||||
for component in self.adapter_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component.to_plain_dict()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_available_adapter_manifest_by_name(self, name: str) -> engine.Component | None:
|
|
||||||
for component in self.adapter_components:
|
|
||||||
if component.metadata.name == name:
|
|
||||||
return component
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
# This method will only be called when the application launching
|
|
||||||
await self.webchat_proxy_bot.run()
|
|
||||||
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.run()
|
|
||||||
|
|
||||||
async def shutdown(self):
|
|
||||||
for bot in self.bots:
|
|
||||||
if bot.enable:
|
|
||||||
await bot.shutdown()
|
|
||||||
self.ap.task_mgr.cancel_by_scope(core_entities.LifecycleControlScope.PLATFORM)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: dingtalk
|
|
||||||
label:
|
|
||||||
en_US: DingTalk
|
|
||||||
zh_Hans: 钉钉
|
|
||||||
description:
|
|
||||||
en_US: DingTalk Adapter
|
|
||||||
zh_Hans: 钉钉适配器,请查看文档了解使用方式
|
|
||||||
icon: dingtalk.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: client_id
|
|
||||||
label:
|
|
||||||
en_US: Client ID
|
|
||||||
zh_Hans: 客户端ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: client_secret
|
|
||||||
label:
|
|
||||||
en_US: Client Secret
|
|
||||||
zh_Hans: 客户端密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: robot_code
|
|
||||||
label:
|
|
||||||
en_US: Robot Code
|
|
||||||
zh_Hans: 机器人代码
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: robot_name
|
|
||||||
label:
|
|
||||||
en_US: Robot Name
|
|
||||||
zh_Hans: 机器人名称
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: markdown_card
|
|
||||||
label:
|
|
||||||
en_US: Markdown Card
|
|
||||||
zh_Hans: 是否使用 Markdown 卡片
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
- name: enable-stream-reply
|
|
||||||
label:
|
|
||||||
en_US: Enable Stream Reply Mode
|
|
||||||
zh_Hans: 启用钉钉卡片流式回复模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use the stream of lark reply mode
|
|
||||||
zh_Hans: 如果启用,将使用钉钉卡片流式方式来回复内容
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
- name: card_template_id
|
|
||||||
label:
|
|
||||||
en_US: card template id
|
|
||||||
zh_Hans: 卡片模板ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "填写你的卡片template_id"
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./dingtalk.py
|
|
||||||
attr: DingTalkAdapter
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: discord
|
|
||||||
label:
|
|
||||||
en_US: Discord
|
|
||||||
zh_Hans: Discord
|
|
||||||
description:
|
|
||||||
en_US: Discord Adapter
|
|
||||||
zh_Hans: Discord 适配器,请查看文档了解使用方式
|
|
||||||
icon: discord.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: client_id
|
|
||||||
label:
|
|
||||||
en_US: Client ID
|
|
||||||
zh_Hans: 客户端ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./discord.py
|
|
||||||
attr: DiscordAdapter
|
|
||||||
@@ -1,809 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import lark_oapi
|
|
||||||
from lark_oapi.api.im.v1 import CreateImageRequest, CreateImageRequestBody
|
|
||||||
import traceback
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
import base64
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import lark_oapi.ws.exception
|
|
||||||
import quart
|
|
||||||
from lark_oapi.api.im.v1 import *
|
|
||||||
import pydantic
|
|
||||||
from lark_oapi.api.cardkit.v1 import *
|
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
|
|
||||||
|
|
||||||
|
|
||||||
class AESCipher(object):
|
|
||||||
def __init__(self, key):
|
|
||||||
self.bs = AES.block_size
|
|
||||||
self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def str_to_bytes(data):
|
|
||||||
u_type = type(b''.decode('utf8'))
|
|
||||||
if isinstance(data, u_type):
|
|
||||||
return data.encode('utf8')
|
|
||||||
return data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _unpad(s):
|
|
||||||
return s[: -ord(s[len(s) - 1 :])]
|
|
||||||
|
|
||||||
def decrypt(self, enc):
|
|
||||||
iv = enc[: AES.block_size]
|
|
||||||
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
|
||||||
return self._unpad(cipher.decrypt(enc[AES.block_size :]))
|
|
||||||
|
|
||||||
def decrypt_string(self, enc):
|
|
||||||
enc = base64.b64decode(enc)
|
|
||||||
return self.decrypt(enc).decode('utf8')
|
|
||||||
|
|
||||||
|
|
||||||
class LarkMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(
|
|
||||||
message_chain: platform_message.MessageChain, api_client: lark_oapi.Client
|
|
||||||
) -> typing.Tuple[list]:
|
|
||||||
message_elements = []
|
|
||||||
pending_paragraph = []
|
|
||||||
for msg in message_chain:
|
|
||||||
if isinstance(msg, platform_message.Plain):
|
|
||||||
# Ensure text is valid UTF-8
|
|
||||||
try:
|
|
||||||
text = msg.text.encode('utf-8').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
except UnicodeError:
|
|
||||||
# If text is not valid UTF-8, try to decode with other encodings
|
|
||||||
try:
|
|
||||||
text = msg.text.encode('latin1').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
except UnicodeError:
|
|
||||||
# If still fails, replace invalid characters
|
|
||||||
text = msg.text.encode('utf-8', errors='replace').decode('utf-8')
|
|
||||||
pending_paragraph.append({'tag': 'md', 'text': text})
|
|
||||||
elif isinstance(msg, platform_message.At):
|
|
||||||
pending_paragraph.append({'tag': 'at', 'user_id': msg.target, 'style': []})
|
|
||||||
elif isinstance(msg, platform_message.AtAll):
|
|
||||||
pending_paragraph.append({'tag': 'at', 'user_id': 'all', 'style': []})
|
|
||||||
elif isinstance(msg, platform_message.Image):
|
|
||||||
image_bytes = None
|
|
||||||
|
|
||||||
if msg.base64:
|
|
||||||
try:
|
|
||||||
# Remove data URL prefix if present
|
|
||||||
if msg.base64.startswith('data:'):
|
|
||||||
msg.base64 = msg.base64.split(',', 1)[1]
|
|
||||||
image_bytes = base64.b64decode(msg.base64)
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
elif msg.url:
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(msg.url) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
image_bytes = await response.read()
|
|
||||||
else:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
elif msg.path:
|
|
||||||
try:
|
|
||||||
with open(msg.path, 'rb') as f:
|
|
||||||
image_bytes = f.read()
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
|
|
||||||
if image_bytes is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create a temporary file to store the image bytes
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
|
||||||
temp_file.write(image_bytes)
|
|
||||||
temp_file.flush()
|
|
||||||
|
|
||||||
# Create image request using the temporary file
|
|
||||||
request = (
|
|
||||||
CreateImageRequest.builder()
|
|
||||||
.request_body(
|
|
||||||
CreateImageRequestBody.builder()
|
|
||||||
.image_type('message')
|
|
||||||
.image(open(temp_file.name, 'rb'))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response = await api_client.im.v1.image.acreate(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.image.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
image_key = response.data.image_key
|
|
||||||
|
|
||||||
message_elements.append(pending_paragraph)
|
|
||||||
message_elements.append(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
'tag': 'img',
|
|
||||||
'image_key': image_key,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
pending_paragraph = []
|
|
||||||
except Exception:
|
|
||||||
traceback.print_exc()
|
|
||||||
continue
|
|
||||||
finally:
|
|
||||||
# Clean up the temporary file
|
|
||||||
import os
|
|
||||||
|
|
||||||
if 'temp_file' in locals():
|
|
||||||
os.unlink(temp_file.name)
|
|
||||||
elif isinstance(msg, platform_message.Forward):
|
|
||||||
for node in msg.node_list:
|
|
||||||
message_elements.extend(await LarkMessageConverter.yiri2target(node.message_chain, api_client))
|
|
||||||
|
|
||||||
if pending_paragraph:
|
|
||||||
message_elements.append(pending_paragraph)
|
|
||||||
|
|
||||||
return message_elements
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(
|
|
||||||
message: lark_oapi.api.im.v1.model.event_message.EventMessage,
|
|
||||||
api_client: lark_oapi.Client,
|
|
||||||
) -> platform_message.MessageChain:
|
|
||||||
message_content = json.loads(message.content)
|
|
||||||
|
|
||||||
lb_msg_list = []
|
|
||||||
|
|
||||||
msg_create_time = datetime.datetime.fromtimestamp(int(message.create_time) / 1000)
|
|
||||||
|
|
||||||
lb_msg_list.append(platform_message.Source(id=message.message_id, time=msg_create_time))
|
|
||||||
|
|
||||||
if message.message_type == 'text':
|
|
||||||
element_list = []
|
|
||||||
|
|
||||||
def text_element_recur(text_ele: dict) -> list[dict]:
|
|
||||||
if text_ele['text'] == '':
|
|
||||||
return []
|
|
||||||
|
|
||||||
at_pattern = re.compile(r'@_user_[\d]+')
|
|
||||||
at_matches = at_pattern.findall(text_ele['text'])
|
|
||||||
|
|
||||||
name_mapping = {}
|
|
||||||
for mathc in at_matches:
|
|
||||||
for mention in message.mentions:
|
|
||||||
if mention.key == mathc:
|
|
||||||
name_mapping[mathc] = mention.name
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(name_mapping.keys()) == 0:
|
|
||||||
return [text_ele]
|
|
||||||
|
|
||||||
# 只处理第一个,剩下的递归处理
|
|
||||||
text_split = text_ele['text'].split(list(name_mapping.keys())[0])
|
|
||||||
|
|
||||||
new_list = []
|
|
||||||
|
|
||||||
left_text = text_split[0]
|
|
||||||
right_text = text_split[1]
|
|
||||||
|
|
||||||
new_list.extend(text_element_recur({'tag': 'text', 'text': left_text, 'style': []}))
|
|
||||||
|
|
||||||
new_list.append(
|
|
||||||
{
|
|
||||||
'tag': 'at',
|
|
||||||
'user_id': list(name_mapping.keys())[0],
|
|
||||||
'user_name': name_mapping[list(name_mapping.keys())[0]],
|
|
||||||
'style': [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
new_list.extend(text_element_recur({'tag': 'text', 'text': right_text, 'style': []}))
|
|
||||||
|
|
||||||
return new_list
|
|
||||||
|
|
||||||
element_list = text_element_recur({'tag': 'text', 'text': message_content['text'], 'style': []})
|
|
||||||
|
|
||||||
message_content = {'title': '', 'content': element_list}
|
|
||||||
|
|
||||||
elif message.message_type == 'post':
|
|
||||||
new_list = []
|
|
||||||
|
|
||||||
for ele in message_content['content']:
|
|
||||||
if type(ele) is dict:
|
|
||||||
new_list.append(ele)
|
|
||||||
elif type(ele) is list:
|
|
||||||
new_list.extend(ele)
|
|
||||||
|
|
||||||
message_content['content'] = new_list
|
|
||||||
elif message.message_type == 'image':
|
|
||||||
message_content['content'] = [{'tag': 'img', 'image_key': message_content['image_key'], 'style': []}]
|
|
||||||
|
|
||||||
for ele in message_content['content']:
|
|
||||||
if ele['tag'] == 'text':
|
|
||||||
lb_msg_list.append(platform_message.Plain(text=ele['text']))
|
|
||||||
elif ele['tag'] == 'at':
|
|
||||||
lb_msg_list.append(platform_message.At(target=ele['user_name']))
|
|
||||||
elif ele['tag'] == 'img':
|
|
||||||
image_key = ele['image_key']
|
|
||||||
|
|
||||||
request: GetMessageResourceRequest = (
|
|
||||||
GetMessageResourceRequest.builder()
|
|
||||||
.message_id(message.message_id)
|
|
||||||
.file_key(image_key)
|
|
||||||
.type('image')
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response: GetMessageResourceResponse = await api_client.im.v1.message_resource.aget(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message_resource.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
image_bytes = response.file.read()
|
|
||||||
image_base64 = base64.b64encode(image_bytes).decode()
|
|
||||||
|
|
||||||
image_format = response.raw.headers['content-type']
|
|
||||||
|
|
||||||
lb_msg_list.append(platform_message.Image(base64=f'data:{image_format};base64,{image_base64}'))
|
|
||||||
|
|
||||||
return platform_message.MessageChain(lb_msg_list)
|
|
||||||
|
|
||||||
|
|
||||||
class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(
|
|
||||||
event: platform_events.MessageEvent,
|
|
||||||
) -> lark_oapi.im.v1.P2ImMessageReceiveV1:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(
|
|
||||||
event: lark_oapi.im.v1.P2ImMessageReceiveV1, api_client: lark_oapi.Client
|
|
||||||
) -> platform_events.Event:
|
|
||||||
message_chain = await LarkMessageConverter.target2yiri(event.event.message, api_client)
|
|
||||||
|
|
||||||
if event.event.message.chat_type == 'p2p':
|
|
||||||
return platform_events.FriendMessage(
|
|
||||||
sender=platform_entities.Friend(
|
|
||||||
id=event.event.sender.sender_id.open_id,
|
|
||||||
nickname=event.event.sender.sender_id.union_id,
|
|
||||||
remark='',
|
|
||||||
),
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=event.event.message.create_time,
|
|
||||||
)
|
|
||||||
elif event.event.message.chat_type == 'group':
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=platform_entities.GroupMember(
|
|
||||||
id=event.event.sender.sender_id.open_id,
|
|
||||||
member_name=event.event.sender.sender_id.union_id,
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.event.message.chat_id,
|
|
||||||
name='',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
),
|
|
||||||
message_chain=message_chain,
|
|
||||||
time=event.event.message.create_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CARD_ID_CACHE_SIZE = 500
|
|
||||||
CARD_ID_CACHE_MAX_LIFETIME = 20 * 60 # 20分钟
|
|
||||||
|
|
||||||
|
|
||||||
class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
bot: lark_oapi.ws.Client = pydantic.Field(exclude=True)
|
|
||||||
api_client: lark_oapi.Client = pydantic.Field(exclude=True)
|
|
||||||
|
|
||||||
bot_account_id: str # 用于在流水线中识别at是否是本bot,直接以bot_name作为标识
|
|
||||||
lark_tenant_key: str = pydantic.Field(exclude=True, default='') # 飞书企业key
|
|
||||||
|
|
||||||
message_converter: LarkMessageConverter = LarkMessageConverter()
|
|
||||||
event_converter: LarkEventConverter = LarkEventConverter()
|
|
||||||
|
|
||||||
listeners: typing.Dict[
|
|
||||||
typing.Type[platform_events.Event],
|
|
||||||
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
||||||
]
|
|
||||||
|
|
||||||
quart_app: quart.Quart = pydantic.Field(exclude=True)
|
|
||||||
|
|
||||||
card_id_dict: dict[str, str] # 消息id到卡片id的映射,便于创建卡片后的发送消息到指定卡片
|
|
||||||
|
|
||||||
seq: int # 用于在发送卡片消息中识别消息顺序,直接以seq作为标识
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
|
||||||
quart_app = quart.Quart(__name__)
|
|
||||||
|
|
||||||
@quart_app.route('/lark/callback', methods=['POST'])
|
|
||||||
async def lark_callback():
|
|
||||||
try:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
if 'encrypt' in data:
|
|
||||||
cipher = AESCipher(config['encrypt-key'])
|
|
||||||
data = cipher.decrypt_string(data['encrypt'])
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
type = data.get('type')
|
|
||||||
if type is None:
|
|
||||||
context = EventContext(data)
|
|
||||||
type = context.header.event_type
|
|
||||||
|
|
||||||
if 'url_verification' == type:
|
|
||||||
# todo 验证verification token
|
|
||||||
return {'challenge': data.get('challenge')}
|
|
||||||
context = EventContext(data)
|
|
||||||
type = context.header.event_type
|
|
||||||
p2v1 = P2ImMessageReceiveV1()
|
|
||||||
p2v1.header = context.header
|
|
||||||
event = P2ImMessageReceiveV1Data()
|
|
||||||
event.message = EventMessage(context.event['message'])
|
|
||||||
event.sender = EventSender(context.event['sender'])
|
|
||||||
p2v1.event = event
|
|
||||||
p2v1.schema = context.schema
|
|
||||||
if 'im.message.receive_v1' == type:
|
|
||||||
try:
|
|
||||||
event = await self.event_converter.target2yiri(p2v1, self.api_client)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
if event.__class__ in self.listeners:
|
|
||||||
await self.listeners[event.__class__](event, self)
|
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
|
||||||
return {'code': 500, 'message': 'error'}
|
|
||||||
|
|
||||||
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
||||||
lb_event = await self.event_converter.target2yiri(event, self.api_client)
|
|
||||||
|
|
||||||
await self.listeners[type(lb_event)](lb_event, self)
|
|
||||||
|
|
||||||
def sync_on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
||||||
asyncio.create_task(on_message(event))
|
|
||||||
|
|
||||||
event_handler = (
|
|
||||||
lark_oapi.EventDispatcherHandler.builder('', '').register_p2_im_message_receive_v1(sync_on_message).build()
|
|
||||||
)
|
|
||||||
|
|
||||||
bot_account_id = config['bot_name']
|
|
||||||
|
|
||||||
bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler)
|
|
||||||
api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build()
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
lark_tenant_key=config.get('lark_tenant_key', ''),
|
|
||||||
card_id_dict={},
|
|
||||||
seq=1,
|
|
||||||
listeners={},
|
|
||||||
quart_app=quart_app,
|
|
||||||
bot=bot,
|
|
||||||
api_client=api_client,
|
|
||||||
bot_account_id=bot_account_id,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def is_stream_output_supported(self) -> bool:
|
|
||||||
is_stream = False
|
|
||||||
if self.config.get('enable-stream-reply', None):
|
|
||||||
is_stream = True
|
|
||||||
return is_stream
|
|
||||||
|
|
||||||
async def create_card_id(self, message_id):
|
|
||||||
try:
|
|
||||||
# self.logger.debug('飞书支持stream输出,创建卡片......')
|
|
||||||
|
|
||||||
card_data = {
|
|
||||||
'schema': '2.0',
|
|
||||||
'config': {
|
|
||||||
'update_multi': True,
|
|
||||||
'streaming_mode': True,
|
|
||||||
'streaming_config': {
|
|
||||||
'print_step': {'default': 1},
|
|
||||||
'print_frequency_ms': {'default': 70},
|
|
||||||
'print_strategy': 'fast',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'body': {
|
|
||||||
'direction': 'vertical',
|
|
||||||
'padding': '12px 12px 12px 12px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'div',
|
|
||||||
'text': {
|
|
||||||
'tag': 'plain_text',
|
|
||||||
'content': 'LangBot',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_color': 'default',
|
|
||||||
},
|
|
||||||
'icon': {
|
|
||||||
'tag': 'custom_icon',
|
|
||||||
'img_key': 'img_v3_02p3_05c65d5d-9bad-440a-a2fb-c89571bfd5bg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'element_id': 'streaming_txt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column_set',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'columns': [
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': 'weighted',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'normal',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '2px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'weight': 1,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{'tag': 'hr', 'margin': '0px 0px 0px 0px'},
|
|
||||||
{
|
|
||||||
'tag': 'column_set',
|
|
||||||
'horizontal_spacing': '12px',
|
|
||||||
'horizontal_align': 'right',
|
|
||||||
'columns': [
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': 'weighted',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'markdown',
|
|
||||||
'content': '<font color="grey-600">以上内容由 AI 生成,仅供参考。更多详细、准确信息可点击引用链接查看</font>',
|
|
||||||
'text_align': 'left',
|
|
||||||
'text_size': 'notation',
|
|
||||||
'margin': '4px 0px 0px 0px',
|
|
||||||
'icon': {
|
|
||||||
'tag': 'standard_icon',
|
|
||||||
'token': 'robot_outlined',
|
|
||||||
'color': 'grey',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
'weight': 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': '20px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'button',
|
|
||||||
'text': {'tag': 'plain_text', 'content': ''},
|
|
||||||
'type': 'text',
|
|
||||||
'width': 'fill',
|
|
||||||
'size': 'medium',
|
|
||||||
'icon': {'tag': 'standard_icon', 'token': 'thumbsup_outlined'},
|
|
||||||
'hover_tips': {'tag': 'plain_text', 'content': '有帮助'},
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'direction': 'vertical',
|
|
||||||
'horizontal_spacing': '8px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tag': 'column',
|
|
||||||
'width': '30px',
|
|
||||||
'elements': [
|
|
||||||
{
|
|
||||||
'tag': 'button',
|
|
||||||
'text': {'tag': 'plain_text', 'content': ''},
|
|
||||||
'type': 'text',
|
|
||||||
'width': 'default',
|
|
||||||
'size': 'medium',
|
|
||||||
'icon': {'tag': 'standard_icon', 'token': 'thumbdown_outlined'},
|
|
||||||
'hover_tips': {'tag': 'plain_text', 'content': '无帮助'},
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'padding': '0px 0px 0px 0px',
|
|
||||||
'vertical_spacing': '8px',
|
|
||||||
'horizontal_align': 'left',
|
|
||||||
'vertical_align': 'top',
|
|
||||||
'margin': '0px 0px 0px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'margin': '0px 0px 4px 0px',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# delay / fast 创建卡片模板,delay 延迟打印,fast 实时打印,可以自定义更好看的消息模板
|
|
||||||
|
|
||||||
request: CreateCardRequest = (
|
|
||||||
CreateCardRequest.builder()
|
|
||||||
.request_body(CreateCardRequestBody.builder().type('card_json').data(json.dumps(card_data)).build())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发起请求
|
|
||||||
response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.cardkit.v1.card.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.card_id_dict[message_id] = response.data.card_id
|
|
||||||
|
|
||||||
card_id = response.data.card_id
|
|
||||||
return card_id
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
async def create_message_card(self, message_id, event) -> str:
|
|
||||||
"""
|
|
||||||
创建卡片消息。
|
|
||||||
使用卡片消息是因为普通消息更新次数有限制,而大模型流式返回结果可能很多而超过限制,而飞书卡片没有这个限制(api免费次数有限)
|
|
||||||
"""
|
|
||||||
# message_id = event.message_chain.message_id
|
|
||||||
|
|
||||||
card_id = await self.create_card_id(message_id)
|
|
||||||
content = {
|
|
||||||
'type': 'card',
|
|
||||||
'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}},
|
|
||||||
} # 当收到消息时发送消息模板,可添加模板变量,详情查看飞书中接口文档
|
|
||||||
request: ReplyMessageRequest = (
|
|
||||||
ReplyMessageRequest.builder()
|
|
||||||
.message_id(event.message_chain.message_id)
|
|
||||||
.request_body(
|
|
||||||
ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 发起请求
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def reply_message(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
):
|
|
||||||
# 不再需要了,因为message_id已经被包含到message_chain中
|
|
||||||
# lark_event = await self.event_converter.yiri2target(message_source)
|
|
||||||
lark_message = await self.message_converter.yiri2target(message, self.api_client)
|
|
||||||
|
|
||||||
final_content = {
|
|
||||||
'zh_Hans': {
|
|
||||||
'title': '',
|
|
||||||
'content': lark_message,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
request: ReplyMessageRequest = (
|
|
||||||
ReplyMessageRequest.builder()
|
|
||||||
.message_id(message_source.message_chain.message_id)
|
|
||||||
.request_body(
|
|
||||||
ReplyMessageRequestBody.builder()
|
|
||||||
.content(json.dumps(final_content))
|
|
||||||
.msg_type('post')
|
|
||||||
.reply_in_thread(False)
|
|
||||||
.uuid(str(uuid.uuid4()))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
|
||||||
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.reply failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def reply_message_chunk(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
bot_message,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
is_final: bool = False,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
回复消息变成更新卡片消息
|
|
||||||
"""
|
|
||||||
# self.seq += 1
|
|
||||||
message_id = bot_message.resp_message_id
|
|
||||||
msg_seq = bot_message.msg_sequence
|
|
||||||
if msg_seq % 8 == 0 or is_final:
|
|
||||||
lark_message = await self.message_converter.yiri2target(message, self.api_client)
|
|
||||||
|
|
||||||
text_message = ''
|
|
||||||
for ele in lark_message[0]:
|
|
||||||
if ele['tag'] == 'text':
|
|
||||||
text_message += ele['text']
|
|
||||||
elif ele['tag'] == 'md':
|
|
||||||
text_message += ele['text']
|
|
||||||
|
|
||||||
# content = {
|
|
||||||
# 'type': 'card_json',
|
|
||||||
# 'data': {'card_id': self.card_id_dict[message_id], 'elements': {'content': text_message}},
|
|
||||||
# }
|
|
||||||
|
|
||||||
request: ContentCardElementRequest = (
|
|
||||||
ContentCardElementRequest.builder()
|
|
||||||
.card_id(self.card_id_dict[message_id])
|
|
||||||
.element_id('streaming_txt')
|
|
||||||
.request_body(
|
|
||||||
ContentCardElementRequestBody.builder()
|
|
||||||
# .uuid("a0d69e20-1dd1-458b-k525-dfeca4015204")
|
|
||||||
.content(text_message)
|
|
||||||
.sequence(msg_seq)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_final and bot_message.tool_calls is None:
|
|
||||||
# self.seq = 1 # 消息回复结束之后重置seq
|
|
||||||
self.card_id_dict.pop(message_id) # 清理已经使用过的卡片
|
|
||||||
# 发起请求
|
|
||||||
response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request)
|
|
||||||
|
|
||||||
# 处理失败返回
|
|
||||||
if not response.success():
|
|
||||||
raise Exception(
|
|
||||||
f'client.im.v1.message.patch failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def is_muted(self, group_id: int) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.listeners[event_type] = callback
|
|
||||||
|
|
||||||
def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
self.listeners.pop(event_type)
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
port = self.config['port']
|
|
||||||
enable_webhook = self.config['enable-webhook']
|
|
||||||
|
|
||||||
if not enable_webhook:
|
|
||||||
try:
|
|
||||||
await self.bot._connect()
|
|
||||||
except lark_oapi.ws.exception.ClientException as e:
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
await self.bot._disconnect()
|
|
||||||
if self.bot._auto_reconnect:
|
|
||||||
await self.bot._reconnect()
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
|
|
||||||
async def shutdown_trigger_placeholder():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await self.quart_app.run_task(
|
|
||||||
host='0.0.0.0',
|
|
||||||
port=port,
|
|
||||||
shutdown_trigger=shutdown_trigger_placeholder,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
|
||||||
# 需要断开连接,不然旧的连接会继续运行,导致飞书消息来时会随机选择一个连接
|
|
||||||
# 断开时lark.ws.Client的_receive_message_loop会打印error日志: receive message loop exit。然后进行重连,
|
|
||||||
# 所以要设置_auto_reconnect=False,让其不重连。
|
|
||||||
self.bot._auto_reconnect = False
|
|
||||||
await self.bot._disconnect()
|
|
||||||
return False
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: lark
|
|
||||||
label:
|
|
||||||
en_US: Lark
|
|
||||||
zh_Hans: 飞书
|
|
||||||
description:
|
|
||||||
en_US: Lark Adapter
|
|
||||||
zh_Hans: 飞书适配器,请查看文档了解使用方式
|
|
||||||
icon: lark.svg
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: app_id
|
|
||||||
label:
|
|
||||||
en_US: App ID
|
|
||||||
zh_Hans: 应用ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: app_secret
|
|
||||||
label:
|
|
||||||
en_US: App Secret
|
|
||||||
zh_Hans: 应用密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: bot_name
|
|
||||||
label:
|
|
||||||
en_US: Bot Name
|
|
||||||
zh_Hans: 机器人名称
|
|
||||||
description:
|
|
||||||
en_US: Must be the same as the name of the bot in Lark, otherwise the bot will not be able to receive messages in the group
|
|
||||||
zh_Hans: 必须与飞书机器人名称一致,否则机器人将无法在群内正常接收消息
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: enable-webhook
|
|
||||||
label:
|
|
||||||
en_US: Enable Webhook Mode
|
|
||||||
zh_Hans: 启用Webhook模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use webhook mode to receive messages. Otherwise, it will use WS long connection mode
|
|
||||||
zh_Hans: 如果启用,机器人将使用 Webhook 模式接收消息。否则,将使用 WS 长连接模式
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Webhook Port
|
|
||||||
zh_Hans: Webhook端口
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
|
|
||||||
zh_Hans: 仅在启用 Webhook 模式时有效,请填写 Webhook 端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2285
|
|
||||||
- name: encrypt-key
|
|
||||||
label:
|
|
||||||
en_US: Encrypt Key
|
|
||||||
zh_Hans: 加密密钥
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the encrypt key
|
|
||||||
zh_Hans: 仅在启用 Webhook 模式时有效,请填写加密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: enable-stream-reply
|
|
||||||
label:
|
|
||||||
en_US: Enable Stream Reply Mode
|
|
||||||
zh_Hans: 启用飞书流式回复模式
|
|
||||||
description:
|
|
||||||
en_US: If enabled, the bot will use the stream of lark reply mode
|
|
||||||
zh_Hans: 如果启用,将使用飞书流式方式来回复内容
|
|
||||||
type: boolean
|
|
||||||
required: true
|
|
||||||
default: false
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./lark.py
|
|
||||||
attr: LarkAdapter
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: LINE
|
|
||||||
label:
|
|
||||||
en_US: LINE
|
|
||||||
zh_Hans: LINE
|
|
||||||
description:
|
|
||||||
en_US: LINE Adapter
|
|
||||||
zh_Hans: LINE适配器,请查看文档了解使用方式
|
|
||||||
ja_JP: LINEアダプター、ドキュメントを参照してください
|
|
||||||
zh_Hant: LINE適配器,請查看文檔了解使用方式
|
|
||||||
icon: line.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: channel_access_token
|
|
||||||
label:
|
|
||||||
en_US: Channel access token
|
|
||||||
zh_Hans: 频道访问令牌
|
|
||||||
ja_JP: チャンネルアクセストークン
|
|
||||||
zh_Hant: 頻道訪問令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Webhook Port
|
|
||||||
zh_Hans: Webhook端口
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the webhook port
|
|
||||||
zh_Hans: 请填写 Webhook 端口
|
|
||||||
ja_JP: Webhookポートを入力してください
|
|
||||||
zh_Hant: 請填寫 Webhook 端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2287
|
|
||||||
- name: channel_secret
|
|
||||||
label:
|
|
||||||
en_US: Channel secret
|
|
||||||
zh_Hans: 消息密钥
|
|
||||||
ja_JP: チャンネルシークレット
|
|
||||||
zh_Hant: 消息密钥
|
|
||||||
description:
|
|
||||||
en_US: Only valid when webhook mode is enabled, please fill in the encrypt key
|
|
||||||
zh_Hans: 请填写加密密钥
|
|
||||||
ja_JP: Webhookモードが有効な場合にのみ、暗号化キーを入力してください
|
|
||||||
zh_Hant: 請填寫加密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./line.py
|
|
||||||
attr: LINEAdapter
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: MessagePlatformAdapter
|
|
||||||
metadata:
|
|
||||||
name: officialaccount
|
|
||||||
label:
|
|
||||||
en_US: Official Account
|
|
||||||
zh_Hans: 微信公众号
|
|
||||||
description:
|
|
||||||
en_US: Official Account Adapter
|
|
||||||
zh_Hans: 微信公众号适配器,请查看文档了解使用方式
|
|
||||||
icon: officialaccount.png
|
|
||||||
spec:
|
|
||||||
config:
|
|
||||||
- name: token
|
|
||||||
label:
|
|
||||||
en_US: Token
|
|
||||||
zh_Hans: 令牌
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: EncodingAESKey
|
|
||||||
label:
|
|
||||||
en_US: EncodingAESKey
|
|
||||||
zh_Hans: 消息加解密密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: AppID
|
|
||||||
label:
|
|
||||||
en_US: App ID
|
|
||||||
zh_Hans: 应用ID
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: AppSecret
|
|
||||||
label:
|
|
||||||
en_US: App Secret
|
|
||||||
zh_Hans: 应用密钥
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: ""
|
|
||||||
- name: Mode
|
|
||||||
label:
|
|
||||||
en_US: Mode
|
|
||||||
zh_Hans: 接入模式
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "drop"
|
|
||||||
- name: LoadingMessage
|
|
||||||
label:
|
|
||||||
en_US: Loading Message
|
|
||||||
zh_Hans: 加载消息
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: "AI正在思考中,请发送任意内容获取回复。"
|
|
||||||
- name: host
|
|
||||||
label:
|
|
||||||
en_US: Host
|
|
||||||
zh_Hans: 监听主机
|
|
||||||
description:
|
|
||||||
en_US: The host that Official Account listens on for Webhook connections.
|
|
||||||
zh_Hans: 微信公众号监听的主机,除非你知道自己在做什么,否则请写 0.0.0.0
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
default: 0.0.0.0
|
|
||||||
- name: port
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 监听端口
|
|
||||||
type: integer
|
|
||||||
required: true
|
|
||||||
default: 2287
|
|
||||||
execution:
|
|
||||||
python:
|
|
||||||
path: ./officialaccount.py
|
|
||||||
attr: OfficialAccountAdapter
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import typing
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.entities as platform_entities
|
|
||||||
from langbot_plugin.api.entities.builtin.command import errors as command_errors
|
|
||||||
from libs.qq_official_api.api import QQOfficialClient
|
|
||||||
from libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
|
||||||
from ...utils import image
|
|
||||||
from ..logger import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialMessageConverter(abstract_platform_adapter.AbstractMessageConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(message_chain: platform_message.MessageChain):
|
|
||||||
content_list = []
|
|
||||||
# 只实现了发文字
|
|
||||||
for msg in message_chain:
|
|
||||||
if type(msg) is platform_message.Plain:
|
|
||||||
content_list.append(
|
|
||||||
{
|
|
||||||
'type': 'text',
|
|
||||||
'content': msg.text,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return content_list
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(message: str, message_id: str, pic_url: str, content_type):
|
|
||||||
yiri_msg_list = []
|
|
||||||
yiri_msg_list.append(platform_message.Source(id=message_id, time=datetime.datetime.now()))
|
|
||||||
if pic_url is not None:
|
|
||||||
base64_url = await image.get_qq_official_image_base64(pic_url=pic_url, content_type=content_type)
|
|
||||||
yiri_msg_list.append(platform_message.Image(base64=base64_url))
|
|
||||||
|
|
||||||
yiri_msg_list.append(platform_message.Plain(text=message))
|
|
||||||
chain = platform_message.MessageChain(yiri_msg_list)
|
|
||||||
return chain
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|
||||||
@staticmethod
|
|
||||||
async def yiri2target(event: platform_events.MessageEvent) -> QQOfficialEvent:
|
|
||||||
return event.source_platform_object
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def target2yiri(event: QQOfficialEvent):
|
|
||||||
"""
|
|
||||||
QQ官方消息转换为LB对象
|
|
||||||
"""
|
|
||||||
yiri_chain = await QQOfficialMessageConverter.target2yiri(
|
|
||||||
message=event.content,
|
|
||||||
message_id=event.d_id,
|
|
||||||
pic_url=event.attachments,
|
|
||||||
content_type=event.content_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.t == 'C2C_MESSAGE_CREATE':
|
|
||||||
friend = platform_entities.Friend(
|
|
||||||
id=event.user_openid,
|
|
||||||
nickname=event.t,
|
|
||||||
remark='',
|
|
||||||
)
|
|
||||||
return platform_events.FriendMessage(
|
|
||||||
sender=friend,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp()),
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
|
|
||||||
if event.t == 'DIRECT_MESSAGE_CREATE':
|
|
||||||
friend = platform_entities.Friend(
|
|
||||||
id=event.guild_id,
|
|
||||||
nickname=event.t,
|
|
||||||
remark='',
|
|
||||||
)
|
|
||||||
return platform_events.FriendMessage(sender=friend, message_chain=yiri_chain, source_platform_object=event)
|
|
||||||
if event.t == 'GROUP_AT_MESSAGE_CREATE':
|
|
||||||
yiri_chain.insert(0, platform_message.At(target='justbot'))
|
|
||||||
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id=event.group_openid,
|
|
||||||
member_name=event.t,
|
|
||||||
permission='MEMBER',
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.group_openid,
|
|
||||||
name='MEMBER',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
)
|
|
||||||
time = int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp())
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=sender,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=time,
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
if event.t == 'AT_MESSAGE_CREATE':
|
|
||||||
yiri_chain.insert(0, platform_message.At(target='justbot'))
|
|
||||||
sender = platform_entities.GroupMember(
|
|
||||||
id=event.channel_id,
|
|
||||||
member_name=event.t,
|
|
||||||
permission='MEMBER',
|
|
||||||
group=platform_entities.Group(
|
|
||||||
id=event.channel_id,
|
|
||||||
name='MEMBER',
|
|
||||||
permission=platform_entities.Permission.Member,
|
|
||||||
),
|
|
||||||
special_title='',
|
|
||||||
join_timestamp=0,
|
|
||||||
last_speak_timestamp=0,
|
|
||||||
mute_time_remaining=0,
|
|
||||||
)
|
|
||||||
time = int(datetime.datetime.strptime(event.timestamp, '%Y-%m-%dT%H:%M:%S%z').timestamp())
|
|
||||||
return platform_events.GroupMessage(
|
|
||||||
sender=sender,
|
|
||||||
message_chain=yiri_chain,
|
|
||||||
time=time,
|
|
||||||
source_platform_object=event,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|
||||||
bot: QQOfficialClient
|
|
||||||
config: dict
|
|
||||||
bot_account_id: str
|
|
||||||
message_converter: QQOfficialMessageConverter = QQOfficialMessageConverter()
|
|
||||||
event_converter: QQOfficialEventConverter = QQOfficialEventConverter()
|
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: EventLogger):
|
|
||||||
bot = QQOfficialClient(
|
|
||||||
app_id=config['appid'], secret=config['secret'], token=config['token'], logger=logger
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
config=config,
|
|
||||||
logger=logger,
|
|
||||||
bot=bot,
|
|
||||||
bot_account_id=config['appid'],
|
|
||||||
)
|
|
||||||
|
|
||||||
async def reply_message(
|
|
||||||
self,
|
|
||||||
message_source: platform_events.MessageEvent,
|
|
||||||
message: platform_message.MessageChain,
|
|
||||||
quote_origin: bool = False,
|
|
||||||
):
|
|
||||||
qq_official_event = await QQOfficialEventConverter.yiri2target(
|
|
||||||
message_source,
|
|
||||||
)
|
|
||||||
|
|
||||||
content_list = await QQOfficialMessageConverter.yiri2target(message)
|
|
||||||
|
|
||||||
# 私聊消息
|
|
||||||
if qq_official_event.t == 'C2C_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_private_text_msg(
|
|
||||||
qq_official_event.user_openid,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 群聊消息
|
|
||||||
if qq_official_event.t == 'GROUP_AT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_group_text_msg(
|
|
||||||
qq_official_event.group_openid,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 频道群聊
|
|
||||||
if qq_official_event.t == 'AT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_channle_group_text_msg(
|
|
||||||
qq_official_event.channel_id,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 频道私聊
|
|
||||||
if qq_official_event.t == 'DIRECT_MESSAGE_CREATE':
|
|
||||||
for content in content_list:
|
|
||||||
if content['type'] == 'text':
|
|
||||||
await self.bot.send_channle_private_text_msg(
|
|
||||||
qq_official_event.guild_id,
|
|
||||||
content['content'],
|
|
||||||
qq_official_event.d_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def register_listener(
|
|
||||||
self,
|
|
||||||
event_type: typing.Type[platform_events.Event],
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
async def on_message(event: QQOfficialEvent):
|
|
||||||
self.bot_account_id = 'justbot'
|
|
||||||
try:
|
|
||||||
return await callback(await self.event_converter.target2yiri(event), self)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f'Error in qqofficial callback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
if event_type == platform_events.FriendMessage:
|
|
||||||
self.bot.on_message('DIRECT_MESSAGE_CREATE')(on_message)
|
|
||||||
self.bot.on_message('C2C_MESSAGE_CREATE')(on_message)
|
|
||||||
elif event_type == platform_events.GroupMessage:
|
|
||||||
self.bot.on_message('GROUP_AT_MESSAGE_CREATE')(on_message)
|
|
||||||
self.bot.on_message('AT_MESSAGE_CREATE')(on_message)
|
|
||||||
|
|
||||||
async def run_async(self):
|
|
||||||
async def shutdown_trigger_placeholder():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await self.bot.run_task(
|
|
||||||
host='0.0.0.0',
|
|
||||||
port=self.config['port'],
|
|
||||||
shutdown_trigger=shutdown_trigger_placeholder,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def kill(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def unregister_listener(
|
|
||||||
self,
|
|
||||||
event_type: type,
|
|
||||||
callback: typing.Callable[
|
|
||||||
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
||||||
],
|
|
||||||
):
|
|
||||||
return super().unregister_listener(event_type, callback)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user