mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Compare commits
1188 Commits
v4.0.4
...
feat/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fac52f3b9b | ||
|
|
9fbc2432e0 | ||
|
|
0b83b0c623 | ||
|
|
95b859c55d | ||
|
|
768d52f509 | ||
|
|
9e9bfbfb3d | ||
|
|
471d9d68b2 | ||
|
|
58e4b35770 | ||
|
|
056e62aa03 | ||
|
|
9330a684fe | ||
|
|
90dffa7cd8 | ||
|
|
ea6c8fba57 | ||
|
|
ce007c49c8 | ||
|
|
4e68a93df7 | ||
|
|
7247d8f221 | ||
|
|
e0e321251e | ||
|
|
8db23bf950 | ||
|
|
8063303cfa | ||
|
|
094b87e578 | ||
|
|
26923c66c0 | ||
|
|
146694539e | ||
|
|
7d6f635664 | ||
|
|
641b15c74d | ||
|
|
0cf29930a8 | ||
|
|
927388c1f7 | ||
|
|
760baa24a3 | ||
|
|
036affe01f | ||
|
|
19557c3227 | ||
|
|
b9ecb27560 | ||
|
|
b96dd8edc7 | ||
|
|
423fa0f942 | ||
|
|
948591d439 | ||
|
|
ac3989d3ba | ||
|
|
1e5acb947b | ||
|
|
74b829a288 | ||
|
|
6e982ff49d | ||
|
|
b220cf02e5 | ||
|
|
66eaa99887 | ||
|
|
5aaa422250 | ||
|
|
b7dcda8b23 | ||
|
|
3c58b9141b | ||
|
|
ddbf390d56 | ||
|
|
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 | ||
|
|
b560432b0b | ||
|
|
99e5478ced | ||
|
|
09dba91a37 | ||
|
|
18ec4adac9 | ||
|
|
8bedaa468a | ||
|
|
0ab366fcac | ||
|
|
d664039e54 | ||
|
|
6535ba4f72 | ||
|
|
3b181cff93 | ||
|
|
d1274366a0 | ||
|
|
35a4b0f55f | ||
|
|
399ebd36d7 | ||
|
|
a3552893aa | ||
|
|
b6cdf18c1a | ||
|
|
bd4c7f634d | ||
|
|
160ca540ab | ||
|
|
74c3a77ed1 | ||
|
|
0b527868bc | ||
|
|
0f35458cf7 | ||
|
|
70ad92ca16 | ||
|
|
c0d56aa905 | ||
|
|
ed869f7e81 | ||
|
|
ea42579374 | ||
|
|
72d701df3e | ||
|
|
1191b34fd4 | ||
|
|
ca3d3b2a66 | ||
|
|
2891708060 | ||
|
|
3f59bfac5c | ||
|
|
ee24582dd3 | ||
|
|
0ffb4d5792 | ||
|
|
5a6206f148 | ||
|
|
b1014313d6 | ||
|
|
fcc2f6a195 | ||
|
|
c8ffc79077 | ||
|
|
1a13a41168 | ||
|
|
bf279049c0 | ||
|
|
05cc58f2d7 | ||
|
|
d887881ea0 | ||
|
|
8bb2f3e745 | ||
|
|
e7e6eeda61 | ||
|
|
b6ff2be4df | ||
|
|
a2ea185602 | ||
|
|
5d60dbf3f9 | ||
|
|
66e252a59f | ||
|
|
8050ea1ffb | ||
|
|
04ab48de8e | ||
|
|
521a941792 | ||
|
|
6741850081 | ||
|
|
32f6d8b253 | ||
|
|
80a6b421e8 | ||
|
|
dc454b24ec | ||
|
|
0dce884519 | ||
|
|
d70196e799 | ||
|
|
2c6f127f47 | ||
|
|
72ec4b77d6 | ||
|
|
8b935175bd | ||
|
|
eae9980f5e | ||
|
|
6a7e88ffd6 | ||
|
|
e2071d9486 | ||
|
|
0b0a0c07a0 | ||
|
|
d7b354b9b4 | ||
|
|
78d36af96b | ||
|
|
6355140cd8 | ||
|
|
c224c32d03 | ||
|
|
826ceab5b8 | ||
|
|
a327182cb2 | ||
|
|
a9beb66aef | ||
|
|
ab6cf6c938 | ||
|
|
fc1e85ff16 | ||
|
|
6f98feaaf1 | ||
|
|
345c8b113f | ||
|
|
a95c422de9 | ||
|
|
93319ec2a8 | ||
|
|
e0d5469ae2 | ||
|
|
1f9f330cef | ||
|
|
f74502c711 | ||
|
|
11acd99c10 | ||
|
|
589f61931a | ||
|
|
caab1c2831 | ||
|
|
e701ceeeba | ||
|
|
2194b2975c | ||
|
|
89b25b8985 | ||
|
|
40f1af4434 | ||
|
|
91959527a4 | ||
|
|
46b4482a7d | ||
|
|
d7fc5283f7 | ||
|
|
4bdd8a021c | ||
|
|
c0ccdaf91a | ||
|
|
d9fa1cbb06 | ||
|
|
8858f432b5 | ||
|
|
e7fe41810e | ||
|
|
8f5ec48522 | ||
|
|
56183867a7 | ||
|
|
ea6ce2f552 | ||
|
|
55df728471 | ||
|
|
8a370a260e | ||
|
|
64764c412b | ||
|
|
f2d5c21712 | ||
|
|
6113c42014 | ||
|
|
fd9d1c4acc | ||
|
|
118ebddae6 | ||
|
|
2742144e12 | ||
|
|
83ff64698b | ||
|
|
b5e22c6db8 | ||
|
|
d3a147bbdd | ||
|
|
8eb1b8759b | ||
|
|
0155d3b0b9 | ||
|
|
e47a5b4e0d | ||
|
|
87ecb4e519 | ||
|
|
df524b8a7a | ||
|
|
8a7df423ab | ||
|
|
cafd623c92 | ||
|
|
4df11ef064 | ||
|
|
4012310d99 | ||
|
|
9e9bc88473 | ||
|
|
aa7c08ee00 | ||
|
|
b98de29b07 | ||
|
|
53ade384eb | ||
|
|
c7c2eb4518 | ||
|
|
37fa318258 | ||
|
|
ff7bebb782 | ||
|
|
30bb26f898 | ||
|
|
9c1f4e1690 | ||
|
|
865ee2ca01 | ||
|
|
c2264080bd | ||
|
|
67b622d5a6 | ||
|
|
a534c02d75 | ||
|
|
da890d3074 | ||
|
|
3049aa7a96 | ||
|
|
8b2480ad3b | ||
|
|
b176959836 | ||
|
|
a0c42a5f6e | ||
|
|
e66f674968 | ||
|
|
dd0e0abdc4 | ||
|
|
13f6396eb4 | ||
|
|
7bbaa4fcad | ||
|
|
e931d5eb88 | ||
|
|
4bbfa2f1d7 | ||
|
|
17d997c88e | ||
|
|
dd30d08c68 | ||
|
|
0ea7609ff1 | ||
|
|
28d4b1dd61 | ||
|
|
5179b3e53a | ||
|
|
8ccda10045 | ||
|
|
46fbfbefea | ||
|
|
288b294148 | ||
|
|
b464d238c5 | ||
|
|
e1a78e8ff9 | ||
|
|
2b8eb5f01c | ||
|
|
8f863cf530 | ||
|
|
2351193c51 | ||
|
|
bf2bc70794 | ||
|
|
ebe0b68e8f | ||
|
|
8c87a47f5a | ||
|
|
b8b9a37825 | ||
|
|
13dd6fcee3 | ||
|
|
39c50d3c12 | ||
|
|
29f0075bd8 | ||
|
|
8a96ffbcc0 | ||
|
|
67f68d8101 | ||
|
|
ad59d92cef | ||
|
|
85f97860c5 | ||
|
|
8fd21e76f2 | ||
|
|
cc83ddbe21 | ||
|
|
99fcde1586 | ||
|
|
eab08dfbf3 | ||
|
|
dbf0200cca | ||
|
|
ac44f35299 | ||
|
|
d6a5fdd911 | ||
|
|
4668db716a | ||
|
|
f7cd6b76f2 | ||
|
|
b6d47187f5 | ||
|
|
051fffd41e | ||
|
|
c5480078b3 | ||
|
|
e744e9c4ef | ||
|
|
9f22b8b585 | ||
|
|
27cee0a4e1 | ||
|
|
6d35fc408c | ||
|
|
0607a0fa5c | ||
|
|
ed57d2fafa | ||
|
|
39ef92676b | ||
|
|
7301476228 | ||
|
|
457cc3eecd | ||
|
|
a381069bcc | ||
|
|
146c38e64c | ||
|
|
763c41729e | ||
|
|
0021efebd7 | ||
|
|
5f18a1b13a | ||
|
|
0124448479 | ||
|
|
621f1301b3 | ||
|
|
e76bc80e51 | ||
|
|
a27560e804 | ||
|
|
46452de7b5 | ||
|
|
2aef139577 | ||
|
|
03b11481ed | ||
|
|
8c5cb71812 | ||
|
|
7c59bc1ce5 | ||
|
|
0b60ef0d06 | ||
|
|
eede354d3b | ||
|
|
eb7b5dcc25 | ||
|
|
47e9ce96fc | ||
|
|
4e95bc542c | ||
|
|
e4f321ea7a | ||
|
|
246eb71b75 | ||
|
|
261f50b8ec | ||
|
|
9736d0708a | ||
|
|
02dbe80d2f | ||
|
|
0f239ace17 | ||
|
|
3a82ae8da5 | ||
|
|
c33c9eaab0 | ||
|
|
87f626f3cc | ||
|
|
e88302f1b4 | ||
|
|
5597dffaeb | ||
|
|
7f25d61531 | ||
|
|
15e524c6e6 | ||
|
|
4a1d033ee9 | ||
|
|
8adc88a8c0 | ||
|
|
a62b38eda7 | ||
|
|
fcef784180 | ||
|
|
c3ed4ef6a1 | ||
|
|
b9f768af25 | ||
|
|
47ff883fc7 | ||
|
|
68906c43ff | ||
|
|
c6deed4e6e | ||
|
|
b45cc59322 | ||
|
|
c33a96823b | ||
|
|
d3ab16761d | ||
|
|
70f23f24b0 | ||
|
|
00a8410c94 | ||
|
|
2a17e89a99 | ||
|
|
8fe0992c15 | ||
|
|
a9776b7b53 | ||
|
|
074d359c8e | ||
|
|
7728b4262b | ||
|
|
4905b5a738 | ||
|
|
43a259a1ae | ||
|
|
cffe493db0 | ||
|
|
0042629bf0 | ||
|
|
a7d638cc9a | ||
|
|
f84a79bf74 | ||
|
|
f5a0cb9175 | ||
|
|
f9a5507029 | ||
|
|
5ce32d2f04 | ||
|
|
4908996cac | ||
|
|
ee545a163f | ||
|
|
6e0e5802cc | ||
|
|
0d53843230 | ||
|
|
b65670cd1a | ||
|
|
ba4b5255a2 | ||
|
|
d60af2b451 | ||
|
|
44ac8b2b63 | ||
|
|
b70001c579 | ||
|
|
4a8f5516f6 | ||
|
|
48d11540ae | ||
|
|
84129e3339 | ||
|
|
377d455ec1 | ||
|
|
41650b585a | ||
|
|
52280d7a05 | ||
|
|
0ce81a2df2 | ||
|
|
d9a2bb9a06 | ||
|
|
cb88da7f02 | ||
|
|
5560a4f52d | ||
|
|
e4d951b174 | ||
|
|
6e08bf71c9 | ||
|
|
daaf4b54ef | ||
|
|
3291266f5d | ||
|
|
307f6acd8c | ||
|
|
f1ac9c77e6 | ||
|
|
b434a4e3d7 | ||
|
|
2f209cd59f | ||
|
|
0f585fd5ef | ||
|
|
a152dece9a | ||
|
|
d3b31f7027 | ||
|
|
c00f05fca4 | ||
|
|
92c3a86356 | ||
|
|
341fdc409d | ||
|
|
ebd542f592 | ||
|
|
194b2d9814 | ||
|
|
7aed5cf1ed | ||
|
|
abc88c4979 | ||
|
|
3fa38f71f1 | ||
|
|
d651d956d6 | ||
|
|
6754666845 | ||
|
|
08e6f46b19 | ||
|
|
8f8c8ff367 | ||
|
|
63ec2a8c34 | ||
|
|
f58c8497c3 | ||
|
|
1497fdae56 | ||
|
|
10a3cb40e1 | ||
|
|
dd1ec15a39 | ||
|
|
ea51cec57e | ||
|
|
28ce986a8c | ||
|
|
489b145606 | ||
|
|
5e92bffaa6 | ||
|
|
277d1b0e30 | ||
|
|
13f4ed8d2c | ||
|
|
91cb5ca36c | ||
|
|
c34d54a6cb | ||
|
|
2d1737da1f | ||
|
|
adb0bf2473 | ||
|
|
a1b8b9d47b | ||
|
|
8df14bf9d9 | ||
|
|
c98d265a1e | ||
|
|
4e6782a6b7 | ||
|
|
5541e9e6d0 | ||
|
|
878ab0ef6b | ||
|
|
b61bd36b14 | ||
|
|
bb672d8f46 | ||
|
|
ba1a26543b | ||
|
|
cb868ee7b2 | ||
|
|
5dd5cb12ad | ||
|
|
2dfa83ff22 | ||
|
|
27bb4e1253 | ||
|
|
45afdbdfbb | ||
|
|
11e52a3ade | ||
|
|
4cbbe9e000 | ||
|
|
e986a0acaf | ||
|
|
f5b893cfe0 | ||
|
|
333ec346ef | ||
|
|
2f2db4d445 | ||
|
|
e31883547d | ||
|
|
88c0066b06 | ||
|
|
fdc79b8d77 | ||
|
|
f244795e57 | ||
|
|
5a2aa19d0f | ||
|
|
f731115805 | ||
|
|
67bc065ccd | ||
|
|
d15df3338f | ||
|
|
c74cf38e9f | ||
|
|
81eb92646f | ||
|
|
019a9317e9 | ||
|
|
0e68a922bd | ||
|
|
4e1d81c9f8 | ||
|
|
199164fc4b | ||
|
|
c9c26213df | ||
|
|
b7c57104c4 | ||
|
|
0be08d8882 | ||
|
|
e0abd19636 | ||
|
|
4380041c7f | ||
|
|
65814a4644 | ||
|
|
7237294008 | ||
|
|
214bc8ada9 | ||
|
|
6a1de889b4 | ||
|
|
4a319b2b20 | ||
|
|
9f269d1614 | ||
|
|
4b57771eb1 | ||
|
|
5922be7e15 | ||
|
|
858cfd8d5a | ||
|
|
cbe297dc59 | ||
|
|
de76fed25a | ||
|
|
301509b1db | ||
|
|
a10e61735d | ||
|
|
1ef0193028 | ||
|
|
1e85d02ae4 | ||
|
|
d78a329aa9 | ||
|
|
bfdf238db5 | ||
|
|
234b61e2f8 | ||
|
|
9f43097361 | ||
|
|
f395cac893 | ||
|
|
fe122281fd | ||
|
|
6d788cadbc | ||
|
|
a79a22a74d | ||
|
|
2ed3b68790 | ||
|
|
bd9331ce62 | ||
|
|
14c161b733 | ||
|
|
815cdf8b4a | ||
|
|
7d5503dab2 | ||
|
|
9ba1ad5bd3 | ||
|
|
367d04d0f0 | ||
|
|
75c3ddde19 | ||
|
|
c6e77e42be | ||
|
|
4d0a39eb65 | ||
|
|
10a44c70b6 | ||
|
|
ac03a2dceb | ||
|
|
56248c350f | ||
|
|
244aaf6e20 | ||
|
|
5b044a1917 | ||
|
|
cd25340826 | ||
|
|
ebd8e014c6 | ||
|
|
a0b7d759ac | ||
|
|
09884d3152 | ||
|
|
bef0d73e83 | ||
|
|
8d28ace252 | ||
|
|
39c062f73e | ||
|
|
0e5c9e19e1 | ||
|
|
01f2ef5694 | ||
|
|
c5b62b6ba3 | ||
|
|
bbf583ddb5 | ||
|
|
22ef1a399e | ||
|
|
0733f8878f | ||
|
|
f36a61dbb2 | ||
|
|
6d8936bd74 | ||
|
|
d2b93b3296 | ||
|
|
552fee9bac | ||
|
|
34fe8b324d | ||
|
|
c4671fbf1c | ||
|
|
4bcc06c955 | ||
|
|
348f6d9eaa | ||
|
|
157ffdc34c | ||
|
|
c81d5a1a49 | ||
|
|
a01706d163 | ||
|
|
a8d03c98dc | ||
|
|
68cdd163d3 | ||
|
|
4005a8a3e2 | ||
|
|
3f0153ea4d | ||
|
|
60b50a35f1 | ||
|
|
abd02f04af | ||
|
|
a60aa6f644 | ||
|
|
542409d48d | ||
|
|
1a10b40b17 | ||
|
|
e2124054bf | ||
|
|
3c6e858c35 | ||
|
|
ee3da8aa17 | ||
|
|
8670ae82a3 | ||
|
|
14411a8af6 | ||
|
|
c246470b37 | ||
|
|
48c9d66ab8 | ||
|
|
f474e42b79 | ||
|
|
5553a86ac8 | ||
|
|
01613b2f0d | ||
|
|
a177786063 | ||
|
|
62b2884011 | ||
|
|
6b782f8761 | ||
|
|
0c2560cafb | ||
|
|
c5eeab2fd0 | ||
|
|
6f2fd72af6 | ||
|
|
2d06f1cadb | ||
|
|
af493c117c | ||
|
|
896fef8cce | ||
|
|
89c1972abe | ||
|
|
1627d04958 | ||
|
|
c959c99e45 | ||
|
|
0eac9135c0 | ||
|
|
0203faa8c1 | ||
|
|
35f76cb7ae | ||
|
|
c34232a26c | ||
|
|
b43dd95dc6 | ||
|
|
5331ba83d7 | ||
|
|
a2038b86f1 | ||
|
|
eb066f3485 | ||
|
|
bf98b82cf2 | ||
|
|
edd70b943d | ||
|
|
3cbc823085 | ||
|
|
48becf2c51 | ||
|
|
56c686cd5a | ||
|
|
208273c0dd | ||
|
|
2ff7ca3025 | ||
|
|
61a2361730 | ||
|
|
f80f997a89 | ||
|
|
18529a42c1 | ||
|
|
3e707b4b6e | ||
|
|
62f0a938a8 | ||
|
|
ad3a163d82 | ||
|
|
f5a4503610 | ||
|
|
ec012cf5ed | ||
|
|
d70eceb72c | ||
|
|
f271608114 | ||
|
|
793f0a9c10 | ||
|
|
4f2ec195fc | ||
|
|
e6bc009414 | ||
|
|
20dc8fb5ab | ||
|
|
9a71edfeb0 | ||
|
|
fe3fd664af | ||
|
|
6402755ac6 | ||
|
|
ac8fe049de | ||
|
|
955b391253 | ||
|
|
08c6672841 | ||
|
|
8917050fae | ||
|
|
21daef46f7 | ||
|
|
8ad60b5b64 | ||
|
|
7e17c96c30 | ||
|
|
f17b06767e | ||
|
|
70a29fc623 |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.github
|
||||||
|
.venv
|
||||||
|
.vscode
|
||||||
|
.data
|
||||||
|
.temp
|
||||||
|
web/.next
|
||||||
|
web/node_modules
|
||||||
|
web/.env
|
||||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: 漏洞反馈
|
name: 漏洞反馈
|
||||||
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://link.langbot.app/zh/docs/network
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -19,7 +19,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 复现步骤
|
label: 复现步骤
|
||||||
description: 如何重现这个问题,越详细越好;提供越多信息,我们会越快解决问题。
|
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL),我们将更快回复您。**
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
2
.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:
|
||||||
|
|||||||
13
.github/pull_request_template.md
vendored
13
.github/pull_request_template.md
vendored
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
> 请在此部分填写你实现/解决/优化的内容:
|
> 请在此部分填写你实现/解决/优化的内容:
|
||||||
> Summary of what you implemented/solved/optimized:
|
> 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
|
||||||
|
|
||||||
@@ -9,7 +20,7 @@
|
|||||||
|
|
||||||
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
||||||
|
|
||||||
- [ ] 阅读仓库[贡献指引](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/RockChinQ/LangBot/blob/master/CONTRIBUTING.md)?
|
- [ ] 阅读仓库[贡献指引](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)了吗? / Have you read the [contribution guide](https://github.com/langbot-app/LangBot/blob/master/CONTRIBUTING.md)?
|
||||||
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
|
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
|
||||||
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.
|
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/build-docker-image.yml
vendored
8
.github/workflows/build-docker-image.yml
vendored
@@ -1,7 +1,5 @@
|
|||||||
name: Build Docker Image
|
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 }}
|
||||||
136
.github/workflows/run-tests.yml
vendored
Normal file
136
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
name: Unit Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, ready_for_review, synchronize]
|
||||||
|
paths:
|
||||||
|
- 'src/langbot/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- '.github/workflows/run-tests.yml'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
|
- 'run_tests.sh'
|
||||||
|
- 'scripts/test-*.sh'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'src/langbot/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- '.github/workflows/run-tests.yml'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
|
- 'run_tests.sh'
|
||||||
|
- 'scripts/test-*.sh'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Unit Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.11', '3.12', '3.13']
|
||||||
|
fail-fast: false
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Run unit + smoke tests
|
||||||
|
run: uv run pytest tests/unit_tests/ tests/smoke/ -q --tb=short
|
||||||
|
|
||||||
|
- name: Test Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Unit Tests Results" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
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
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -22,7 +22,7 @@ tips.py
|
|||||||
venv*
|
venv*
|
||||||
bin/
|
bin/
|
||||||
.vscode
|
.vscode
|
||||||
test_*
|
/test_*
|
||||||
venv/
|
venv/
|
||||||
hugchat.json
|
hugchat.json
|
||||||
qcapi
|
qcapi
|
||||||
@@ -42,4 +42,17 @@ botpy.log*
|
|||||||
test.py
|
test.py
|
||||||
/web_ui
|
/web_ui
|
||||||
.venv/
|
.venv/
|
||||||
uv.lock
|
/test
|
||||||
|
plugins.bak
|
||||||
|
coverage.xml
|
||||||
|
.coverage
|
||||||
|
src/langbot/web/
|
||||||
|
testsdk/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Next.js build cache (legacy)
|
||||||
|
web/.next/
|
||||||
|
|||||||
37
.mcp.json
Normal file
37
.mcp.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"shadcn@latest",
|
||||||
|
"mcp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sequential-thinking": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||||
|
"env": {}
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||||
|
"env": {
|
||||||
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "uvx",
|
||||||
|
"args": ["mcp-server-fetch"],
|
||||||
|
"env": {}
|
||||||
|
},
|
||||||
|
"playwright": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp@latest"],
|
||||||
|
"env": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,16 +9,14 @@ repos:
|
|||||||
# Run the formatter of backend.
|
# 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
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
3.12
|
|
||||||
88
AGENTS.md
Normal file
88
AGENTS.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file is for guiding code agents (like Claude Code, GitHub Copilot, OpenAI Codex, etc.) to work in LangBot project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
LangBot is a open-source LLM native instant messaging bot development platform, aiming to provide an out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, supporting global instant messaging platforms, and providing rich API interfaces, supporting custom development.
|
||||||
|
|
||||||
|
LangBot has a comprehensive frontend, all operations can be performed through the frontend. The project splited into these major parts:
|
||||||
|
|
||||||
|
- `./src/langbot`: The main python package of the project, below are the main modules in this package:
|
||||||
|
- `./pkg`: The core python package of the project backend.
|
||||||
|
- `./pkg/platform`: The platform module of the project, containing the logic of message platform adapters, bot managers, message session managers, etc.
|
||||||
|
- `./pkg/provider`: The provider module of the project, containing the logic of LLM providers, tool providers, etc.
|
||||||
|
- `./pkg/pipeline`: The pipeline module of the project, containing the logic of pipelines, stages, query pool, etc.
|
||||||
|
- `./pkg/api`: The api module of the project, containing the http api controllers and services.
|
||||||
|
- `./pkg/plugin`: LangBot bridge for connecting with plugin system.
|
||||||
|
- `./libs`: Some SDKs we previously developed for the project, such as `qq_official_api`, `wecom_api`, etc.
|
||||||
|
- `./templates`: Templates of config files, components, etc.
|
||||||
|
- `./web`: Frontend codebase, built with Next.js + **shadcn** + **Tailwind CSS**.
|
||||||
|
- `./docker`: docker-compose deployment files.
|
||||||
|
|
||||||
|
## Backend Development
|
||||||
|
|
||||||
|
We use `uv` to manage dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uv
|
||||||
|
uv sync --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the backend and run the project in development mode.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can access the project at `http://127.0.0.1:5300`.
|
||||||
|
|
||||||
|
## Frontend Development
|
||||||
|
|
||||||
|
We use `pnpm` to manage dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
cp .env.example .env
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can access the project at `http://127.0.0.1:3000`.
|
||||||
|
|
||||||
|
## Plugin System Architecture
|
||||||
|
|
||||||
|
LangBot is composed of various internal components such as Large Language Model tools, commands, messaging platform adapters, LLM requesters, and more. To meet extensibility and flexibility requirements, we have implemented a production-grade plugin system.
|
||||||
|
|
||||||
|
Each plugin runs in an independent process, managed uniformly by the Plugin Runtime. It has two operating modes: `stdio` and `websocket`. When LangBot is started directly by users (not running in a container), it uses `stdio` mode, which is common for personal users or lightweight environments. When LangBot runs in a container, it uses `websocket` mode, designed specifically for production environments.
|
||||||
|
|
||||||
|
Plugin Runtime automatically starts each installed plugin and interacts through stdio. In plugin development scenarios, developers can use the lbp command-line tool to start plugins and connect to the running Runtime via WebSocket for debugging.
|
||||||
|
|
||||||
|
> Plugin SDK, CLI, Runtime, and entities definitions shared between LangBot and plugins are contained in the [`langbot-plugin-sdk`](https://github.com/langbot-app/langbot-plugin-sdk) repository.
|
||||||
|
|
||||||
|
## Some Development Tips and Standards
|
||||||
|
|
||||||
|
- LangBot is a global project, any comments in code should be in English, and user experience should be considered in all aspects.
|
||||||
|
- Thus you should consider the i18n support in all aspects.
|
||||||
|
- LangBot is widely adopted in both toC and toB scenarios, so you should consider the compatibility and security in all aspects.
|
||||||
|
- If you were asked to make a commit, please follow the commit message format:
|
||||||
|
- format: <type>(<scope>): <subject>
|
||||||
|
- type: must be a specific type, such as feat (new feature), fix (bug fix), docs (documentation), style (code style), refactor (refactoring), perf (performance optimization), etc.
|
||||||
|
- scope: the scope of the commit, such as the package name, the file name, the function name, the class name, the module name, etc.
|
||||||
|
- subject: the subject of the commit, such as the description of the commit, the reason for the commit, the impact of the commit, etc.
|
||||||
|
- LangBot uses [Alembic](https://alembic.sqlalchemy.org/) to manage database migrations, supporting both SQLite and PostgreSQL. Migration files are located in `src/langbot/pkg/persistence/alembic/versions/`. If you changed the definition of database entities (ORM models), generate a new migration script by running `uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "description of your change"` in the project root (requires `data/config.yaml` to exist). Review and edit the generated script before committing. Migrations are executed automatically on LangBot startup. For data migrations (e.g. modifying JSON field content), you need to manually add the migration code in the generated script.
|
||||||
|
|
||||||
|
## Some Principles
|
||||||
|
|
||||||
|
- Keep it simple, stupid.
|
||||||
|
- Entities should not be multiplied unnecessarily
|
||||||
|
- 八荣八耻
|
||||||
|
|
||||||
|
以瞎猜接口为耻,以认真查询为荣。
|
||||||
|
以模糊执行为耻,以寻求确认为荣。
|
||||||
|
以臆想业务为耻,以人类确认为荣。
|
||||||
|
以创造接口为耻,以复用现有为荣。
|
||||||
|
以跳过验证为耻,以主动测试为荣。
|
||||||
|
以破坏架构为耻,以遵循规范为荣。
|
||||||
|
以假装理解为耻,以诚实无知为荣。
|
||||||
|
以盲目修改为耻,以谨慎重构为荣。
|
||||||
@@ -4,7 +4,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY web ./web
|
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" ]
|
||||||
862
LICENSE
862
LICENSE
@@ -1,661 +1,201 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
Apache License
|
||||||
Version 3, 19 November 2007
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
1. Definitions.
|
||||||
Preamble
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
other entities that control, are controlled by, or are under common
|
||||||
share and change all versions of a program--to make sure it remains free
|
control with that entity. For the purposes of this definition,
|
||||||
software for all its users.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
When we speak of free software, we are referring to freedom, not
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
want it, that you can change the software or use pieces of it in new
|
exercising permissions granted by this License.
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
Developers that use our General Public Licenses protect your rights
|
including but not limited to software source code, documentation
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
source, and configuration files.
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
A secondary benefit of defending all users' freedom is that
|
not limited to compiled object code, generated documentation,
|
||||||
improvements made in alternate versions of the program, if they
|
and conversions to other media types.
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
Object form, made available under the License, as indicated by a
|
||||||
software used on network servers, this result may fail to come about.
|
copyright notice that is included in or attached to the work
|
||||||
The GNU General Public License permits making a modified version and
|
(an example is provided in the Appendix below).
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
The GNU Affero General Public License is designed specifically to
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
ensure that, in such cases, the modified source code becomes available
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
to the community. It requires the operator of a network server to
|
of this License, Derivative Works shall not include works that remain
|
||||||
provide the source code of the modified version running there to the
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
users of that server. Therefore, public use of a modified version, on
|
the Work and Derivative Works thereof.
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
An older license, called the Affero General Public License and
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
this license.
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
The precise terms and conditions for copying, distribution and
|
communication on electronic mailing lists, source code control systems,
|
||||||
modification follow.
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
TERMS AND CONDITIONS
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
0. Definitions.
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
"recipients" may be individuals or organizations.
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
earlier work or a work "based on" the earlier work.
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
A "covered work" means either the unmodified Program or a work based
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
on the Program.
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
To "propagate" a work means to do anything with it that, without
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
permission, would make you directly or secondarily liable for
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
infringement under applicable copyright law, except executing it on a
|
institute patent litigation against any entity (including a
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
distribution (with or without modification), making available to the
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
public, and in some countries other activities as well.
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
To "convey" a work means any kind of propagation that enables other
|
as of the date such litigation is filed.
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
modifications, and in Source or Object form, provided that You
|
||||||
to the extent that it includes a convenient and prominently visible
|
meet the following conditions:
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
(a) You must give any other recipients of the Work or
|
||||||
extent that warranties are provided), that licensees may convey the
|
Derivative Works a copy of this License; and
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
(b) You must cause any modified files to carry prominent notices
|
||||||
menu, a prominent item in the list meets this criterion.
|
stating that You changed the files; and
|
||||||
|
|
||||||
1. Source Code.
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
The "source code" for a work means the preferred form of the work
|
attribution notices from the Source form of the Work,
|
||||||
for making modifications to it. "Object code" means any non-source
|
excluding those notices that do not pertain to any part of
|
||||||
form of a work.
|
the Derivative Works; and
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
standard defined by a recognized standards body, or, in the case of
|
distribution, then any Derivative Works that You distribute must
|
||||||
interfaces specified for a particular programming language, one that
|
include a readable copy of the attribution notices contained
|
||||||
is widely used among developers working in that language.
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
The "System Libraries" of an executable work include anything, other
|
of the following places: within a NOTICE text file distributed
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
as part of the Derivative Works; within the Source form or
|
||||||
packaging a Major Component, but which is not part of that Major
|
documentation, if provided along with the Derivative Works; or,
|
||||||
Component, and (b) serves only to enable use of the work with that
|
within a display generated by the Derivative Works, if and
|
||||||
Major Component, or to implement a Standard Interface for which an
|
wherever such third-party notices normally appear. The contents
|
||||||
implementation is available to the public in source code form. A
|
of the NOTICE file are for informational purposes only and
|
||||||
"Major Component", in this context, means a major essential component
|
do not modify the License. You may add Your own attribution
|
||||||
(kernel, window system, and so on) of the specific operating system
|
notices within Derivative Works that You distribute, alongside
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
produce the work, or an object code interpreter used to run it.
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
You may add Your own copyright statement to Your modifications and
|
||||||
work) run the object code and to modify the work, including scripts to
|
may provide additional or different license terms and conditions
|
||||||
control those activities. However, it does not include the work's
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
System Libraries, or general-purpose tools or generally available free
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
programs which are used unmodified in performing those activities but
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
which are not part of the work. For example, Corresponding Source
|
the conditions stated in this License.
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
linked subprograms that the work is specifically designed to require,
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
such as by intimate data communication or control flow between those
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
subprograms and other parts of the work.
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
The Corresponding Source need not include anything that users
|
the terms of any separate license agreement you may have executed
|
||||||
can regenerate automatically from other parts of the Corresponding
|
with Licensor regarding such Contributions.
|
||||||
Source.
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
The Corresponding Source for a work in source code form is that
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
same work.
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
2. Basic Permissions.
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
All rights granted under this License are granted for the term of
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
permission to run the unmodified Program. The output from running a
|
implied, including, without limitation, any warranties or conditions
|
||||||
covered work is covered by this License only if the output, given its
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
content, constitutes a covered work. This License acknowledges your
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
in force. You may convey covered works to others for the sole purpose
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
of having them make modifications exclusively for you, or provide you
|
unless required by applicable law (such as deliberate and grossly
|
||||||
with facilities for running those works, provided that you comply with
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
the terms of this License in conveying all material for which you do
|
liable to You for damages, including any direct, indirect, special,
|
||||||
not control copyright. Those thus making or running the covered works
|
incidental, or consequential damages of any character arising as a
|
||||||
for you must do so exclusively on your behalf, under your direction
|
result of this License or out of the use or inability to use the
|
||||||
and control, on terms that prohibit them from making any copies of
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
your copyrighted material outside their relationship with you.
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
Conveying under any other circumstances is permitted solely under
|
has been advised of the possibility of such damages.
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
No covered work shall be deemed part of an effective technological
|
License. However, in accepting such obligations, You may act only
|
||||||
measure under any applicable law fulfilling obligations under article
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
similar laws prohibiting or restricting circumvention of such
|
defend, and hold each Contributor harmless for any liability
|
||||||
measures.
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
END OF TERMS AND CONDITIONS
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
To apply the Apache License to your work, attach the following
|
||||||
technological measures.
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
4. Conveying Verbatim Copies.
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
You may convey verbatim copies of the Program's source code as you
|
file or class name and description of purpose be included on the
|
||||||
receive it, in any medium, provided that you conspicuously and
|
same "printed page" as the copyright notice for easier
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
identification within third-party archives.
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
Copyright [yyyy] [name of copyright owner]
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
You may charge any price or no price for each copy that you convey,
|
You may obtain a copy of the License at
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
You may convey a work based on the Program, or the modifications to
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
produce it from the Program, in the form of source code under the
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
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/
|
||||||
230
README.md
230
README.md
@@ -1,156 +1,176 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
<a href="https://langbot.app">项目主页</a> |
|
<h3>Production-grade platform for building agentic IM bots.</h3>
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a> |
|
<h4>Quickly build, debug, and ship AI bots to Slack, Discord, Telegram, WeChat, and more.</h4>
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a> |
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
|
|
||||||
|
|
||||||
<div align="center">
|
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)
|
||||||
😎高稳定、🧩支持扩展、🦄多模态 - 大模型原生即时通信机器人平台🤖
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
[](https://github.com/RockChinQ/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)
|
||||||
|
|
||||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
<a href="https://langbot.app">Website</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Features</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Docs</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app/cloud">Cloud</a> |
|
||||||
|
<a href="https://space.langbot.app">Plugin Market</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> 近期 GeWeChat 项目归档,我们已经适配 WeChatPad 协议端,个微恢复正常使用,详情请查看文档。
|
---
|
||||||
|
|
||||||
## ✨ 特性
|
## What is LangBot?
|
||||||
|
|
||||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
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.
|
||||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
|
||||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
|
||||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
|
||||||
|
|
||||||
## 📦 开始使用
|
### Key Capabilities
|
||||||
|
|
||||||
#### Docker Compose 部署
|
- **AI Conversations & Agents** — Multi-turn dialogues, tool calling, multi-modal support, streaming output. Built-in RAG (knowledge base) with deep integration to [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Universal IM Platform Support** — One codebase for Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Production-Ready** — Access control, rate limiting, sensitive word filtering, comprehensive monitoring, and exception handling. Trusted by enterprises.
|
||||||
|
- **Plugin Ecosystem** — Hundreds of plugins, event-driven architecture, component extensions, and [MCP protocol](https://modelcontextprotocol.io/) support.
|
||||||
|
- **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required.
|
||||||
|
- **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling.
|
||||||
|
|
||||||
|
[→ Learn more about all features](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recommended)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Zero deployment, ready to use.
|
||||||
|
|
||||||
|
### One-Line Launch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/RockChinQ/LangBot
|
uvx langbot
|
||||||
cd LangBot
|
```
|
||||||
|
|
||||||
|
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
docker compose up -d
|
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
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="450px"/>
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Official |
|
||||||
|
| Telegram | ✅ | Official |
|
||||||
|
| Slack | ✅ | Official |
|
||||||
|
| LINE | ✅ | Official |
|
||||||
|
| QQ | ✅ | Personal & Official API (Channel, DM, Group) |
|
||||||
|
| WeCom | ✅ | Enterprise WeChat, External CS, AI Bot |
|
||||||
|
| WeChat | ✅ | Personal & Official Account |
|
||||||
|
| Lark | ✅ | Official |
|
||||||
|
| DingTalk | ✅ | Official |
|
||||||
|
| KOOK | ✅ | Official |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Supports multiple bridged platforms such as Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip, and more |
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="450px"/>
|
---
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="450px"/>
|
## Supported LLMs & Integrations
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="450px"/>
|
| Provider | Type | Status |
|
||||||
|
| ----------------------------------------------------------------------------------------------------------------- | ------------ | ------ |
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | Local LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | Local LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocol | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Gateway | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Gateway | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Gateway | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Gateway | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Gateway | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU Platform | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU Platform | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU Platform | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Gateway | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Gateway | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Gateway | ✅ |
|
||||||
|
|
||||||
<img alt="回复效果(带有联网插件)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
[→ View all integrations](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
- WebUI Demo: https://demo.langbot.dev/
|
---
|
||||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
|
||||||
- 注意:仅展示webui效果,公开环境,请不要在其中填入您的任何敏感信息。
|
|
||||||
|
|
||||||
## 🔌 组件兼容性
|
## Why LangBot?
|
||||||
|
|
||||||
### 消息平台
|
| Use Case | How LangBot Helps |
|
||||||
|
| --------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
|
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
|
||||||
|
| **Internal Tools** | Connect n8n/Dify workflows to WeCom/DingTalk for automated business processes |
|
||||||
|
| **Community Management** | Moderate QQ/Discord groups with AI-powered content filtering and interaction |
|
||||||
|
| **Multi-Platform Presence** | One bot, all platforms. Manage from a single dashboard |
|
||||||
|
|
||||||
| 平台 | 状态 | 备注 |
|
---
|
||||||
| --- | --- | --- |
|
|
||||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
|
||||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
|
||||||
| 企业微信 | ✅ | |
|
|
||||||
| 企微对外客服 | ✅ | |
|
|
||||||
| 个人微信 | ✅ | |
|
|
||||||
| 微信公众号 | ✅ | |
|
|
||||||
| 飞书 | ✅ | |
|
|
||||||
| 钉钉 | ✅ | |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | 🚧 | |
|
|
||||||
| WhatsApp | 🚧 | |
|
|
||||||
|
|
||||||
🚧: 正在开发中
|
## Live Demo
|
||||||
|
|
||||||
### 大模型能力
|
**Try it now:** https://demo.langbot.dev/
|
||||||
|
|
||||||
| 模型 | 状态 | 备注 |
|
- Email: `demo@langbot.app`
|
||||||
| --- | --- | --- |
|
- Password: `langbot123456`
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
|
||||||
|
|
||||||
### TTS
|
_Note: Public demo environment. Do not enter sensitive information._
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
---
|
||||||
| --- | --- |
|
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
|
||||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
|
||||||
|
|
||||||
### 文生图
|
## Community
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
[](https://discord.gg/wdNEHETs87)
|
||||||
| --- | --- |
|
|
||||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
|
||||||
|
|
||||||
## 😘 社区贡献
|
- [Discord Community](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
感谢以下[代码贡献者](https://github.com/RockChinQ/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
|
---
|
||||||
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
## Star History
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contributors) who have helped make LangBot better:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
199
README_CN.md
Normal file
199
README_CN.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生产级 AI 即时通信机器人开发平台。</h3>
|
||||||
|
<h4>快速构建、调试和部署 AI 机器人到微信、QQ、飞书、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">官网</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/guide">文档</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app/cloud">Cloud</a> |
|
||||||
|
<a href="https://space.langbot.app">插件市场</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路线图</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
LangBot 是一个**开源的生产级平台**,用于构建 AI 驱动的即时通信机器人。它将大语言模型(LLM)连接到各种聊天平台,帮助你创建能够对话、执行任务、并集成到现有工作流程中的智能 Agent。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 对话与 Agent** — 多轮对话、工具调用、多模态、流式输出。自带 RAG(知识库),深度集成 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||||
|
- **全平台支持** — 一套代码,覆盖 QQ、微信、企业微信、飞书、钉钉、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生产就绪** — 访问控制、限速、敏感词过滤、全面监控与异常处理,已被多家企业采用。
|
||||||
|
- **插件生态** — 数百个插件,跨进程的事件驱动架构,组件扩展,适配 [MCP 协议](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
|
||||||
|
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推荐)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,开箱即用。
|
||||||
|
|
||||||
|
### 一键启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)。访问 http://localhost:5300 即可使用。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一键云部署
|
||||||
|
|
||||||
|
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手动部署](https://link.langbot.app/zh/docs/manual-deploy) · [宝塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的平台
|
||||||
|
|
||||||
|
| 平台 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| QQ | ✅ | 个人号、官方机器人(频道、私聊、群聊) |
|
||||||
|
| 微信 | ✅ | 个人微信、微信公众号 |
|
||||||
|
| 企业微信 | ✅ | 应用消息、对外客服、智能机器人 |
|
||||||
|
| 飞书 | ✅ | 官方 |
|
||||||
|
| 钉钉 | ✅ | 官方 |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Discord | ✅ | 官方 |
|
||||||
|
| Telegram | ✅ | 官方 |
|
||||||
|
| Slack | ✅ | 官方 |
|
||||||
|
| LINE | ✅ | 官方 |
|
||||||
|
| KOOK | ✅ | 官方 |
|
||||||
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支持多种桥接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的大模型与集成
|
||||||
|
|
||||||
|
| 提供商 | 类型 | 状态 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智谱AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 协议 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里云百炼](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [小马算力](https://www.tokenpony.cn/453z1) | 聚合平台 | ✅ |
|
||||||
|
| [百宝箱Tbox](https://www.tbox.cn/open) | 智能体平台 | ✅ |
|
||||||
|
| [七牛云Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
[→ 查看完整集成列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
### TTS(语音合成)
|
||||||
|
|
||||||
|
| 平台/模型 | 备注 |
|
||||||
|
|-----------|------|
|
||||||
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
|
|
||||||
|
### 文生图
|
||||||
|
|
||||||
|
| 平台/模型 | 备注 |
|
||||||
|
|-----------|------|
|
||||||
|
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么选择 LangBot?
|
||||||
|
|
||||||
|
| 使用场景 | LangBot 如何帮助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客户服务** | 将 AI Agent 部署到微信/企微/钉钉/飞书,基于知识库自动回答用户问题 |
|
||||||
|
| **内部工具** | 将 n8n/Dify 工作流接入企微/钉钉,实现业务流程自动化 |
|
||||||
|
| **社群运营** | 在 QQ/Discord 群中使用 AI 驱动的内容审核与智能互动 |
|
||||||
|
| **多平台触达** | 一个机器人,覆盖所有平台。通过统一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 在线演示
|
||||||
|
|
||||||
|
**立即体验:** https://demo.langbot.dev/
|
||||||
|
- 邮箱:`demo@langbot.app`
|
||||||
|
- 密码:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公开演示环境,请不要在其中填入任何敏感信息。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社区
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
- [Discord 社区](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社区群](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趋势
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
感谢所有[贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)对 LangBot 的帮助:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## For Code Agents
|
||||||
|
|
||||||
|
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
|
||||||
|
-->
|
||||||
137
README_EN.md
137
README_EN.md
@@ -1,137 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://langbot.app">
|
|
||||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
|
||||||
|
|
||||||
<a href="https://langbot.app">Home</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
😎High Stability, 🧩Extension Supported, 🦄Multi-modal - LLM Native Instant Messaging Bot Platform🤖
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
|
||||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
|
||||||
[](https://github.com/RockChinQ/LangBot/releases/latest)
|
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
|
||||||
|
|
||||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, and multi-modal capabilities. Deeply integrates with [Dify](https://dify.ai). Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
|
||||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
|
||||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
|
||||||
- 😻 [New] Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
|
||||||
|
|
||||||
## 📦 Getting Started
|
|
||||||
|
|
||||||
#### Docker Compose Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/RockChinQ/LangBot
|
|
||||||
cd LangBot
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:5300 to start using it.
|
|
||||||
|
|
||||||
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
|
||||||
|
|
||||||
#### One-click Deployment on BTPanel
|
|
||||||
|
|
||||||
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
|
|
||||||
|
|
||||||
#### Zeabur Cloud Deployment
|
|
||||||
|
|
||||||
Community contributed Zeabur template.
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railway Cloud Deployment
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
|
||||||
|
|
||||||
#### Other Deployment Methods
|
|
||||||
|
|
||||||
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
|
|
||||||
|
|
||||||
## 📸 Demo
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="Reply Effect (with Internet Plugin)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
|
||||||
|
|
||||||
- WebUI Demo: https://demo.langbot.dev/
|
|
||||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
|
||||||
- Note: Only the WebUI effect is shown, please do not fill in any sensitive information in the public environment.
|
|
||||||
|
|
||||||
## 🔌 Component Compatibility
|
|
||||||
|
|
||||||
### Message Platform
|
|
||||||
|
|
||||||
| Platform | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Personal QQ | ✅ | |
|
|
||||||
| QQ Official API | ✅ | |
|
|
||||||
| WeCom | ✅ | |
|
|
||||||
| WeComCS | ✅ | |
|
|
||||||
| Personal WeChat | ✅ | |
|
|
||||||
| Lark | ✅ | |
|
|
||||||
| DingTalk | ✅ | |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | 🚧 | |
|
|
||||||
| WhatsApp | 🚧 | |
|
|
||||||
|
|
||||||
🚧: In development
|
|
||||||
|
|
||||||
### LLMs
|
|
||||||
|
|
||||||
| LLM | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
|
|
||||||
|
|
||||||
## 🤝 Community Contribution
|
|
||||||
|
|
||||||
Thank you for the following [code contributors](https://github.com/RockChinQ/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
|
||||||
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
|
||||||
</a>
|
|
||||||
174
README_ES.md
Normal file
174
README_ES.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Plataforma de grado de producción para construir bots de mensajería instantánea con agentes de IA.</h3>
|
||||||
|
<h4>Construya, depure y despliegue bots de IA rápidamente en Slack, Discord, Telegram, WeChat y más.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / Español / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Inicio</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Características</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Documentación</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Mercado de Plugins</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Hoja de Ruta</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ¿Qué es LangBot?
|
||||||
|
|
||||||
|
LangBot es una **plataforma de código abierto y grado de producción** para construir bots de mensajería instantánea impulsados por IA. Conecta modelos de lenguaje de gran escala (LLMs) con cualquier plataforma de chat, permitiéndole crear agentes inteligentes que pueden conversar, ejecutar tareas e integrarse con sus flujos de trabajo existentes.
|
||||||
|
|
||||||
|
### Capacidades Clave
|
||||||
|
|
||||||
|
- **Conversaciones e Agentes IA** — Diálogos de múltiples turnos, llamadas a herramientas, soporte multimodal, salida en streaming. RAG (base de conocimientos) incorporado con integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Soporte Universal de Plataformas de MI** — Un solo código base para Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Listo para Producción** — Control de acceso, limitación de velocidad, filtrado de palabras sensibles, monitoreo completo y manejo de excepciones. De confianza para empresas.
|
||||||
|
- **Ecosistema de Plugins** — Cientos de plugins, arquitectura basada en eventos, extensiones de componentes y soporte del [protocolo MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML.
|
||||||
|
- **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones.
|
||||||
|
|
||||||
|
[→ Conocer más sobre todas las funcionalidades](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inicio Rápido
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recomendado)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sin despliegue, listo para usar.
|
||||||
|
|
||||||
|
### Lanzamiento en una línea
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requiere [uv](https://docs.astral.sh/uv/getting-started/installation/). Visite http://localhost:5300 — listo.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Despliegue en la Nube con un Clic
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Más opciones:** [Docker](https://link.langbot.app/en/docs/docker) · [Manual](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plataformas Soportadas
|
||||||
|
|
||||||
|
| Plataforma | Estado | Notas |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Oficial |
|
||||||
|
| Telegram | ✅ | Oficial |
|
||||||
|
| Slack | ✅ | Oficial |
|
||||||
|
| LINE | ✅ | Oficial |
|
||||||
|
| QQ | ✅ | Personal y API Oficial (Canal, DM, Grupo) |
|
||||||
|
| WeCom | ✅ | WeChat Empresarial, CS Externo, AI Bot |
|
||||||
|
| WeChat | ✅ | Personal y Cuenta Oficial |
|
||||||
|
| Lark | ✅ | Oficial |
|
||||||
|
| DingTalk | ✅ | Oficial |
|
||||||
|
| KOOK | ✅ | Oficial |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Admite varias plataformas puenteadas como Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip y más |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLMs e Integraciones Soportadas
|
||||||
|
|
||||||
|
| Proveedor | Tipo | Estado |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocolo | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Pasarela | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Pasarela | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Pasarela | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Pasarela | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Pasarela | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plataforma GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plataforma GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plataforma GPU | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Pasarela | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Pasarela | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Pasarela | ✅ |
|
||||||
|
|
||||||
|
[→ Ver todas las integraciones](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ¿Por qué LangBot?
|
||||||
|
|
||||||
|
| Caso de Uso | Cómo Ayuda LangBot |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Atención al cliente** | Despliegue agentes de IA en Slack/Discord/Telegram que respondan preguntas usando su base de conocimientos |
|
||||||
|
| **Herramientas internas** | Conecte flujos de trabajo de n8n/Dify a WeCom/DingTalk para procesos empresariales automatizados |
|
||||||
|
| **Gestión de comunidades** | Modere grupos de QQ/Discord con filtrado de contenido e interacción impulsados por IA |
|
||||||
|
| **Presencia multiplataforma** | Un solo bot, todas las plataformas. Gestione desde un único panel de control |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo en Vivo
|
||||||
|
|
||||||
|
**Pruébelo ahora:** https://demo.langbot.dev/
|
||||||
|
- Correo electrónico: `demo@langbot.app`
|
||||||
|
- Contraseña: `langbot123456`
|
||||||
|
|
||||||
|
*Nota: Entorno de demostración público. No ingrese información confidencial.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comunidad
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Comunidad de Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historial de Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Colaboradores
|
||||||
|
|
||||||
|
Gracias a todos los [colaboradores](https://github.com/langbot-app/LangBot/graphs/contributors) que han ayudado a mejorar LangBot:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
174
README_FR.md
Normal file
174
README_FR.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Plateforme de niveau production pour construire des bots de messagerie instantanée avec agents IA.</h3>
|
||||||
|
<h4>Créez, déboguez et déployez rapidement des bots IA sur Slack, Discord, Telegram, WeChat et plus.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / Français / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Accueil</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Fonctionnalités</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Documentation</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Marché des Plugins</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Feuille de Route</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qu'est-ce que LangBot ?
|
||||||
|
|
||||||
|
LangBot est une **plateforme open-source de niveau production** pour créer des bots de messagerie instantanée alimentés par l'IA. Elle connecte les grands modèles de langage (LLMs) à n'importe quelle plateforme de chat, vous permettant de créer des agents intelligents capables de converser, d'exécuter des tâches et de s'intégrer à vos workflows existants.
|
||||||
|
|
||||||
|
### Capacités Clés
|
||||||
|
|
||||||
|
- **Conversations IA & Agents** — Dialogues multi-tours, appels d'outils, support multimodal, sortie en streaming. RAG (base de connaissances) intégré avec intégration profonde de [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Support Universel des Plateformes de MI** — Un seul code pour Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Prêt pour la Production** — Contrôle d'accès, limitation de débit, filtrage de mots sensibles, surveillance complète et gestion des exceptions. Approuvé par les entreprises.
|
||||||
|
- **Écosystème de Plugins** — Des centaines de plugins, architecture événementielle, extensions de composants, et support du [protocole MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise.
|
||||||
|
- **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions.
|
||||||
|
|
||||||
|
[→ En savoir plus sur toutes les fonctionnalités](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démarrage Rapide
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recommandé)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Sans déploiement, prêt à utiliser.
|
||||||
|
|
||||||
|
### Lancement en une ligne
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nécessite [uv](https://docs.astral.sh/uv/getting-started/installation/). Visitez http://localhost:5300 — c'est prêt.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Déploiement Cloud en un Clic
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Plus d'options :** [Docker](https://link.langbot.app/en/docs/docker) · [Manuel](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plateformes Supportées
|
||||||
|
|
||||||
|
| Plateforme | Statut | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Officiel |
|
||||||
|
| Telegram | ✅ | Officiel |
|
||||||
|
| Slack | ✅ | Officiel |
|
||||||
|
| LINE | ✅ | Officiel |
|
||||||
|
| QQ | ✅ | Personnel & API Officielle (Canal, DM, Groupe) |
|
||||||
|
| WeCom | ✅ | WeChat Entreprise, CS Externe, AI Bot |
|
||||||
|
| WeChat | ✅ | Personnel & Compte Officiel |
|
||||||
|
| Lark | ✅ | Officiel |
|
||||||
|
| DingTalk | ✅ | Officiel |
|
||||||
|
| KOOK | ✅ | Officiel |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Prend en charge plusieurs plateformes via ponts, comme Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip, etc. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLMs et Intégrations Supportés
|
||||||
|
|
||||||
|
| Fournisseur | Type | Statut |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM Local | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM Local | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocole | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Passerelle | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Passerelle | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Passerelle | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Passerelle | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Passerelle | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Passerelle | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Passerelle | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Plateforme GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Plateforme GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Plateforme GPU | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Passerelle | ✅ |
|
||||||
|
|
||||||
|
[→ Voir toutes les intégrations](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pourquoi LangBot ?
|
||||||
|
|
||||||
|
| Cas d'Usage | Comment LangBot Aide |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Support Client** | Déployez des agents IA sur Slack/Discord/Telegram qui répondent aux questions en utilisant votre base de connaissances |
|
||||||
|
| **Outils Internes** | Connectez les workflows n8n/Dify à WeCom/DingTalk pour automatiser vos processus métier |
|
||||||
|
| **Gestion de Communauté** | Modérez les groupes QQ/Discord avec un filtrage de contenu et des interactions alimentés par l'IA |
|
||||||
|
| **Présence Multi-plateforme** | Un seul bot, toutes les plateformes. Gérez tout depuis un tableau de bord unique |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démo en Ligne
|
||||||
|
|
||||||
|
**Essayez maintenant :** https://demo.langbot.dev/
|
||||||
|
- Email : `demo@langbot.app`
|
||||||
|
- Mot de passe : `langbot123456`
|
||||||
|
|
||||||
|
*Note : Environnement de démonstration public. Ne saisissez pas d'informations sensibles.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Communauté
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Communauté Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historique des Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributeurs
|
||||||
|
|
||||||
|
Merci à tous les [contributeurs](https://github.com/langbot-app/LangBot/graphs/contributors) qui ont aidé à améliorer LangBot :
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
230
README_JP.md
230
README_JP.md
@@ -1,136 +1,174 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://trendshift.io/repositories/12901" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12901" alt="RockChinQ%2FLangBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
<a href="https://langbot.app">ホーム</a> |
|
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
|
|
||||||
|
|
||||||
<div align="center">
|
[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)
|
||||||
😎高い安定性、🧩拡張サポート、🦄マルチモーダル - LLMネイティブインスタントメッセージングボットプラットフォーム🤖
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://deepwiki.com/RockChinQ/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/RockChinQ/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)
|
||||||
|
|
||||||
[简体中文](README.md) / [English](README_EN.md) / [日本語](README_JP.md) / (PR for your language)
|
<a href="https://langbot.app">ホーム</a> |
|
||||||
|
<a href="https://link.langbot.app/ja/docs/features">機能</a> |
|
||||||
|
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</a> |
|
||||||
|
<a href="https://link.langbot.app/ja/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">プラグインマーケット</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## ✨ 機能
|
---
|
||||||
|
|
||||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル機能をサポート。 [Dify](https://dify.ai) と深く統合。現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
## LangBot とは?
|
||||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
|
||||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
|
||||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
|
||||||
|
|
||||||
## 📦 始め方
|
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||||
|
|
||||||
#### Docker Compose デプロイ
|
### 主な機能
|
||||||
|
|
||||||
|
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAG(ナレッジベース)を内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) と深く統合。
|
||||||
|
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
|
||||||
|
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
|
||||||
|
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
|
||||||
|
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
|
||||||
|
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
|
||||||
|
|
||||||
|
[→ すべての機能について詳しく見る](https://link.langbot.app/ja/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## クイックスタート
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推奨)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — デプロイ不要、すぐに使えます。
|
||||||
|
|
||||||
|
### ワンライン起動
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/RockChinQ/LangBot
|
uvx langbot
|
||||||
cd 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
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
http://localhost:5300 にアクセスして使用を開始します。
|
### ワンクリッククラウドデプロイ
|
||||||
|
|
||||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
|
||||||
|
|
||||||
#### BTPanelでのワンクリックデプロイ
|
|
||||||
|
|
||||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
|
||||||
|
|
||||||
#### Zeaburクラウドデプロイ
|
|
||||||
|
|
||||||
コミュニティが提供するZeaburテンプレート。
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
[](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)のドキュメントを参照してください。
|
---
|
||||||
|
|
||||||
## 📸 デモ
|
## 対応プラットフォーム
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/bot-page.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/create-model.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/edit-pipeline.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="bots" src="https://docs.langbot.app/webui/plugin-market.png" width="400px"/>
|
|
||||||
|
|
||||||
<img alt="返信効果(インターネットプラグイン付き)" src="https://docs.langbot.app/QChatGPT-0516.png" width="500px"/>
|
|
||||||
|
|
||||||
- WebUIデモ: https://demo.langbot.dev/
|
|
||||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
|
||||||
- 注意: WebUIの効果のみを示しています。公開環境では、機密情報を入力しないでください。
|
|
||||||
|
|
||||||
## 🔌 コンポーネントの互換性
|
|
||||||
|
|
||||||
### メッセージプラットフォーム
|
|
||||||
|
|
||||||
| プラットフォーム | ステータス | 備考 |
|
| プラットフォーム | ステータス | 備考 |
|
||||||
| --- | --- | --- |
|
|----------|--------|-------|
|
||||||
| 個人QQ | ✅ | |
|
| Discord | ✅ | 公式 |
|
||||||
| QQ公式API | ✅ | |
|
| Telegram | ✅ | 公式 |
|
||||||
| WeCom | ✅ | |
|
| Slack | ✅ | 公式 |
|
||||||
| WeComCS | ✅ | |
|
| LINE | ✅ | 公式 |
|
||||||
| 個人WeChat | ✅ | |
|
| QQ | ✅ | 個人・公式API(チャンネル・DM・グループ) |
|
||||||
| Lark | ✅ | |
|
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||||
| DingTalk | ✅ | |
|
| WeChat | ✅ | 個人・公式アカウント |
|
||||||
| Discord | ✅ | |
|
| Lark | ✅ | 公式 |
|
||||||
| Telegram | ✅ | |
|
| DingTalk | ✅ | 公式 |
|
||||||
| Slack | ✅ | |
|
| KOOK | ✅ | 公式 |
|
||||||
| LINE | 🚧 | |
|
| Satori | ✅ | |
|
||||||
| WhatsApp | 🚧 | |
|
| Email | ✅ | Matrix、Satori |
|
||||||
|
| Matrix | ✅ | Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip など複数のブリッジ先プラットフォームに対応 |
|
||||||
|
|
||||||
🚧: 開発中
|
---
|
||||||
|
|
||||||
### LLMs
|
## 対応LLMと統合
|
||||||
|
|
||||||
| LLM | ステータス | 備考 |
|
| プロバイダー | タイプ | ステータス |
|
||||||
| --- | --- | --- |
|
|----------|------|--------|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
|
||||||
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
|
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
|
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
|
||||||
| [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) | ゲートウェイ | ✅ |
|
||||||
| [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) | ゲートウェイ | ✅ |
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
|
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | ゲートウェイ | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ |
|
||||||
|
|
||||||
## 🤝 コミュニティ貢献
|
[→ すべての統合を表示](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/RockChinQ/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
---
|
||||||
|
|
||||||
<a href="https://github.com/RockChinQ/LangBot/graphs/contributors">
|
## なぜ LangBot?
|
||||||
<img src="https://contrib.rocks/image?repo=RockChinQ/LangBot" />
|
|
||||||
|
| ユースケース | LangBot の活用方法 |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
|
||||||
|
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
|
||||||
|
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
|
||||||
|
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ライブデモ
|
||||||
|
|
||||||
|
**今すぐ試す:** https://demo.langbot.dev/
|
||||||
|
- メール: `demo@langbot.app`
|
||||||
|
- パスワード: `langbot123456`
|
||||||
|
|
||||||
|
*注意: 公開デモ環境です。機密情報を入力しないでください。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コミュニティ
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 推移
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コントリビューター
|
||||||
|
|
||||||
|
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
174
README_KO.md
Normal file
174
README_KO.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>AI 에이전트 IM 봇 구축을 위한 프로덕션 등급 플랫폼.</h3>
|
||||||
|
<h4>Slack, Discord, Telegram, WeChat 등에 AI 봇을 빠르게 구축, 디버그 및 배포.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / 한국어 / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">홈</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">기능</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">문서</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">플러그인 마켓</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">로드맵</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LangBot이란?
|
||||||
|
|
||||||
|
LangBot은 AI 기반 인스턴트 메시징 봇을 구축하기 위한 **오픈소스 프로덕션 등급 플랫폼**입니다. 대규모 언어 모델(LLM)을 모든 채팅 플랫폼에 연결하여 대화, 작업 실행, 기존 워크플로우와의 통합이 가능한 지능형 에이전트를 만들 수 있습니다.
|
||||||
|
|
||||||
|
### 핵심 기능
|
||||||
|
|
||||||
|
- **AI 대화 및 에이전트** — 멀티턴 대화, 도구 호출, 멀티모달 지원, 스트리밍 출력. 내장 RAG(지식 베이스)와 [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org) 심층 통합.
|
||||||
|
- **유니버설 IM 플랫폼 지원** — 단일 코드베이스로 Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK 지원.
|
||||||
|
- **프로덕션 레디** — 접근 제어, 속도 제한, 민감어 필터링, 종합 모니터링 및 예외 처리. 기업 환경에서 검증됨.
|
||||||
|
- **플러그인 생태계** — 수백 개의 플러그인, 이벤트 기반 아키텍처, 컴포넌트 확장, [MCP 프로토콜](https://modelcontextprotocol.io/) 지원.
|
||||||
|
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
|
||||||
|
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
|
||||||
|
|
||||||
|
[→ 모든 기능 자세히 보기](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (추천)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 배포 없이 바로 사용.
|
||||||
|
|
||||||
|
### 원라인 실행
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> [uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요. http://localhost:5300 방문 — 완료.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 원클릭 클라우드 배포
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**더 많은 옵션:** [Docker](https://link.langbot.app/en/docs/docker) · [수동 배포](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 플랫폼
|
||||||
|
|
||||||
|
| 플랫폼 | 상태 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| Discord | ✅ | 공식 |
|
||||||
|
| Telegram | ✅ | 공식 |
|
||||||
|
| Slack | ✅ | 공식 |
|
||||||
|
| LINE | ✅ | 공식 |
|
||||||
|
| QQ | ✅ | 개인 및 공식 API (채널, DM, 그룹) |
|
||||||
|
| WeCom | ✅ | 기업 WeChat, 외부 CS, AI Bot |
|
||||||
|
| WeChat | ✅ | 개인 및 공식 계정 |
|
||||||
|
| Lark | ✅ | 공식 |
|
||||||
|
| DingTalk | ✅ | 공식 |
|
||||||
|
| KOOK | ✅ | 공식 |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip 등 여러 브리지 플랫폼 지원 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 LLM 및 통합
|
||||||
|
|
||||||
|
| 제공자 | 유형 | 상태 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 로컬 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 로컬 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 프로토콜 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 게이트웨이 | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | 게이트웨이 | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 게이트웨이 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 게이트웨이 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 게이트웨이 | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 플랫폼 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 플랫폼 | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 플랫폼 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 게이트웨이 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 게이트웨이 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 게이트웨이 | ✅ |
|
||||||
|
|
||||||
|
[→ 모든 통합 보기](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 왜 LangBot인가?
|
||||||
|
|
||||||
|
| 사용 사례 | LangBot 활용 방법 |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| **고객 지원** | 지식 베이스를 활용하여 질문에 답변하는 AI 에이전트를 Slack/Discord/Telegram에 배포 |
|
||||||
|
| **내부 도구** | n8n/Dify 워크플로우를 WeCom/DingTalk에 연결하여 비즈니스 프로세스 자동화 |
|
||||||
|
| **커뮤니티 관리** | AI 기반 콘텐츠 필터링 및 상호작용으로 QQ/Discord 그룹 관리 |
|
||||||
|
| **멀티 플랫폼** | 하나의 봇으로 모든 플랫폼 지원. 단일 대시보드에서 관리 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 라이브 데모
|
||||||
|
|
||||||
|
**지금 체험:** https://demo.langbot.dev/
|
||||||
|
- 이메일: `demo@langbot.app`
|
||||||
|
- 비밀번호: `langbot123456`
|
||||||
|
|
||||||
|
*참고: 공개 데모 환경입니다. 민감한 정보를 입력하지 마세요.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 커뮤니티
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord 커뮤니티](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 추이
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기여자
|
||||||
|
|
||||||
|
LangBot을 더 나은 프로젝트로 만들어 주신 모든 [기여자](https://github.com/langbot-app/LangBot/graphs/contributors)분들께 감사드립니다:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
174
README_RU.md
Normal file
174
README_RU.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Платформа производственного уровня для создания агентных IM-ботов.</h3>
|
||||||
|
<h4>Быстро создавайте, отлаживайте и развертывайте ИИ-ботов в Slack, Discord, Telegram, WeChat и других платформах.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / Русский / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Главная</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Возможности</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Документация</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Магазин плагинов</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Дорожная карта</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что такое LangBot?
|
||||||
|
|
||||||
|
LangBot — это **платформа с открытым исходным кодом производственного уровня** для создания ИИ-ботов в мессенджерах. Она связывает большие языковые модели (LLM) с любой чат-платформой, позволяя создавать интеллектуальных агентов, которые могут вести диалоги, выполнять задачи и интегрироваться с вашими существующими рабочими процессами.
|
||||||
|
|
||||||
|
### Ключевые возможности
|
||||||
|
|
||||||
|
- **ИИ-диалоги и агенты** — Многораундовые диалоги, вызов инструментов, мультимодальная поддержка, потоковый вывод. Встроенная реализация RAG (база знаний) с глубокой интеграцией в [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Универсальная поддержка IM-платформ** — Единая кодовая база для Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Готовность к продакшену** — Контроль доступа, ограничение скорости, фильтрация чувствительных слов, комплексный мониторинг и обработка исключений. Проверено в корпоративной среде.
|
||||||
|
- **Экосистема плагинов** — Сотни плагинов, событийно-ориентированная архитектура, расширения компонентов и поддержка [протокола MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
|
||||||
|
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
|
||||||
|
|
||||||
|
[→ Подробнее обо всех возможностях](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Рекомендуется)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Без развёртывания, готово к использованию.
|
||||||
|
|
||||||
|
### Запуск одной командой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Требуется [uv](https://docs.astral.sh/uv/getting-started/installation/). Откройте http://localhost:5300 — готово.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Облачное развертывание одним кликом
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Другие варианты:** [Docker](https://link.langbot.app/en/docs/docker) · [Ручная установка](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые платформы
|
||||||
|
|
||||||
|
| Платформа | Статус | Примечания |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| Discord | ✅ | Официальный |
|
||||||
|
| Telegram | ✅ | Официальный |
|
||||||
|
| Slack | ✅ | Официальный |
|
||||||
|
| LINE | ✅ | Официальный |
|
||||||
|
| QQ | ✅ | Личный и официальный API (Канал, ЛС, Группа) |
|
||||||
|
| WeCom | ✅ | Корпоративный WeChat, внешний CS, AI-бот |
|
||||||
|
| WeChat | ✅ | Личный и официальный аккаунт |
|
||||||
|
| Lark | ✅ | Официальный |
|
||||||
|
| DingTalk | ✅ | Официальный |
|
||||||
|
| KOOK | ✅ | Официальный |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Поддерживает несколько платформ через мосты, включая Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip и другие |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые LLM и интеграции
|
||||||
|
|
||||||
|
| Провайдер | Тип | Статус |
|
||||||
|
|-----------|-----|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | Локальный LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | Локальный LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Протокол | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Шлюз | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Шлюз | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Шлюз | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Шлюз | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Шлюз | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Шлюз | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Шлюз | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Платформа GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Платформа GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Платформа GPU | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Шлюз | ✅ |
|
||||||
|
|
||||||
|
[→ Смотреть все интеграции](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Почему LangBot?
|
||||||
|
|
||||||
|
| Сценарий использования | Как помогает LangBot |
|
||||||
|
|------------------------|----------------------|
|
||||||
|
| **Поддержка клиентов** | Разверните ИИ-агентов в Slack/Discord/Telegram, которые отвечают на вопросы, используя вашу базу знаний |
|
||||||
|
| **Внутренние инструменты** | Подключите рабочие процессы n8n/Dify к WeCom/DingTalk для автоматизации бизнес-процессов |
|
||||||
|
| **Управление сообществом** | Модерируйте группы QQ/Discord с помощью ИИ-фильтрации контента и взаимодействия |
|
||||||
|
| **Мультиплатформенное присутствие** | Один бот — все платформы. Управляйте из единой панели |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Демо
|
||||||
|
|
||||||
|
**Попробуйте прямо сейчас:** https://demo.langbot.dev/
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Пароль: `langbot123456`
|
||||||
|
|
||||||
|
*Примечание: Публичная демо-среда. Не вводите конфиденциальную информацию.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сообщество
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Сообщество Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## История Stars
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Участники
|
||||||
|
|
||||||
|
Спасибо всем [участникам](https://github.com/langbot-app/LangBot/graphs/contributors), которые помогли сделать LangBot лучше:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
192
README_TW.md
Normal file
192
README_TW.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
|
||||||
|
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">官網</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/guide">文件</a> |
|
||||||
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">外掛市場</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 什麼是 LangBot?
|
||||||
|
|
||||||
|
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型(LLM)連接到各種聊天平台,幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG(知識庫),深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org) 等 LLMOps 平台。
|
||||||
|
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
|
||||||
|
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
|
||||||
|
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud(推薦)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,開箱即用。
|
||||||
|
|
||||||
|
### 一鍵啟動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)。訪問 http://localhost:5300 即可使用。
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一鍵雲端部署
|
||||||
|
|
||||||
|
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**更多方式:** [Docker](https://link.langbot.app/zh/docs/docker) · [手動部署](https://link.langbot.app/zh/docs/manual-deploy) · [寶塔面板](https://link.langbot.app/zh/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支援的平台
|
||||||
|
|
||||||
|
| 平台 | 狀態 | 備註 |
|
||||||
|
|------|------|------|
|
||||||
|
| Discord | ✅ | 官方 |
|
||||||
|
| Telegram | ✅ | 官方 |
|
||||||
|
| Slack | ✅ | 官方 |
|
||||||
|
| LINE | ✅ | 官方 |
|
||||||
|
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||||
|
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||||
|
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||||
|
| 飛書 | ✅ | 官方 |
|
||||||
|
| 釘釘 | ✅ | 官方 |
|
||||||
|
| KOOK | ✅ | 官方 |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支援多種橋接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支援的大模型與整合
|
||||||
|
|
||||||
|
| 提供商 | 類型 | 狀態 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
### TTS(語音合成)
|
||||||
|
|
||||||
|
| 平台/模型 | 備註 |
|
||||||
|
|-----------|------|
|
||||||
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
|
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
|
|
||||||
|
### 文生圖
|
||||||
|
|
||||||
|
| 平台/模型 | 備註 |
|
||||||
|
|-----------|------|
|
||||||
|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
|
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 為什麼選擇 LangBot?
|
||||||
|
|
||||||
|
| 使用場景 | LangBot 如何幫助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
|
||||||
|
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
|
||||||
|
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
|
||||||
|
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 線上演示
|
||||||
|
|
||||||
|
**立即體驗:** https://demo.langbot.dev/
|
||||||
|
- 信箱:`demo@langbot.app`
|
||||||
|
- 密碼:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
- [Discord 社群](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 貢獻者
|
||||||
|
|
||||||
|
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
174
README_VI.md
Normal file
174
README_VI.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://langbot.app">
|
||||||
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>Nền tảng cấp sản xuất để xây dựng bot IM với AI agent.</h3>
|
||||||
|
<h4>Xây dựng, gỡ lỗi và triển khai bot AI nhanh chóng trên Slack, Discord, Telegram, WeChat và nhiều nền tảng khác.</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / Tiếng Việt
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Trang chủ</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Tính năng</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Tài liệu</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">Chợ Plugin</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Lộ trình</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LangBot là gì?
|
||||||
|
|
||||||
|
LangBot là một **nền tảng mã nguồn mở, cấp sản xuất** để xây dựng bot nhắn tin tức thời được hỗ trợ bởi AI. Nó kết nối các Mô hình Ngôn ngữ Lớn (LLM) với bất kỳ nền tảng chat nào, cho phép bạn tạo các agent thông minh có thể trò chuyện, thực hiện tác vụ và tích hợp với quy trình làm việc hiện có của bạn.
|
||||||
|
|
||||||
|
### Khả năng chính
|
||||||
|
|
||||||
|
- **Hội thoại AI & Agent** — Đối thoại nhiều lượt, gọi công cụ, hỗ trợ đa phương thức, đầu ra streaming. RAG (cơ sở kiến thức) tích hợp sẵn với tích hợp sâu vào [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org).
|
||||||
|
- **Hỗ trợ đa nền tảng IM** — Một mã nguồn cho Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Sẵn sàng cho sản xuất** — Kiểm soát truy cập, giới hạn tốc độ, lọc từ nhạy cảm, giám sát toàn diện và xử lý ngoại lệ. Được doanh nghiệp tin dùng.
|
||||||
|
- **Hệ sinh thái Plugin** — Hàng trăm plugin, kiến trúc hướng sự kiện, mở rộng thành phần, và hỗ trợ [giao thức MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML.
|
||||||
|
- **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ.
|
||||||
|
|
||||||
|
[→ Tìm hiểu thêm về tất cả tính năng](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bắt đầu nhanh
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Khuyên dùng)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Không cần triển khai, sẵn sàng sử dụng.
|
||||||
|
|
||||||
|
### Khởi chạy một dòng
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Yêu cầu [uv](https://docs.astral.sh/uv/getting-started/installation/). Truy cập http://localhost:5300 — xong.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triển khai đám mây một cú nhấp
|
||||||
|
|
||||||
|
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||||
|
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||||
|
|
||||||
|
**Thêm tùy chọn:** [Docker](https://link.langbot.app/en/docs/docker) · [Thủ công](https://link.langbot.app/en/docs/manual-deploy) · [BTPanel](https://link.langbot.app/en/docs/bt-panel) · [Kubernetes](./docker/README_K8S.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nền tảng được hỗ trợ
|
||||||
|
|
||||||
|
| Nền tảng | Trạng thái | Ghi chú |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Chính thức |
|
||||||
|
| Telegram | ✅ | Chính thức |
|
||||||
|
| Slack | ✅ | Chính thức |
|
||||||
|
| LINE | ✅ | Chính thức |
|
||||||
|
| QQ | ✅ | Cá nhân & API chính thức (Kênh, DM, Nhóm) |
|
||||||
|
| WeCom | ✅ | WeChat doanh nghiệp, CS bên ngoài, AI Bot |
|
||||||
|
| WeChat | ✅ | Cá nhân & Tài khoản công khai |
|
||||||
|
| Lark | ✅ | Chính thức |
|
||||||
|
| DingTalk | ✅ | Chính thức |
|
||||||
|
| KOOK | ✅ | Chính thức |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Hỗ trợ nhiều nền tảng qua bridge như Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip và hơn thế nữa |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LLM và tích hợp được hỗ trợ
|
||||||
|
|
||||||
|
| Nhà cung cấp | Loại | Trạng thái |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | LLM cục bộ | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | LLM cục bộ | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Giao thức | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Cổng | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Cổng | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Cổng | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Cổng | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Cổng | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | Nền tảng GPU | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | Nền tảng GPU | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Cổng | ✅ |
|
||||||
|
| [302.AI](https://share.302.ai/SuTG99) | Cổng | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Cổng | ✅ |
|
||||||
|
|
||||||
|
[→ Xem tất cả tích hợp](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tại sao chọn LangBot?
|
||||||
|
|
||||||
|
| Trường hợp sử dụng | LangBot giúp như thế nào |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **Hỗ trợ khách hàng** | Triển khai agent AI trên Slack/Discord/Telegram để trả lời câu hỏi bằng cơ sở kiến thức của bạn |
|
||||||
|
| **Công cụ nội bộ** | Kết nối quy trình n8n/Dify với WeCom/DingTalk để tự động hóa quy trình kinh doanh |
|
||||||
|
| **Quản lý cộng đồng** | Quản lý nhóm QQ/Discord với tính năng lọc nội dung và tương tác được hỗ trợ bởi AI |
|
||||||
|
| **Đa nền tảng** | Một bot, tất cả nền tảng. Quản lý từ một bảng điều khiển duy nhất |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Demo trực tuyến
|
||||||
|
|
||||||
|
**Thử ngay:** https://demo.langbot.dev/
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Mật khẩu: `langbot123456`
|
||||||
|
|
||||||
|
*Lưu ý: Môi trường demo công khai. Không nhập thông tin nhạy cảm.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cộng đồng
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Cộng đồng Discord](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lịch sử Star
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Người đóng góp
|
||||||
|
|
||||||
|
Cảm ơn tất cả [người đóng góp](https://github.com/langbot-app/LangBot/graphs/contributors) đã giúp LangBot trở nên tốt hơn:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
4
codecov.yml
Normal file
4
codecov.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project: off
|
||||||
|
patch: off
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
langbot:
|
|
||||||
image: rockchin/langbot:latest
|
|
||||||
container_name: langbot
|
|
||||||
volumes:
|
|
||||||
- ./data:/app/data
|
|
||||||
- ./plugins:/app/plugins
|
|
||||||
restart: on-failure
|
|
||||||
environment:
|
|
||||||
- TZ=Asia/Shanghai
|
|
||||||
ports:
|
|
||||||
- 5300:5300 # 供 WebUI 使用
|
|
||||||
- 2280-2290:2280-2290 # 供消息平台适配器方向连接
|
|
||||||
# 根据具体环境配置网络
|
|
||||||
629
docker/README_K8S.md
Normal file
629
docker/README_K8S.md
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
# LangBot Kubernetes 部署指南 / Kubernetes Deployment Guide
|
||||||
|
|
||||||
|
[简体中文](#简体中文) | [English](#english)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 简体中文
|
||||||
|
|
||||||
|
### 概述
|
||||||
|
|
||||||
|
本指南提供了在 Kubernetes 集群中部署 LangBot 的完整步骤。Kubernetes 部署配置基于 `docker-compose.yaml`,适用于生产环境的容器化部署。
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
|
||||||
|
- Kubernetes 集群(版本 1.19+)
|
||||||
|
- `kubectl` 命令行工具已配置并可访问集群
|
||||||
|
- 集群中有可用的存储类(StorageClass)用于持久化存储(可选但推荐)
|
||||||
|
- 至少 2 vCPU 和 4GB RAM 的可用资源
|
||||||
|
|
||||||
|
### 架构说明
|
||||||
|
|
||||||
|
Kubernetes 部署包含以下组件:
|
||||||
|
|
||||||
|
1. **langbot**: 主应用服务
|
||||||
|
- 提供 Web UI(端口 5300)
|
||||||
|
- 处理平台 webhook(端口 2280-2290)
|
||||||
|
- 数据持久化卷
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: 插件运行时服务
|
||||||
|
- WebSocket 通信(端口 5400)
|
||||||
|
- 插件数据持久化卷
|
||||||
|
|
||||||
|
3. **持久化存储**:
|
||||||
|
- `langbot-data`: LangBot 主数据
|
||||||
|
- `langbot-plugins`: 插件文件
|
||||||
|
- `langbot-plugin-runtime-data`: 插件运行时数据
|
||||||
|
|
||||||
|
### 快速开始
|
||||||
|
|
||||||
|
#### 1. 下载部署文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# 或直接下载 kubernetes.yaml
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 部署到 Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 应用所有配置
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# 检查部署状态
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# 查看 Pod 日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 访问 LangBot
|
||||||
|
|
||||||
|
默认情况下,LangBot 服务使用 ClusterIP 类型,只能在集群内部访问。您可以选择以下方式之一来访问:
|
||||||
|
|
||||||
|
**选项 A: 端口转发(推荐用于测试)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
然后访问 http://localhost:5300
|
||||||
|
|
||||||
|
**选项 B: NodePort(适用于开发环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 NodePort Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取节点 IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# 访问 http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 C: LoadBalancer(适用于云环境)**
|
||||||
|
|
||||||
|
编辑 `kubernetes.yaml`,取消注释 LoadBalancer Service 部分,然后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 获取外部 IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# 访问 http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项 D: Ingress(推荐用于生产环境)**
|
||||||
|
|
||||||
|
确保集群中已安装 Ingress Controller(如 nginx-ingress),然后:
|
||||||
|
|
||||||
|
1. 编辑 `kubernetes.yaml` 中的 Ingress 配置
|
||||||
|
2. 修改域名为您的实际域名
|
||||||
|
3. 应用配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# 访问 http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
#### 环境变量
|
||||||
|
|
||||||
|
在 `ConfigMap` 中配置环境变量:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # 修改为您的时区
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储配置
|
||||||
|
|
||||||
|
默认使用动态存储分配。如果您有特定的 StorageClass,请在 PVC 中指定:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 资源限制
|
||||||
|
|
||||||
|
根据您的需求调整资源限制:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常用操作
|
||||||
|
|
||||||
|
#### 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 LangBot 主服务日志
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# 查看插件运行时日志
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 重启服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重启 LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# 重启插件运行时
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 更新镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新到最新版本
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# 检查更新状态
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 扩容(不推荐)
|
||||||
|
|
||||||
|
注意:由于 LangBot 使用 ReadWriteOnce 的持久化存储,不支持多副本扩容。如需高可用,请考虑使用 ReadWriteMany 存储或其他架构方案。
|
||||||
|
|
||||||
|
#### 备份数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 PVC 数据
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### 卸载
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除所有资源(保留 PVC)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# 删除 PVC(会删除数据)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# 删除命名空间
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排查
|
||||||
|
|
||||||
|
#### Pod 无法启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看 Pod 状态
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# 查看详细信息
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# 查看事件
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 存储问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 PVC 状态
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# 检查 PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 网络访问问题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# 检查端口转发
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境建议
|
||||||
|
|
||||||
|
1. **使用特定版本标签**:避免使用 `latest` 标签,使用具体版本号如 `rockchin/langbot:v1.0.0`
|
||||||
|
2. **配置资源限制**:根据实际负载调整 CPU 和内存限制
|
||||||
|
3. **使用 Ingress + TLS**:配置 HTTPS 访问和证书管理
|
||||||
|
4. **配置监控和告警**:集成 Prometheus、Grafana 等监控工具
|
||||||
|
5. **定期备份**:配置自动备份策略保护数据
|
||||||
|
6. **使用专用 StorageClass**:为生产环境配置高性能存储
|
||||||
|
7. **配置亲和性规则**:确保 Pod 调度到合适的节点
|
||||||
|
|
||||||
|
### 高级配置
|
||||||
|
|
||||||
|
#### 使用 Secrets 管理敏感信息
|
||||||
|
|
||||||
|
如果需要配置 API 密钥等敏感信息:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 Deployment 中引用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 配置水平自动扩缩容(HPA)
|
||||||
|
|
||||||
|
注意:需要确保使用 ReadWriteMany 存储类型
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参考资源
|
||||||
|
|
||||||
|
- [LangBot 官方文档](https://docs.langbot.app)
|
||||||
|
- [Docker 部署文档](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes 官方文档](https://kubernetes.io/docs/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
This guide provides complete steps for deploying LangBot in a Kubernetes cluster. The Kubernetes deployment configuration is based on `docker-compose.yaml` and is suitable for production containerized deployments.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes cluster (version 1.19+)
|
||||||
|
- `kubectl` command-line tool configured with cluster access
|
||||||
|
- Available StorageClass in the cluster for persistent storage (optional but recommended)
|
||||||
|
- At least 2 vCPU and 4GB RAM of available resources
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
The Kubernetes deployment includes the following components:
|
||||||
|
|
||||||
|
1. **langbot**: Main application service
|
||||||
|
- Provides Web UI (port 5300)
|
||||||
|
- Handles platform webhooks (ports 2280-2290)
|
||||||
|
- Data persistence volume
|
||||||
|
|
||||||
|
2. **langbot-plugin-runtime**: Plugin runtime service
|
||||||
|
- WebSocket communication (port 5400)
|
||||||
|
- Plugin data persistence volume
|
||||||
|
|
||||||
|
3. **Persistent Storage**:
|
||||||
|
- `langbot-data`: LangBot main data
|
||||||
|
- `langbot-plugins`: Plugin files
|
||||||
|
- `langbot-plugin-runtime-data`: Plugin runtime data
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
#### 1. Download Deployment Files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot/docker
|
||||||
|
|
||||||
|
# Or download kubernetes.yaml directly
|
||||||
|
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Deploy to Kubernetes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply all configurations
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
# Check deployment status
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
# View Pod logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Access LangBot
|
||||||
|
|
||||||
|
By default, LangBot service uses ClusterIP type, accessible only within the cluster. Choose one of the following methods to access:
|
||||||
|
|
||||||
|
**Option A: Port Forwarding (Recommended for testing)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
Then visit http://localhost:5300
|
||||||
|
|
||||||
|
**Option B: NodePort (Suitable for development)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the NodePort Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get node IP
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
# Visit http://<NODE_IP>:30300
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: LoadBalancer (Suitable for cloud environments)**
|
||||||
|
|
||||||
|
Edit `kubernetes.yaml`, uncomment the LoadBalancer Service section, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Get external IP
|
||||||
|
kubectl get svc -n langbot langbot-loadbalancer
|
||||||
|
# Visit http://<EXTERNAL_IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option D: Ingress (Recommended for production)**
|
||||||
|
|
||||||
|
Ensure an Ingress Controller (e.g., nginx-ingress) is installed in the cluster, then:
|
||||||
|
|
||||||
|
1. Edit the Ingress configuration in `kubernetes.yaml`
|
||||||
|
2. Change the domain to your actual domain
|
||||||
|
3. Apply configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
# Visit http://langbot.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
Configure environment variables in ConfigMap:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai" # Change to your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Configuration
|
||||||
|
|
||||||
|
Uses dynamic storage provisioning by default. If you have a specific StorageClass, specify it in PVC:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
storageClassName: your-storage-class-name
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource Limits
|
||||||
|
|
||||||
|
Adjust resource limits based on your needs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View LangBot main service logs
|
||||||
|
kubectl logs -n langbot -l app=langbot -f
|
||||||
|
|
||||||
|
# View plugin runtime logs
|
||||||
|
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Restart Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart LangBot
|
||||||
|
kubectl rollout restart deployment/langbot -n langbot
|
||||||
|
|
||||||
|
# Restart plugin runtime
|
||||||
|
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Images
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update to latest version
|
||||||
|
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||||
|
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||||
|
|
||||||
|
# Check update status
|
||||||
|
kubectl rollout status deployment/langbot -n langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Scaling (Not Recommended)
|
||||||
|
|
||||||
|
Note: Due to LangBot using ReadWriteOnce persistent storage, multi-replica scaling is not supported. For high availability, consider using ReadWriteMany storage or alternative architectures.
|
||||||
|
|
||||||
|
#### Backup Data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup PVC data
|
||||||
|
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||||
|
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
### Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete all resources (keep PVCs)
|
||||||
|
kubectl delete deployment,service,configmap -n langbot --all
|
||||||
|
|
||||||
|
# Delete PVCs (will delete data)
|
||||||
|
kubectl delete pvc -n langbot --all
|
||||||
|
|
||||||
|
# Delete namespace
|
||||||
|
kubectl delete namespace langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### Pods Not Starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Pod status
|
||||||
|
kubectl get pods -n langbot
|
||||||
|
|
||||||
|
# View detailed information
|
||||||
|
kubectl describe pod -n langbot <pod-name>
|
||||||
|
|
||||||
|
# View events
|
||||||
|
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storage Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PVC status
|
||||||
|
kubectl get pvc -n langbot
|
||||||
|
|
||||||
|
# Check PV
|
||||||
|
kubectl get pv
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Network Access Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Service
|
||||||
|
kubectl get svc -n langbot
|
||||||
|
|
||||||
|
# Test port forwarding
|
||||||
|
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Recommendations
|
||||||
|
|
||||||
|
1. **Use specific version tags**: Avoid using `latest` tag, use specific version like `rockchin/langbot:v1.0.0`
|
||||||
|
2. **Configure resource limits**: Adjust CPU and memory limits based on actual load
|
||||||
|
3. **Use Ingress + TLS**: Configure HTTPS access and certificate management
|
||||||
|
4. **Configure monitoring and alerts**: Integrate monitoring tools like Prometheus, Grafana
|
||||||
|
5. **Regular backups**: Configure automated backup strategy to protect data
|
||||||
|
6. **Use dedicated StorageClass**: Configure high-performance storage for production
|
||||||
|
7. **Configure affinity rules**: Ensure Pods are scheduled to appropriate nodes
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
#### Using Secrets for Sensitive Information
|
||||||
|
|
||||||
|
If you need to configure sensitive information like API keys:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: langbot-secrets
|
||||||
|
namespace: langbot
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
api_key: <base64-encoded-value>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference in Deployment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langbot-secrets
|
||||||
|
key: api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configure Horizontal Pod Autoscaling (HPA)
|
||||||
|
|
||||||
|
Note: Requires ReadWriteMany storage type
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: langbot-hpa
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: langbot
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 3
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
```
|
||||||
|
|
||||||
|
### References
|
||||||
|
|
||||||
|
- [LangBot Official Documentation](https://docs.langbot.app)
|
||||||
|
- [Docker Deployment Guide](https://link.langbot.app/zh/docs/docker)
|
||||||
|
- [Kubernetes Official Documentation](https://kubernetes.io/docs/)
|
||||||
74
docker/deploy-k8s-test.sh
Executable file
74
docker/deploy-k8s-test.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick test script for LangBot Kubernetes deployment
|
||||||
|
# This script helps you test the Kubernetes deployment locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 LangBot Kubernetes Deployment Test Script"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for kubectl
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
echo "❌ kubectl is not installed. Please install kubectl first."
|
||||||
|
echo "Visit: https://kubernetes.io/docs/tasks/tools/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ kubectl is installed"
|
||||||
|
|
||||||
|
# Check if kubectl can connect to a cluster
|
||||||
|
if ! kubectl cluster-info &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ No Kubernetes cluster found."
|
||||||
|
echo ""
|
||||||
|
echo "To test locally, you can use:"
|
||||||
|
echo " - kind: https://kind.sigs.k8s.io/"
|
||||||
|
echo " - minikube: https://minikube.sigs.k8s.io/"
|
||||||
|
echo " - k3s: https://k3s.io/"
|
||||||
|
echo ""
|
||||||
|
echo "Example with kind:"
|
||||||
|
echo " kind create cluster --name langbot-test"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Connected to Kubernetes cluster"
|
||||||
|
kubectl cluster-info
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ask user to confirm
|
||||||
|
read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Deployment cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Deploying LangBot..."
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Waiting for pods to be ready..."
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Deployment status:"
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🌐 To access LangBot Web UI, run:"
|
||||||
|
echo " kubectl port-forward -n langbot svc/langbot 5300:5300"
|
||||||
|
echo ""
|
||||||
|
echo "Then visit: http://localhost:5300"
|
||||||
|
echo ""
|
||||||
|
echo "📝 To view logs:"
|
||||||
|
echo " kubectl logs -n langbot -l app=langbot -f"
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ To uninstall:"
|
||||||
|
echo " kubectl delete namespace langbot"
|
||||||
|
echo ""
|
||||||
37
docker/docker-compose.yaml
Normal file
37
docker/docker-compose.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Docker Compose configuration for LangBot
|
||||||
|
# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
langbot_plugin_runtime:
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
container_name: langbot_plugin_runtime
|
||||||
|
volumes:
|
||||||
|
- ./data/plugins:/app/data/plugins
|
||||||
|
ports:
|
||||||
|
- 5401:5401
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
networks:
|
||||||
|
- langbot_network
|
||||||
|
|
||||||
|
langbot:
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
container_name: langbot
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- 5300:5300 # For web ui and webhook callback
|
||||||
|
- 2280-2285:2280-2285 # For platform reverse connection
|
||||||
|
networks:
|
||||||
|
- langbot_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
langbot_network:
|
||||||
|
driver: bridge
|
||||||
400
docker/kubernetes.yaml
Normal file
400
docker/kubernetes.yaml
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Kubernetes Deployment for LangBot
|
||||||
|
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# kubectl apply -f kubernetes.yaml
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - A Kubernetes cluster (1.19+)
|
||||||
|
# - kubectl configured to communicate with your cluster
|
||||||
|
# - (Optional) A StorageClass for dynamic volume provisioning
|
||||||
|
#
|
||||||
|
# Components:
|
||||||
|
# - Namespace: langbot
|
||||||
|
# - PersistentVolumeClaims for data persistence
|
||||||
|
# - Deployments for langbot and langbot_plugin_runtime
|
||||||
|
# - Services for network access
|
||||||
|
# - ConfigMap for timezone configuration
|
||||||
|
|
||||||
|
---
|
||||||
|
# Namespace
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for LangBot plugins
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugins
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# PersistentVolumeClaim for Plugin Runtime data
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime-data
|
||||||
|
namespace: langbot
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
# Uncomment and modify if you have a specific StorageClass
|
||||||
|
# storageClassName: your-storage-class
|
||||||
|
|
||||||
|
---
|
||||||
|
# ConfigMap for environment configuration
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langbot-config
|
||||||
|
namespace: langbot
|
||||||
|
data:
|
||||||
|
TZ: "Asia/Shanghai"
|
||||||
|
PLUGIN__RUNTIME_WS_URL: "ws://langbot-plugin-runtime:5400/control/ws"
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot Plugin Runtime
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot-plugin-runtime
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 5400
|
||||||
|
name: runtime
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
volumeMounts:
|
||||||
|
- name: plugin-data
|
||||||
|
mountPath: /app/data/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "250m"
|
||||||
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5400
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: plugin-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugin-runtime-data
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot Plugin Runtime
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot-plugin-runtime
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot-plugin-runtime
|
||||||
|
ports:
|
||||||
|
- port: 5400
|
||||||
|
targetPort: 5400
|
||||||
|
protocol: TCP
|
||||||
|
name: runtime
|
||||||
|
|
||||||
|
---
|
||||||
|
# Deployment for LangBot
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: langbot
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 5300
|
||||||
|
name: web
|
||||||
|
protocol: TCP
|
||||||
|
- containerPort: 2280
|
||||||
|
name: webhook-start
|
||||||
|
protocol: TCP
|
||||||
|
# Note: Kubernetes doesn't support port ranges directly in container ports
|
||||||
|
# The webhook ports 2280-2290 are available, but we only expose the start of the range
|
||||||
|
# If you need all ports exposed, consider using a Service with multiple port definitions
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
- name: PLUGIN__RUNTIME_WS_URL
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: PLUGIN__RUNTIME_WS_URL
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: plugins
|
||||||
|
mountPath: /app/plugins
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "4Gi"
|
||||||
|
cpu: "2000m"
|
||||||
|
# Liveness probe to restart container if it becomes unresponsive
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 60
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
# Readiness probe to know when container is ready to accept traffic
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 5300
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-data
|
||||||
|
- name: plugins
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: langbot-plugins
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot (ClusterIP for internal access)
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot
|
||||||
|
ports:
|
||||||
|
- port: 5300
|
||||||
|
targetPort: 5300
|
||||||
|
protocol: TCP
|
||||||
|
name: web
|
||||||
|
- port: 2280
|
||||||
|
targetPort: 2280
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2280
|
||||||
|
- port: 2281
|
||||||
|
targetPort: 2281
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2281
|
||||||
|
- port: 2282
|
||||||
|
targetPort: 2282
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2282
|
||||||
|
- port: 2283
|
||||||
|
targetPort: 2283
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2283
|
||||||
|
- port: 2284
|
||||||
|
targetPort: 2284
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2284
|
||||||
|
- port: 2285
|
||||||
|
targetPort: 2285
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2285
|
||||||
|
- port: 2286
|
||||||
|
targetPort: 2286
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2286
|
||||||
|
- port: 2287
|
||||||
|
targetPort: 2287
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2287
|
||||||
|
- port: 2288
|
||||||
|
targetPort: 2288
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2288
|
||||||
|
- port: 2289
|
||||||
|
targetPort: 2289
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2289
|
||||||
|
- port: 2290
|
||||||
|
targetPort: 2290
|
||||||
|
protocol: TCP
|
||||||
|
name: webhook-2290
|
||||||
|
|
||||||
|
---
|
||||||
|
# Ingress for external access (Optional - requires Ingress Controller)
|
||||||
|
# Uncomment and modify the following section if you want to expose LangBot via Ingress
|
||||||
|
# apiVersion: networking.k8s.io/v1
|
||||||
|
# kind: Ingress
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-ingress
|
||||||
|
# namespace: langbot
|
||||||
|
# annotations:
|
||||||
|
# # Uncomment and modify based on your ingress controller
|
||||||
|
# # nginx.ingress.kubernetes.io/rewrite-target: /
|
||||||
|
# # cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
# spec:
|
||||||
|
# ingressClassName: nginx # Change based on your ingress controller
|
||||||
|
# rules:
|
||||||
|
# - host: langbot.yourdomain.com # Change to your domain
|
||||||
|
# http:
|
||||||
|
# paths:
|
||||||
|
# - path: /
|
||||||
|
# pathType: Prefix
|
||||||
|
# backend:
|
||||||
|
# service:
|
||||||
|
# name: langbot
|
||||||
|
# port:
|
||||||
|
# number: 5300
|
||||||
|
# # Uncomment for TLS/HTTPS
|
||||||
|
# # tls:
|
||||||
|
# # - hosts:
|
||||||
|
# # - langbot.yourdomain.com
|
||||||
|
# # secretName: langbot-tls
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with LoadBalancer (Alternative to Ingress)
|
||||||
|
# Uncomment the following if you want to expose LangBot directly via LoadBalancer
|
||||||
|
# This is useful in cloud environments (AWS, GCP, Azure, etc.)
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-loadbalancer
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: LoadBalancer
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 80
|
||||||
|
# targetPort: 5300
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook-start
|
||||||
|
# # Add more webhook ports as needed
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot with NodePort (Alternative for exposing service)
|
||||||
|
# Uncomment if you want to expose LangBot via NodePort
|
||||||
|
# This is useful for testing or when LoadBalancer is not available
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: langbot-nodeport
|
||||||
|
# namespace: langbot
|
||||||
|
# labels:
|
||||||
|
# app: langbot
|
||||||
|
# spec:
|
||||||
|
# type: NodePort
|
||||||
|
# selector:
|
||||||
|
# app: langbot
|
||||||
|
# ports:
|
||||||
|
# - port: 5300
|
||||||
|
# targetPort: 5300
|
||||||
|
# nodePort: 30300 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: web
|
||||||
|
# - port: 2280
|
||||||
|
# targetPort: 2280
|
||||||
|
# nodePort: 30280 # Must be in range 30000-32767
|
||||||
|
# protocol: TCP
|
||||||
|
# name: webhook
|
||||||
291
docs/API_KEY_AUTH.md
Normal file
291
docs/API_KEY_AUTH.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# API Key Authentication
|
||||||
|
|
||||||
|
LangBot now supports API key authentication for external systems to access its HTTP service API.
|
||||||
|
|
||||||
|
## Managing API Keys
|
||||||
|
|
||||||
|
API keys can be managed through the web interface:
|
||||||
|
|
||||||
|
1. Log in to the LangBot web interface
|
||||||
|
2. Click the "API Keys" button at the bottom of the sidebar
|
||||||
|
3. Create, view, copy, or delete API keys as needed
|
||||||
|
|
||||||
|
## Using API Keys
|
||||||
|
|
||||||
|
### Authentication Headers
|
||||||
|
|
||||||
|
Include your API key in the request header using one of these methods:
|
||||||
|
|
||||||
|
**Method 1: X-API-Key header (Recommended)**
|
||||||
|
```
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method 2: Authorization Bearer token**
|
||||||
|
```
|
||||||
|
Authorization: Bearer lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available APIs
|
||||||
|
|
||||||
|
All existing LangBot APIs now support **both user token and API key authentication**. This means you can use API keys to access:
|
||||||
|
|
||||||
|
- **Model Management** - `/api/v1/provider/models/llm` and `/api/v1/provider/models/embedding`
|
||||||
|
- **Bot Management** - `/api/v1/platform/bots`
|
||||||
|
- **Pipeline Management** - `/api/v1/pipelines`
|
||||||
|
- **Knowledge Base** - `/api/v1/knowledge/*`
|
||||||
|
- **MCP Servers** - `/api/v1/mcp/servers`
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
### Authentication Methods
|
||||||
|
|
||||||
|
Each endpoint accepts **either**:
|
||||||
|
1. **User Token** (via `Authorization: Bearer <user_jwt_token>`) - for web UI and authenticated users
|
||||||
|
2. **API Key** (via `X-API-Key` or `Authorization: Bearer <api_key>`) - for external services
|
||||||
|
|
||||||
|
## Example: Model Management
|
||||||
|
|
||||||
|
### List All LLM Models
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"uuid": "model-uuid",
|
||||||
|
"name": "GPT-4",
|
||||||
|
"description": "OpenAI GPT-4 model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {...},
|
||||||
|
"abilities": ["chat", "vision"],
|
||||||
|
"created_at": "2024-01-01T00:00:00",
|
||||||
|
"updated_at": "2024-01-01T00:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/provider/models/llm
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Custom Model",
|
||||||
|
"description": "Description of the model",
|
||||||
|
"requester": "openai-chat-completions",
|
||||||
|
"requester_config": {
|
||||||
|
"model": "gpt-4",
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"api_keys": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"keys": ["sk-..."]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abilities": ["chat"],
|
||||||
|
"extra_args": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
PUT /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Updated Model Name",
|
||||||
|
"description": "Updated description",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete an LLM Model
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/v1/provider/models/llm/{model_uuid}
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Bot Management
|
||||||
|
|
||||||
|
### List All Bots
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Bot
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/platform/bots
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Pipeline Management
|
||||||
|
|
||||||
|
### List All Pipelines
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Pipeline
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines
|
||||||
|
X-API-Key: lbk_your_api_key_here
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 401 Unauthorized
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "No valid authentication provided (user token or API key required)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Invalid API key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 404 Not Found
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -1,
|
||||||
|
"msg": "Resource not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 500 Internal Server Error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": -2,
|
||||||
|
"msg": "Error message details"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Keep API keys secure**: Store them securely and never commit them to version control
|
||||||
|
2. **Use HTTPS**: Always use HTTPS in production to encrypt API key transmission
|
||||||
|
3. **Rotate keys regularly**: Create new API keys periodically and delete old ones
|
||||||
|
4. **Use descriptive names**: Give your API keys meaningful names to track their usage
|
||||||
|
5. **Delete unused keys**: Remove API keys that are no longer needed
|
||||||
|
6. **Use X-API-Key header**: Prefer using the `X-API-Key` header for clarity
|
||||||
|
|
||||||
|
## Example: Python Client
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
API_KEY = "lbk_your_api_key_here"
|
||||||
|
BASE_URL = "http://your-langbot-server:5300"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all models
|
||||||
|
response = requests.get(f"{BASE_URL}/api/v1/provider/models/llm", headers=headers)
|
||||||
|
models = response.json()["data"]["models"]
|
||||||
|
|
||||||
|
print(f"Found {len(models)} models")
|
||||||
|
for model in models:
|
||||||
|
print(f"- {model['name']}: {model['description']}")
|
||||||
|
|
||||||
|
# Create a new bot
|
||||||
|
bot_data = {
|
||||||
|
"name": "My Telegram Bot",
|
||||||
|
"adapter": "telegram",
|
||||||
|
"config": {
|
||||||
|
"token": "your-telegram-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{BASE_URL}/api/v1/platform/bots",
|
||||||
|
headers=headers,
|
||||||
|
json=bot_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
bot_uuid = response.json()["data"]["uuid"]
|
||||||
|
print(f"Bot created with UUID: {bot_uuid}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all models
|
||||||
|
curl -X GET \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
http://your-langbot-server:5300/api/v1/provider/models/llm
|
||||||
|
|
||||||
|
# Create a new pipeline
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "My Pipeline",
|
||||||
|
"config": {...}
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/pipelines
|
||||||
|
|
||||||
|
# Get bot logs
|
||||||
|
curl -X POST \
|
||||||
|
-H "X-API-Key: lbk_your_api_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"from_index": -1,
|
||||||
|
"max_count": 10
|
||||||
|
}' \
|
||||||
|
http://your-langbot-server:5300/api/v1/platform/bots/{bot_uuid}/logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The same endpoints work for both the web UI (with user tokens) and external services (with API keys)
|
||||||
|
- No need to learn different API paths - use the existing API documentation with API key authentication
|
||||||
|
- All endpoints that previously required user authentication now also accept API keys
|
||||||
|
|
||||||
412
docs/MIGRATION_SUMMARY.md
Normal file
412
docs/MIGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
# WebChat 到 WebSocket 迁移总结
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
已完全移除旧的基于SSE的WebChat系统,并替换为基于WebSocket的双向实时通信系统。这是一个内置在LangBot中的完整IM系统,支持流式输出。
|
||||||
|
|
||||||
|
## 已删除的文件
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- ❌ `src/langbot/pkg/api/http/controller/groups/pipelines/webchat.py` - 旧的SSE路由
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.py` - 旧的WebChat适配器
|
||||||
|
- ❌ `src/langbot/pkg/platform/sources/webchat.yaml` - 旧的配置文件
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- ❌ BackendClient中所有SSE相关代码已完全移除
|
||||||
|
- ❌ DebugDialog中所有SSE相关逻辑已完全替换
|
||||||
|
|
||||||
|
## 新增的文件
|
||||||
|
|
||||||
|
### 后端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket连接管理器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_manager.py
|
||||||
|
```
|
||||||
|
- 管理所有并发WebSocket连接
|
||||||
|
- 线程安全的连接池
|
||||||
|
- 按流水线、会话类型分组
|
||||||
|
- 广播和单播消息功能
|
||||||
|
- 连接统计和监控
|
||||||
|
|
||||||
|
**2. WebSocket适配器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket_adapter.py
|
||||||
|
```
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- **完整流式支持** (`reply_message_chunk` 方法)
|
||||||
|
- 双向消息流处理
|
||||||
|
- 消息历史管理
|
||||||
|
- 会话管理
|
||||||
|
|
||||||
|
**3. WebSocket路由控制器**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/api/http/controller/groups/pipelines/websocket_chat.py
|
||||||
|
```
|
||||||
|
- WebSocket端点处理
|
||||||
|
- REST API接口
|
||||||
|
- 心跳机制
|
||||||
|
- 连接生命周期管理
|
||||||
|
|
||||||
|
**4. 配置文件**
|
||||||
|
```
|
||||||
|
src/langbot/pkg/platform/sources/websocket.yaml
|
||||||
|
```
|
||||||
|
- WebSocket适配器元数据
|
||||||
|
|
||||||
|
### 前端核心文件
|
||||||
|
|
||||||
|
**1. WebSocket客户端**
|
||||||
|
```
|
||||||
|
web/src/app/infra/websocket/WebSocketClient.ts
|
||||||
|
```
|
||||||
|
- WebSocket连接管理
|
||||||
|
- 自动重连(最多5次)
|
||||||
|
- 心跳机制(30秒)
|
||||||
|
- 事件回调系统
|
||||||
|
|
||||||
|
**2. 更新的组件**
|
||||||
|
```
|
||||||
|
web/src/app/home/pipelines/components/debug-dialog/DebugDialog.tsx
|
||||||
|
```
|
||||||
|
- 完全重写,使用WebSocket
|
||||||
|
- 实时连接状态显示
|
||||||
|
- 流式消息支持
|
||||||
|
- 自动重连
|
||||||
|
|
||||||
|
**3. HTTP客户端更新**
|
||||||
|
```
|
||||||
|
web/src/app/infra/http/BackendClient.ts
|
||||||
|
```
|
||||||
|
- 移除所有旧的WebChat API
|
||||||
|
- 仅保留WebSocket API
|
||||||
|
|
||||||
|
### 测试工具
|
||||||
|
|
||||||
|
**Python测试客户端**
|
||||||
|
```
|
||||||
|
test_websocket_client.py
|
||||||
|
```
|
||||||
|
- 单连接交互测试
|
||||||
|
- 多连接并发测试
|
||||||
|
- 命令行工具
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
|
||||||
|
**使用文档**
|
||||||
|
```
|
||||||
|
WEBSOCKET_README.md
|
||||||
|
```
|
||||||
|
- 完整的API文档
|
||||||
|
- 架构说明
|
||||||
|
- 使用示例
|
||||||
|
- 故障排查
|
||||||
|
|
||||||
|
## 核心变更
|
||||||
|
|
||||||
|
### 后端变更
|
||||||
|
|
||||||
|
**1. botmgr.py**
|
||||||
|
- ❌ 移除 `webchat_proxy_bot`
|
||||||
|
- ✅ 仅保留 `websocket_proxy_bot`
|
||||||
|
- ✅ 更新适配器过滤逻辑(排除`websocket`而非`webchat`)
|
||||||
|
|
||||||
|
**2. 适配器注册**
|
||||||
|
```python
|
||||||
|
# 旧代码(已删除)
|
||||||
|
webchat_adapter_class = self.adapter_dict['webchat']
|
||||||
|
self.webchat_proxy_bot = RuntimeBot(...)
|
||||||
|
|
||||||
|
# 新代码
|
||||||
|
websocket_adapter_class = self.adapter_dict['websocket']
|
||||||
|
self.websocket_proxy_bot = RuntimeBot(
|
||||||
|
uuid='websocket-proxy-bot',
|
||||||
|
name='WebSocket',
|
||||||
|
adapter='websocket',
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端变更
|
||||||
|
|
||||||
|
**1. API调用完全更换**
|
||||||
|
|
||||||
|
旧代码(已删除):
|
||||||
|
```typescript
|
||||||
|
// SSE流式请求
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ is_stream: true })
|
||||||
|
})
|
||||||
|
// 手动解析 text/event-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
新代码:
|
||||||
|
```typescript
|
||||||
|
// WebSocket实时通信
|
||||||
|
const wsClient = new WebSocketClient(pipelineId, sessionType);
|
||||||
|
await wsClient.connect();
|
||||||
|
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
// 流式消息自动处理
|
||||||
|
setMessages(prev => [...prev, message]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wsClient.sendMessage(messageChain);
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 连接状态管理**
|
||||||
|
|
||||||
|
新增功能:
|
||||||
|
- ✅ 实时连接状态指示器(绿色/红色圆点)
|
||||||
|
- ✅ 连接/断开toast提示
|
||||||
|
- ✅ 自动重连逻辑
|
||||||
|
- ✅ 心跳保活
|
||||||
|
|
||||||
|
**3. 流式支持**
|
||||||
|
|
||||||
|
完整的流式消息处理:
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
if (message.is_final) {
|
||||||
|
// 最终消息
|
||||||
|
finalizeBotMessage(message);
|
||||||
|
} else {
|
||||||
|
// 中间消息块,实时更新UI
|
||||||
|
updateBotMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## API对比
|
||||||
|
|
||||||
|
### WebSocket端点
|
||||||
|
|
||||||
|
**连接**
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**消息格式**
|
||||||
|
|
||||||
|
客户端发送:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{"type": "Plain", "text": "你好"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
服务器响应(流式):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "你好,我是...",
|
||||||
|
"is_final": false,
|
||||||
|
"timestamp": "2025-01-28T..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/messages/<type>` | GET | 获取消息历史 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/reset/<type>` | POST | 重置会话 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/connections` | GET | 获取连接统计 |
|
||||||
|
| `/api/v1/pipelines/<uuid>/ws/broadcast` | POST | 广播消息 |
|
||||||
|
|
||||||
|
## 流式支持详解
|
||||||
|
|
||||||
|
### 后端流式实现
|
||||||
|
|
||||||
|
**WebSocket Adapter**
|
||||||
|
```python
|
||||||
|
async def reply_message_chunk(
|
||||||
|
self,
|
||||||
|
message_source: platform_events.MessageEvent,
|
||||||
|
bot_message,
|
||||||
|
message: platform_message.MessageChain,
|
||||||
|
quote_origin: bool = False,
|
||||||
|
is_final: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""回复消息块 - 流式"""
|
||||||
|
message_data = WebSocketMessage(
|
||||||
|
id=-1,
|
||||||
|
role='assistant',
|
||||||
|
content=str(message),
|
||||||
|
message_chain=[component.__dict__ for component in message],
|
||||||
|
timestamp=datetime.now().isoformat(),
|
||||||
|
is_final=is_final and bot_message.tool_calls is None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送到队列,由WebSocket连接处理发送
|
||||||
|
await session.resp_queues[message_id].put(message_data)
|
||||||
|
return message_data.model_dump()
|
||||||
|
|
||||||
|
async def is_stream_output_supported(self) -> bool:
|
||||||
|
"""WebSocket始终支持流式输出"""
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端流式处理
|
||||||
|
|
||||||
|
**DebugDialog组件**
|
||||||
|
```typescript
|
||||||
|
wsClient.onMessage((message) => {
|
||||||
|
setMessages((prevMessages) => {
|
||||||
|
const existingIndex = prevMessages.findIndex(
|
||||||
|
(msg) => msg.role === 'assistant' && msg.content === 'Generating...'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// 更新正在生成的消息
|
||||||
|
const updatedMessages = [...prevMessages];
|
||||||
|
updatedMessages[existingIndex] = message;
|
||||||
|
return updatedMessages;
|
||||||
|
} else {
|
||||||
|
// 添加新消息
|
||||||
|
return [...prevMessages, message];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 兼容性说明
|
||||||
|
|
||||||
|
### ⚠️ 不兼容旧版本
|
||||||
|
|
||||||
|
此次迁移**完全不兼容**旧的WebChat系统:
|
||||||
|
|
||||||
|
1. **API端点变更**
|
||||||
|
- 旧: `/api/v1/pipelines/<uuid>/chat/send`
|
||||||
|
- 新: `ws://.../<uuid>/ws/connect`
|
||||||
|
|
||||||
|
2. **通信协议变更**
|
||||||
|
- 旧: HTTP + SSE (Server-Sent Events)
|
||||||
|
- 新: WebSocket (双向)
|
||||||
|
|
||||||
|
3. **流式实现变更**
|
||||||
|
- 旧: `text/event-stream` 格式
|
||||||
|
- 新: WebSocket JSON消息
|
||||||
|
|
||||||
|
### 迁移要求
|
||||||
|
|
||||||
|
使用新系统需要:
|
||||||
|
1. ✅ 前端必须支持WebSocket
|
||||||
|
2. ✅ 后端必须运行新的WebSocket适配器
|
||||||
|
3. ✅ 清除旧的WebChat相关配置
|
||||||
|
|
||||||
|
## 优势对比
|
||||||
|
|
||||||
|
| 特性 | 旧WebChat (SSE) | 新WebSocket |
|
||||||
|
|------|----------------|-------------|
|
||||||
|
| 双向通信 | ❌ 单向(服务器→客户端) | ✅ 双向 |
|
||||||
|
| 主动推送 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
| 连接管理 | ❌ 无状态 | ✅ 有状态,完整生命周期 |
|
||||||
|
| 流式输出 | ✅ 支持 | ✅ 支持(更优) |
|
||||||
|
| 心跳机制 | ❌ 无 | ✅ 30秒心跳 |
|
||||||
|
| 自动重连 | ❌ 无 | ✅ 最多5次 |
|
||||||
|
| 多连接 | ⚠️ 难以管理 | ✅ 完整支持 |
|
||||||
|
| 连接状态 | ❌ 不可见 | ✅ 实时显示 |
|
||||||
|
| 广播功能 | ❌ 不支持 | ✅ 支持 |
|
||||||
|
|
||||||
|
## 测试方式
|
||||||
|
|
||||||
|
### 1. Python测试客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 单连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试(5个连接)
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端测试
|
||||||
|
|
||||||
|
1. 启动LangBot服务器
|
||||||
|
2. 访问前端界面
|
||||||
|
3. 打开流水线调试对话框
|
||||||
|
4. 观察连接状态指示器(左下角圆点)
|
||||||
|
5. 发送消息测试流式响应
|
||||||
|
|
||||||
|
### 3. 浏览器控制台测试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/<uuid>/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('已连接');
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [{type: 'Plain', text: '你好'}]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log('收到:', JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么完全删除旧代码而不保留兼容性?
|
||||||
|
A: 根据需求,不需要考虑任何对老版本的兼容性,彻底迁移可以避免代码冗余和维护负担。
|
||||||
|
|
||||||
|
### Q: 流式输出如何工作?
|
||||||
|
A:
|
||||||
|
1. 后端通过`reply_message_chunk`发送消息块
|
||||||
|
2. 消息块放入队列
|
||||||
|
3. WebSocket连接从队列取出并发送
|
||||||
|
4. 前端实时更新UI
|
||||||
|
5. `is_final=true`表示最后一块
|
||||||
|
|
||||||
|
### Q: 如何确保连接不断开?
|
||||||
|
A:
|
||||||
|
1. 客户端每30秒发送心跳(ping)
|
||||||
|
2. 服务器响应pong
|
||||||
|
3. 连接断开时自动重连(最多5次)
|
||||||
|
|
||||||
|
### Q: 如何实现后端主动推送?
|
||||||
|
A:
|
||||||
|
1. 调用 `/api/v1/pipelines/<uuid>/ws/broadcast` API
|
||||||
|
2. 消息会被推送到该流水线的所有连接
|
||||||
|
3. 前端通过`onBroadcast`回调接收
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
✅ **完成的工作**
|
||||||
|
- 完全移除旧的WebChat/SSE系统
|
||||||
|
- 实现完整的WebSocket双向通信系统
|
||||||
|
- 支持流式输出
|
||||||
|
- 支持多连接并发
|
||||||
|
- 实现自动重连和心跳机制
|
||||||
|
- 提供完整的测试工具和文档
|
||||||
|
|
||||||
|
✅ **核心特性**
|
||||||
|
- 双向实时通信
|
||||||
|
- 流式消息支持
|
||||||
|
- 多连接管理
|
||||||
|
- 自动重连
|
||||||
|
- 心跳保活
|
||||||
|
- 连接状态可视化
|
||||||
|
- 广播消息
|
||||||
|
|
||||||
|
✅ **技术亮点**
|
||||||
|
- 异步架构(asyncio)
|
||||||
|
- 线程安全的连接管理
|
||||||
|
- 独立的消息队列
|
||||||
|
- 完整的错误处理
|
||||||
|
- 模块化设计
|
||||||
|
|
||||||
|
🎉 系统已完全迁移到WebSocket,无任何旧代码遗留!
|
||||||
117
docs/PYPI_INSTALLATION.md
Normal file
117
docs/PYPI_INSTALLATION.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# LangBot PyPI Package Installation
|
||||||
|
|
||||||
|
## Quick Start with uvx
|
||||||
|
|
||||||
|
The easiest way to run LangBot is using `uvx` (recommended for quick testing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically download and run the latest version of LangBot.
|
||||||
|
|
||||||
|
## Install with pip/uv
|
||||||
|
|
||||||
|
You can also install LangBot as a regular Python package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using pip
|
||||||
|
pip install langbot
|
||||||
|
|
||||||
|
# Using uv
|
||||||
|
uv pip install langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Python module syntax:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation with Frontend
|
||||||
|
|
||||||
|
When published to PyPI, the LangBot package includes the pre-built frontend files. You don't need to build the frontend separately.
|
||||||
|
|
||||||
|
## Data Directory
|
||||||
|
|
||||||
|
When running LangBot as a package, it will create a `data/` directory in your current working directory to store configuration, logs, and other runtime data. You can run LangBot from any directory, and it will set up its data directory there.
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
LangBot supports the following command line options:
|
||||||
|
|
||||||
|
- `--standalone-runtime`: Use standalone plugin runtime
|
||||||
|
- `--debug`: Enable debug mode
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
langbot --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison with Other Installation Methods
|
||||||
|
|
||||||
|
### PyPI Package (uvx/pip)
|
||||||
|
- **Pros**: Easy to install and update, no need to clone repository or build frontend
|
||||||
|
- **Cons**: Less flexible for development/customization
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
- **Pros**: Isolated environment, easy deployment
|
||||||
|
- **Cons**: Requires Docker
|
||||||
|
|
||||||
|
### Manual Source Installation
|
||||||
|
- **Pros**: Full control, easy to customize and develop
|
||||||
|
- **Cons**: Requires building frontend, managing dependencies manually
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
If you want to contribute or customize LangBot, you should still use the manual installation method by cloning the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/langbot-app/LangBot
|
||||||
|
cd LangBot
|
||||||
|
uv sync
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
To update to the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With pip
|
||||||
|
pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uv
|
||||||
|
uv pip install --upgrade langbot
|
||||||
|
|
||||||
|
# With uvx (automatically uses latest)
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- Python 3.10.1 or higher
|
||||||
|
- Operating System: Linux, macOS, or Windows
|
||||||
|
|
||||||
|
## Differences from Source Installation
|
||||||
|
|
||||||
|
When running LangBot from the PyPI package (via uvx or pip), there are a few behavioral differences compared to running from source:
|
||||||
|
|
||||||
|
1. **Version Check**: The package version does not prompt for user input when the Python version is incompatible. It simply prints an error message and exits. This makes it compatible with non-interactive environments like containers and CI/CD.
|
||||||
|
|
||||||
|
2. **Working Directory**: The package version does not require being run from the LangBot project root. You can run `langbot` from any directory, and it will create a `data/` directory in your current working directory.
|
||||||
|
|
||||||
|
3. **Frontend Files**: The frontend is pre-built and included in the package, so you don't need to run `npm build` separately.
|
||||||
|
|
||||||
|
These differences are intentional to make the package more user-friendly and suitable for various deployment scenarios.
|
||||||
259
docs/SEEKDB_INTEGRATION.md
Normal file
259
docs/SEEKDB_INTEGRATION.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# SeekDB Vector Database Integration
|
||||||
|
|
||||||
|
This document describes how to use OceanBase SeekDB as the vector database backend for LangBot's knowledge base feature.
|
||||||
|
|
||||||
|
## What is SeekDB?
|
||||||
|
|
||||||
|
**OceanBase SeekDB** is an AI-native search database that unifies relational, vector, text, JSON and GIS in a single engine, enabling hybrid search and in-database AI workflows. It's developed by OceanBase and released under Apache 2.0 license.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Hybrid Search**: Combine vector search, full-text search and relational query in a single statement
|
||||||
|
- **Multi-Model Support**: Support relational, vector, text, JSON and GIS in a single engine
|
||||||
|
- **Lightweight**: Requires as little as 1 CPU core and 2 GB of memory
|
||||||
|
- **Multiple Deployment Modes**: Supports both embedded mode and client/server mode
|
||||||
|
- **MySQL Compatible**: Powered by OceanBase engine with full ACID compliance and MySQL compatibility
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
SeekDB support is automatically included when you install LangBot. The required dependency `pyseekdb` is listed in `pyproject.toml`.
|
||||||
|
|
||||||
|
If you need to install it manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Platform Compatibility
|
||||||
|
|
||||||
|
### Embedded Mode
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full embedded mode support via `pylibseekdb` |
|
||||||
|
| macOS | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
| Windows | ❌ Not Supported | `pylibseekdb` is Linux-only; use server mode instead |
|
||||||
|
|
||||||
|
**Important**: Embedded mode requires the `pylibseekdb` library, which is only available on Linux. If you're on macOS or Windows, you must use server mode.
|
||||||
|
|
||||||
|
### Server Mode (Docker)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Linux | ✅ Supported | Full Docker support |
|
||||||
|
| macOS | ⚠️ Known Issue | Docker container initialization failure - [See Issue #36](https://github.com/oceanbase/seekdb/issues/36) |
|
||||||
|
| Windows | ⚠️ Untested | Should work but not yet tested |
|
||||||
|
|
||||||
|
**macOS Users**: Currently, SeekDB Docker containers have an initialization issue on macOS ([oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36)). Until this is resolved, we recommend:
|
||||||
|
- Using ChromaDB or Qdrant as alternatives
|
||||||
|
- Connecting to a remote SeekDB server on Linux if available
|
||||||
|
|
||||||
|
### Server Mode (Remote Connection)
|
||||||
|
|
||||||
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| All Platforms | ✅ Supported | Connect to SeekDB running on a remote Linux server |
|
||||||
|
|
||||||
|
**Recommendation for macOS/Windows users**: Deploy SeekDB on a Linux server and connect via server mode configuration.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Embedded Mode (Recommended for Development)
|
||||||
|
|
||||||
|
Embedded mode runs SeekDB directly within the LangBot process, storing data locally. This is the simplest setup and requires no external services.
|
||||||
|
|
||||||
|
Edit your `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: embedded
|
||||||
|
path: './data/seekdb' # Path to store SeekDB data
|
||||||
|
database: 'langbot' # Database name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Mode (For Production)
|
||||||
|
|
||||||
|
Server mode connects to a remote SeekDB server or OceanBase server. This is recommended for production deployments.
|
||||||
|
|
||||||
|
#### SeekDB Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: '' # Can also use SEEKDB_PASSWORD env var
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OceanBase Server
|
||||||
|
|
||||||
|
If you're using OceanBase with seekdb capabilities:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'localhost'
|
||||||
|
port: 2881
|
||||||
|
tenant: 'sys' # OceanBase tenant name
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Parameters
|
||||||
|
|
||||||
|
| Parameter | Required | Default | Description |
|
||||||
|
|-----------|----------|--------------|-------------|
|
||||||
|
| `mode` | No | `embedded` | Deployment mode: `embedded` or `server` |
|
||||||
|
| `path` | No | `./data/seekdb` | Data directory for embedded mode |
|
||||||
|
| `database` | No | `langbot` | Database name |
|
||||||
|
| `host` | No | `localhost` | Server host (server mode only) |
|
||||||
|
| `port` | No | `2881` | Server port (server mode only) |
|
||||||
|
| `user` | No | `root` | Username (server mode only) |
|
||||||
|
| `password` | No | `''` | Password (server mode only) |
|
||||||
|
| `tenant` | No | None | OceanBase tenant (optional, server mode only) |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Once configured, SeekDB will be used automatically for all knowledge base operations in LangBot:
|
||||||
|
|
||||||
|
1. **Creating Knowledge Bases**: Vectors will be stored in SeekDB collections
|
||||||
|
2. **Adding Documents**: Document embeddings will be indexed in SeekDB
|
||||||
|
3. **Searching**: Vector similarity search will use SeekDB's efficient indexing
|
||||||
|
4. **Deleting**: Document removal will delete vectors from SeekDB
|
||||||
|
|
||||||
|
No code changes are required - just update your configuration!
|
||||||
|
|
||||||
|
## Architecture Details
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
The SeekDB adapter is implemented in `src/langbot/pkg/vector/vdbs/seekdb.py` and follows the same `VectorDatabase` interface as Chroma and Qdrant adapters.
|
||||||
|
|
||||||
|
Key methods:
|
||||||
|
- `add_embeddings()`: Add vectors with metadata to a collection
|
||||||
|
- `search()`: Perform vector similarity search
|
||||||
|
- `delete_by_file_id()`: Delete vectors by file ID metadata
|
||||||
|
- `get_or_create_collection()`: Manage collections
|
||||||
|
- `delete_collection()`: Remove entire collections
|
||||||
|
|
||||||
|
### Vector Storage
|
||||||
|
|
||||||
|
- Collections are created with HNSW (Hierarchical Navigable Small World) index
|
||||||
|
- Default distance metric: Cosine similarity
|
||||||
|
- Default vector dimension: 384 (adjusts automatically based on embeddings)
|
||||||
|
- Metadata is stored alongside vectors for filtering
|
||||||
|
|
||||||
|
## Advantages Over Other Vector Databases
|
||||||
|
|
||||||
|
### vs. ChromaDB
|
||||||
|
- ✅ Better MySQL compatibility
|
||||||
|
- ✅ Hybrid search capabilities (vector + full-text + SQL)
|
||||||
|
- ✅ Production-grade distributed mode support
|
||||||
|
- ✅ Lightweight embedded mode
|
||||||
|
|
||||||
|
### vs. Qdrant
|
||||||
|
- ✅ SQL query support
|
||||||
|
- ✅ MySQL ecosystem integration
|
||||||
|
- ✅ Simpler deployment (no Docker required for embedded mode)
|
||||||
|
- ✅ Multi-model data support (not just vectors)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Import Error
|
||||||
|
|
||||||
|
If you see: `ImportError: pyseekdb is not installed`
|
||||||
|
|
||||||
|
Solution:
|
||||||
|
```bash
|
||||||
|
pip install pyseekdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embedded Mode Error on macOS/Windows
|
||||||
|
|
||||||
|
**Error**:
|
||||||
|
```
|
||||||
|
RuntimeError: Embedded Client is not available because pylibseekdb is not available.
|
||||||
|
Please install pylibseekdb (Linux only) or use RemoteServerClient (host/port) instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `pylibseekdb` is only available on Linux platforms.
|
||||||
|
|
||||||
|
**Solution**: Use server mode instead:
|
||||||
|
1. Deploy SeekDB on a Linux server or VM
|
||||||
|
2. Configure LangBot to use server mode:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: seekdb
|
||||||
|
seekdb:
|
||||||
|
mode: server
|
||||||
|
host: 'your-seekdb-server-ip'
|
||||||
|
port: 2881
|
||||||
|
database: 'langbot'
|
||||||
|
user: 'root'
|
||||||
|
password: ''
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative**: Use ChromaDB or Qdrant, which work on all platforms:
|
||||||
|
```yaml
|
||||||
|
vdb:
|
||||||
|
use: chroma # or qdrant
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Container Fails on macOS
|
||||||
|
|
||||||
|
**Symptoms**:
|
||||||
|
```bash
|
||||||
|
docker run -d -p 2881:2881 oceanbase/seekdb:latest
|
||||||
|
# Container exits immediately with code 30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error in logs**:
|
||||||
|
```
|
||||||
|
[ERROR] Code: Agent.SeekDB.Not.Exists
|
||||||
|
Message: initialize failed: init agent failed: SeekDB not exists in current directory.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: This is a known issue with SeekDB Docker containers on macOS. See [oceanbase/seekdb#36](https://github.com/oceanbase/seekdb/issues/36).
|
||||||
|
|
||||||
|
**Status**: Under investigation by OceanBase team.
|
||||||
|
|
||||||
|
**Workaround Options**:
|
||||||
|
1. **Use alternatives**: ChromaDB or Qdrant work perfectly on macOS
|
||||||
|
2. **Remote server**: Deploy SeekDB on a Linux server and connect remotely
|
||||||
|
3. **Wait for fix**: Monitor the GitHub issue for updates
|
||||||
|
|
||||||
|
### Connection Error (Server Mode)
|
||||||
|
|
||||||
|
If SeekDB server is not reachable, check:
|
||||||
|
1. Server is running: `ps aux | grep observer`
|
||||||
|
2. Port is accessible: `nc -zv localhost 2881`
|
||||||
|
3. Credentials are correct in config
|
||||||
|
4. Firewall allows connections on port 2881
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
For large datasets:
|
||||||
|
- Use server mode instead of embedded mode
|
||||||
|
- Ensure adequate memory allocation
|
||||||
|
- Consider using OceanBase distributed mode for very large scale
|
||||||
|
- Adjust HNSW index parameters if needed
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- SeekDB GitHub: https://github.com/oceanbase/seekdb
|
||||||
|
- pyseekdb SDK: https://github.com/oceanbase/pyseekdb
|
||||||
|
- OceanBase Documentation: https://oceanbase.ai
|
||||||
|
- LangBot Documentation: https://docs.langbot.app
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
SeekDB is licensed under Apache License 2.0.
|
||||||
180
docs/TESTING_SUMMARY.md
Normal file
180
docs/TESTING_SUMMARY.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# Pipeline Unit Tests - Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Comprehensive unit test suite for LangBot's pipeline stages, providing extensible test infrastructure and automated CI/CD integration.
|
||||||
|
|
||||||
|
## What Was Implemented
|
||||||
|
|
||||||
|
### 1. Test Infrastructure (`tests/pipeline/conftest.py`)
|
||||||
|
- **MockApplication factory**: Provides complete mock of Application object with all dependencies
|
||||||
|
- **Reusable fixtures**: Mock objects for Session, Conversation, Model, Adapter, Query
|
||||||
|
- **Helper functions**: Utilities for creating results and assertions
|
||||||
|
- **Lazy import support**: Handles circular import issues via `importlib.import_module()`
|
||||||
|
|
||||||
|
### 2. Test Coverage
|
||||||
|
|
||||||
|
#### Pipeline Stages Tested:
|
||||||
|
- ✅ **test_bansess.py** (6 tests) - Access control whitelist/blacklist logic
|
||||||
|
- ✅ **test_ratelimit.py** (3 tests) - Rate limiting acquire/release logic
|
||||||
|
- ✅ **test_preproc.py** (3 tests) - Message preprocessing and variable setup
|
||||||
|
- ✅ **test_respback.py** (2 tests) - Response sending with/without quotes
|
||||||
|
- ✅ **test_resprule.py** (3 tests) - Group message rule matching
|
||||||
|
- ✅ **test_pipelinemgr.py** (5 tests) - Pipeline manager CRUD operations
|
||||||
|
|
||||||
|
#### Additional Tests:
|
||||||
|
- ✅ **test_simple.py** (5 tests) - Test infrastructure validation
|
||||||
|
- ✅ **test_stages_integration.py** - Integration tests with full imports
|
||||||
|
|
||||||
|
**Total: 27 test cases**
|
||||||
|
|
||||||
|
### 3. CI/CD Integration
|
||||||
|
|
||||||
|
**GitHub Actions Workflow** (`.github/workflows/pipeline-tests.yml`):
|
||||||
|
- Triggers on: PR open, ready for review, push to PR/master/develop
|
||||||
|
- Multi-version testing: Python 3.10, 3.11, 3.12
|
||||||
|
- Coverage reporting: Integrated with Codecov
|
||||||
|
- Auto-runs via `run_tests.sh` script
|
||||||
|
|
||||||
|
### 4. Configuration Files
|
||||||
|
|
||||||
|
- **pytest.ini** - Pytest configuration with asyncio support
|
||||||
|
- **run_tests.sh** - Automated test runner with coverage
|
||||||
|
- **tests/README.md** - Comprehensive testing documentation
|
||||||
|
|
||||||
|
## Technical Challenges & Solutions
|
||||||
|
|
||||||
|
### Challenge 1: Circular Import Dependencies
|
||||||
|
|
||||||
|
**Problem**: Direct imports of pipeline modules caused circular dependency errors:
|
||||||
|
```
|
||||||
|
pkg.pipeline.stage → pkg.core.app → pkg.pipeline.pipelinemgr → pkg.pipeline.resprule
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Implemented lazy imports using `importlib.import_module()`:
|
||||||
|
```python
|
||||||
|
def get_bansess_module():
|
||||||
|
return import_module('pkg.pipeline.bansess.bansess')
|
||||||
|
|
||||||
|
# Use in tests
|
||||||
|
bansess = get_bansess_module()
|
||||||
|
stage = bansess.BanSessionCheckStage(mock_app)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Challenge 2: Pydantic Validation Errors
|
||||||
|
|
||||||
|
**Problem**: Some stages use Pydantic models that validate `new_query` parameter.
|
||||||
|
|
||||||
|
**Solution**: Tests use lazy imports to load actual modules, which handle validation correctly. Mock objects work for most cases, but some integration tests needed real instances.
|
||||||
|
|
||||||
|
### Challenge 3: Mock Configuration
|
||||||
|
|
||||||
|
**Problem**: Lists don't allow `.copy` attribute assignment in Python.
|
||||||
|
|
||||||
|
**Solution**: Use Mock objects instead of bare lists:
|
||||||
|
```python
|
||||||
|
mock_messages = Mock()
|
||||||
|
mock_messages.copy = Mock(return_value=[])
|
||||||
|
conversation.messages = mock_messages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Execution
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
|
||||||
|
Running `bash run_tests.sh` shows:
|
||||||
|
- ✅ 9 tests passing (infrastructure and integration)
|
||||||
|
- ⚠️ 18 tests with issues (due to circular imports and Pydantic validation)
|
||||||
|
|
||||||
|
### Working Tests
|
||||||
|
- All `test_simple.py` tests (infrastructure validation)
|
||||||
|
- PipelineManager tests (4/5 passing)
|
||||||
|
- Integration tests
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
Some tests encounter:
|
||||||
|
1. **Circular import errors** - When importing certain stage modules
|
||||||
|
2. **Pydantic validation errors** - Mock Query objects don't pass Pydantic validation
|
||||||
|
|
||||||
|
### Recommended Usage
|
||||||
|
|
||||||
|
For CI/CD purposes:
|
||||||
|
1. Run `test_simple.py` to validate test infrastructure
|
||||||
|
2. Run `test_pipelinemgr.py` for manager logic
|
||||||
|
3. Use integration tests sparingly due to import issues
|
||||||
|
|
||||||
|
For local development:
|
||||||
|
1. Use the test infrastructure as a template
|
||||||
|
2. Add new tests following the lazy import pattern
|
||||||
|
3. Prefer integration-style tests that test behavior not imports
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
### Short Term
|
||||||
|
1. **Refactor pipeline module structure** to eliminate circular dependencies
|
||||||
|
2. **Add Pydantic model factories** for creating valid test instances
|
||||||
|
3. **Expand integration tests** once import issues are resolved
|
||||||
|
|
||||||
|
### Long Term
|
||||||
|
1. **Integration tests** - Full pipeline execution tests
|
||||||
|
2. **Performance benchmarks** - Measure stage execution time
|
||||||
|
3. **Mutation testing** - Verify test quality with mutation testing
|
||||||
|
4. **Property-based testing** - Use Hypothesis for edge case discovery
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── .github/workflows/
|
||||||
|
│ └── pipeline-tests.yml # CI/CD workflow
|
||||||
|
├── tests/
|
||||||
|
│ ├── README.md # Testing documentation
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── pipeline/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── conftest.py # Shared fixtures
|
||||||
|
│ ├── test_simple.py # Infrastructure tests ✅
|
||||||
|
│ ├── test_bansess.py # BanSession tests
|
||||||
|
│ ├── test_ratelimit.py # RateLimit tests
|
||||||
|
│ ├── test_preproc.py # PreProcessor tests
|
||||||
|
│ ├── test_respback.py # ResponseBack tests
|
||||||
|
│ ├── test_resprule.py # ResponseRule tests
|
||||||
|
│ ├── test_pipelinemgr.py # Manager tests ✅
|
||||||
|
│ └── test_stages_integration.py # Integration tests
|
||||||
|
├── pytest.ini # Pytest config
|
||||||
|
├── run_tests.sh # Test runner
|
||||||
|
└── TESTING_SUMMARY.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Run Tests Locally
|
||||||
|
```bash
|
||||||
|
bash run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Specific Test File
|
||||||
|
```bash
|
||||||
|
pytest tests/pipeline/test_simple.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run with Coverage
|
||||||
|
```bash
|
||||||
|
pytest tests/pipeline/ --cov=pkg/pipeline --cov-report=html
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Coverage Report
|
||||||
|
```bash
|
||||||
|
open htmlcov/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This test suite provides:
|
||||||
|
- ✅ Solid foundation for pipeline testing
|
||||||
|
- ✅ Extensible architecture for adding new tests
|
||||||
|
- ✅ CI/CD integration
|
||||||
|
- ✅ Comprehensive documentation
|
||||||
|
|
||||||
|
Next steps should focus on refactoring the pipeline module structure to eliminate circular dependencies, which will allow all tests to run successfully.
|
||||||
394
docs/WEBSOCKET_README.md
Normal file
394
docs/WEBSOCKET_README.md
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
# LangBot WebSocket 双向通信系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
这是一个内置在 LangBot 中的完整 IM (即时通讯) 系统,支持:
|
||||||
|
|
||||||
|
- ✅ WebSocket 双向实时通信
|
||||||
|
- ✅ 多个客户端并发连接
|
||||||
|
- ✅ 前端到后端的消息发送
|
||||||
|
- ✅ 后端到前端的主动推送
|
||||||
|
- ✅ 流式响应支持
|
||||||
|
- ✅ 连接管理和会话隔离
|
||||||
|
- ✅ 心跳机制
|
||||||
|
- ✅ 广播消息功能
|
||||||
|
|
||||||
|
## 架构设计
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **WebSocketConnectionManager** (`websocket_manager.py`)
|
||||||
|
- 管理所有活跃的 WebSocket 连接
|
||||||
|
- 支持按流水线、会话类型查询连接
|
||||||
|
- 提供广播和单播功能
|
||||||
|
- 线程安全的并发访问控制
|
||||||
|
|
||||||
|
2. **WebSocketAdapter** (`websocket_adapter.py`)
|
||||||
|
- 实现平台适配器接口
|
||||||
|
- 处理消息的接收和发送
|
||||||
|
- 支持流式输出
|
||||||
|
- 管理消息历史
|
||||||
|
|
||||||
|
3. **WebSocketChatRouterGroup** (`websocket_chat.py`)
|
||||||
|
- WebSocket 路由控制器
|
||||||
|
- 处理连接建立、消息收发
|
||||||
|
- 实现心跳机制
|
||||||
|
- 提供 REST API 接口
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### WebSocket 连接
|
||||||
|
|
||||||
|
#### 建立连接
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://localhost:8000/api/v1/pipelines/<pipeline_uuid>/ws/connect?session_type=<person|group>
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数:**
|
||||||
|
- `pipeline_uuid`: 流水线 UUID (必需)
|
||||||
|
- `session_type`: 会话类型,可选 `person` 或 `group` (默认: `person`)
|
||||||
|
|
||||||
|
**连接成功响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "connected",
|
||||||
|
"connection_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"pipeline_uuid": "your-pipeline-uuid",
|
||||||
|
"session_type": "person",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息格式
|
||||||
|
|
||||||
|
#### 客户端发送消息
|
||||||
|
|
||||||
|
**发送聊天消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"message": [
|
||||||
|
{
|
||||||
|
"type": "Plain",
|
||||||
|
"text": "你好,这是一条测试消息"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**发送心跳:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**主动断开连接:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "disconnect"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务器响应消息
|
||||||
|
|
||||||
|
**聊天响应 (流式):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "response",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "这是机器人的回复",
|
||||||
|
"message_chain": [...],
|
||||||
|
"timestamp": "2025-01-28T12:00:00",
|
||||||
|
"is_final": false,
|
||||||
|
"connection_id": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**心跳响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pong",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**广播消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "broadcast",
|
||||||
|
"message": "这是一条广播消息",
|
||||||
|
"timestamp": "2025-01-28T12:00:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误消息:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "error",
|
||||||
|
"message": "错误描述"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### REST API 接口
|
||||||
|
|
||||||
|
#### 1. 获取消息历史
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/messages/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"messages": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 重置会话
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/reset/<session_type>
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Session reset successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 获取连接统计
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/pipelines/<pipeline_uuid>/ws/connections
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"stats": {
|
||||||
|
"total_connections": 5,
|
||||||
|
"pipelines": 2,
|
||||||
|
"connections_by_pipeline": {
|
||||||
|
"pipeline-1": 3,
|
||||||
|
"pipeline-2": 2
|
||||||
|
},
|
||||||
|
"connections_by_session_type": {
|
||||||
|
"person": 4,
|
||||||
|
"group": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connections": [
|
||||||
|
{
|
||||||
|
"connection_id": "...",
|
||||||
|
"session_type": "person",
|
||||||
|
"created_at": "2025-01-28T12:00:00",
|
||||||
|
"last_active": "2025-01-28T12:05:00",
|
||||||
|
"is_active": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 广播消息 (后端主动推送)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/pipelines/<pipeline_uuid>/ws/broadcast
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "这是一条广播消息"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"message": "Broadcast sent successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Python 客户端示例
|
||||||
|
|
||||||
|
使用提供的测试客户端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
pip install websockets
|
||||||
|
|
||||||
|
# 单个连接测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid>
|
||||||
|
|
||||||
|
# 指定会话类型
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --session-type group
|
||||||
|
|
||||||
|
# 多连接并发测试
|
||||||
|
python test_websocket_client.py <pipeline_uuid> --multi 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript 客户端示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 建立 WebSocket 连接
|
||||||
|
const ws = new WebSocket('ws://localhost:8000/api/v1/pipelines/your-pipeline-uuid/ws/connect?session_type=person');
|
||||||
|
|
||||||
|
// 连接建立
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket 连接已建立');
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'message',
|
||||||
|
message: [
|
||||||
|
{
|
||||||
|
type: 'Plain',
|
||||||
|
text: '你好'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'connected') {
|
||||||
|
console.log('连接成功:', data.connection_id);
|
||||||
|
} else if (data.type === 'response') {
|
||||||
|
console.log('机器人回复:', data.data.content);
|
||||||
|
if (data.data.is_final) {
|
||||||
|
console.log('响应完成');
|
||||||
|
}
|
||||||
|
} else if (data.type === 'broadcast') {
|
||||||
|
console.log('收到广播:', data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket 连接已关闭');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket 错误:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送心跳
|
||||||
|
setInterval(() => {
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'ping' }));
|
||||||
|
}
|
||||||
|
}, 30000); // 每 30 秒发送一次心跳
|
||||||
|
```
|
||||||
|
|
||||||
|
## 特性说明
|
||||||
|
|
||||||
|
### 1. 多连接支持
|
||||||
|
|
||||||
|
系统支持同时建立多个 WebSocket 连接,每个连接都有唯一的 `connection_id`。连接按照流水线和会话类型进行分组管理。
|
||||||
|
|
||||||
|
### 2. 双向通信
|
||||||
|
|
||||||
|
- **前端 → 后端**: 客户端可以主动发送消息给服务器
|
||||||
|
- **后端 → 前端**: 服务器可以通过广播 API 主动推送消息给客户端
|
||||||
|
|
||||||
|
### 3. 流式响应
|
||||||
|
|
||||||
|
支持流式输出,机器人的响应会分块发送,客户端可以实时显示部分响应内容。
|
||||||
|
|
||||||
|
### 4. 会话隔离
|
||||||
|
|
||||||
|
支持 `person` 和 `group` 两种会话类型,不同类型的会话消息历史互不影响。
|
||||||
|
|
||||||
|
### 5. 连接管理
|
||||||
|
|
||||||
|
- 自动追踪连接状态
|
||||||
|
- 记录最后活跃时间
|
||||||
|
- 支持连接统计查询
|
||||||
|
- 连接断开时自动清理资源
|
||||||
|
|
||||||
|
### 6. 心跳机制
|
||||||
|
|
||||||
|
客户端可以定期发送 `ping` 消息,服务器会响应 `pong`,用于保持连接活跃和检测连接状态。
|
||||||
|
|
||||||
|
## 架构优势
|
||||||
|
|
||||||
|
1. **高并发**: 使用 asyncio 异步架构,支持大量并发连接
|
||||||
|
2. **可扩展**: 模块化设计,易于扩展新功能
|
||||||
|
3. **线程安全**: 连接管理器使用锁机制保证并发安全
|
||||||
|
4. **消息队列**: 每个连接独立的发送队列,避免消息混乱
|
||||||
|
5. **灵活路由**: 支持按流水线、会话类型灵活路由消息
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **认证**: 当前 WebSocket 连接不需要认证,生产环境建议添加认证机制
|
||||||
|
2. **心跳**: 建议客户端实现心跳机制,避免连接超时
|
||||||
|
3. **重连**: 客户端应实现断线重连逻辑
|
||||||
|
4. **消息大小**: 注意控制单条消息大小,避免内存溢出
|
||||||
|
5. **连接数限制**: 生产环境建议设置最大连接数限制
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 连接失败
|
||||||
|
|
||||||
|
1. 检查流水线 UUID 是否正确
|
||||||
|
2. 检查服务器是否正常运行
|
||||||
|
3. 检查防火墙设置
|
||||||
|
|
||||||
|
### 消息发送失败
|
||||||
|
|
||||||
|
1. 检查消息格式是否正确
|
||||||
|
2. 检查连接是否仍然活跃
|
||||||
|
3. 查看服务器日志获取详细错误信息
|
||||||
|
|
||||||
|
### 性能问题
|
||||||
|
|
||||||
|
1. 检查并发连接数是否过多
|
||||||
|
2. 检查消息处理速度
|
||||||
|
3. 考虑使用连接池或负载均衡
|
||||||
|
|
||||||
|
## 开发调试
|
||||||
|
|
||||||
|
启用详细日志:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import logging
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_adapter').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.platform.sources.websocket_manager').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('langbot.pkg.api.http.controller.groups.pipelines.websocket_chat').setLevel(logging.DEBUG)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续改进建议
|
||||||
|
|
||||||
|
1. 添加用户认证和授权机制
|
||||||
|
2. 实现消息持久化
|
||||||
|
3. 添加消息加密
|
||||||
|
4. 实现更丰富的消息类型 (图片、文件等)
|
||||||
|
5. 添加消息已读/未读状态
|
||||||
|
6. 实现群组聊天功能
|
||||||
|
7. 添加在线状态显示
|
||||||
|
8. 实现消息撤回功能
|
||||||
335
docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md
Normal file
335
docs/agent-runner-pluginization/AGENT_CONTEXT_PROTOCOL.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# Agent-owned Context 协议设计
|
||||||
|
|
||||||
|
本文档描述插件化 AgentRunner 场景下的上下文边界。结论先行:LangBot 不应成为最终 agentic context manager;LangBot 应提供 context substrate,AgentRunner 或其背后的 agent runtime 自己决定如何管理历史、压缩、召回和 KV cache。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
**当前分支已落地**:
|
||||||
|
|
||||||
|
- ✅ `AgentRunContext` — event-first context 模型
|
||||||
|
- ✅ `ContextAccess` — cursor、inline policy、available APIs
|
||||||
|
- ✅ `AgentRunAPIProxy.history` — page/search API
|
||||||
|
- ✅ `AgentRunAPIProxy.events` — get/page API
|
||||||
|
- ✅ `AgentRunAPIProxy.artifacts` — metadata/read_range API
|
||||||
|
- ✅ `AgentRunAPIProxy.state` — get/set/delete API
|
||||||
|
- ✅ EventLog / Transcript / ArtifactStore — host 事实源
|
||||||
|
- ✅ PersistentStateStore — 持久化状态存储
|
||||||
|
- ✅ `max-round` / host-side history window 已从 LangBot Host/Pipeline 语义中移除;如某 runner 仍需要类似参数,应由该 runner 自己解释配置
|
||||||
|
- ✅ 外部 harness context projection 已用 Claude Code runner 做 MVP 验证:context 文件、skill 投影、MCP 配置和 host-owned resume state
|
||||||
|
|
||||||
|
## 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 不再把 `max-round` 作为目标设计
|
||||||
|
|
||||||
|
`max-round` 这类历史窗口参数不应继续作为 AgentRunner 协议或 Pipeline adapter 的核心概念。
|
||||||
|
|
||||||
|
如果某个 runner 仍需要“最多读取多少轮历史”这样的策略参数,应由该 runner 在自己的 manifest/config schema 中声明,并作为 binding config 存到 `ctx.config` / `runner_config`。Host 只提供 history pull API、cursor、hard cap 和权限边界;runner 自己决定是否读取、读取多少、如何截断和压缩。
|
||||||
|
|
||||||
|
当前 official local-agent 方向是通过 Host history API 拉取 transcript,并由 runner 自己管理模型上下文。它不依赖 Pipeline adapter 下发历史窗口。
|
||||||
|
|
||||||
|
新协议不应该问“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 bootstrap window。简单 runner 如果需要历史窗口,应在 runner 内部通过 Host history API 拉取并裁剪。
|
||||||
|
|
||||||
|
## 2. Event 到来时传什么
|
||||||
|
|
||||||
|
默认 `AgentRunContext` 应尽量小且稳定:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunContext(BaseModel):
|
||||||
|
run_id: str
|
||||||
|
trigger: AgentTrigger
|
||||||
|
event: AgentEventContext
|
||||||
|
conversation: ConversationContext | None
|
||||||
|
actor: ActorContext | None
|
||||||
|
subject: SubjectContext | None
|
||||||
|
input: AgentInput
|
||||||
|
delivery: DeliveryContext
|
||||||
|
resources: AgentResources
|
||||||
|
context: ContextAccess
|
||||||
|
state: AgentRunState
|
||||||
|
runtime: AgentRuntimeContext
|
||||||
|
config: dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
默认规则:
|
||||||
|
|
||||||
|
- 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 APIs when authorized.
|
||||||
|
- Official runners MUST consume Host infrastructure through the same public APIs as third-party runners.
|
||||||
|
|
||||||
|
### 2.1 必须 inline 的内容
|
||||||
|
|
||||||
|
每次 run 必须 inline:
|
||||||
|
|
||||||
|
- 当前 event 的稳定类型、id、时间、source。
|
||||||
|
- 当前输入文本和结构化内容。
|
||||||
|
- 附件 / 文件 / 图片的 metadata 和 artifact ref。
|
||||||
|
- actor、subject、conversation、thread、bot、workspace。
|
||||||
|
- delivery 能力,例如是否支持 streaming、reply target、平台限制。
|
||||||
|
- 已授权资源列表。
|
||||||
|
- context cursors 和可用 API 能力。
|
||||||
|
- runner binding config。
|
||||||
|
|
||||||
|
这些是 agent 决定下一步需要的最低信息。
|
||||||
|
|
||||||
|
### 2.2 默认不 inline 的内容
|
||||||
|
|
||||||
|
默认不要 inline:
|
||||||
|
|
||||||
|
- 完整历史消息。
|
||||||
|
- 大文件全文。
|
||||||
|
- 大工具结果。
|
||||||
|
- 全量知识库内容。
|
||||||
|
- 平台原始 payload 大对象。
|
||||||
|
- 每轮重新生成的大段 summary。
|
||||||
|
|
||||||
|
这些会破坏跨进程序列化成本、泄露范围、KV cache 稳定性,也会迫使 host 替 agent 做 context 策略。
|
||||||
|
|
||||||
|
### 2.3 不提供 Host Bootstrap Window
|
||||||
|
|
||||||
|
`AgentRunContext.bootstrap` 可以作为协议里的可选扩展字段保留,但 LangBot Host 默认不填历史窗口,也不通过 Pipeline 配置决定窗口大小。
|
||||||
|
|
||||||
|
如果 runner 需要类似 `recent_tail` 的策略,它应在自己的 manifest/config schema 中声明参数,并在 runner 内部通过 `history_page` / `history_search` 读取、裁剪和压缩历史。Host 只负责权限、分页、hard cap 和事实源。
|
||||||
|
|
||||||
|
## 3. ContextAccess
|
||||||
|
|
||||||
|
`ContextAccess` 是 host 交给 agent 的上下文读取入口描述:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ContextAccess(BaseModel):
|
||||||
|
conversation_id: str | None
|
||||||
|
thread_id: str | None
|
||||||
|
latest_cursor: str | None
|
||||||
|
event_seq: int | None
|
||||||
|
transcript_seq: int | None
|
||||||
|
has_history_before: bool
|
||||||
|
inline_policy: InlineContextPolicy
|
||||||
|
available_apis: ContextAPICapabilities
|
||||||
|
```
|
||||||
|
|
||||||
|
它告诉 agent:
|
||||||
|
|
||||||
|
- 当前事件位于哪条 conversation / thread。
|
||||||
|
- 若需要更多历史,从哪个 cursor 开始拉。
|
||||||
|
- host inline 了什么,没 inline 什么。
|
||||||
|
- 当前 run 有哪些 context API 权限。
|
||||||
|
|
||||||
|
## 4. Agent 如何获取更多上下文
|
||||||
|
|
||||||
|
所有 API 都必须走 `AgentRunAPIProxy`,并由 host 用 `run_id` 校验。
|
||||||
|
|
||||||
|
### 4.1 History API
|
||||||
|
|
||||||
|
```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 API
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.history.search(
|
||||||
|
query="用户之前提到的数据库连接信息",
|
||||||
|
filters={
|
||||||
|
"conversation_id": ctx.context.conversation_id,
|
||||||
|
"event_types": ["message.received"],
|
||||||
|
},
|
||||||
|
top_k=10,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Search 可以先用数据库全文索引,后续再接 embedding recall。它是 host 提供的检索能力,不等于 agent 的长期记忆策略。
|
||||||
|
|
||||||
|
### 4.3 Event API
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.events.get(event_id)
|
||||||
|
await api.events.page(before_cursor=..., limit=...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Event API 用于读取非消息事件、工具事件、系统事件。Agent 不应把所有事件都当成 user/assistant message。
|
||||||
|
|
||||||
|
### 4.4 Artifact API
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.artifacts.metadata(artifact_id)
|
||||||
|
await api.artifacts.read_range(artifact_id, offset=0, length=65536)
|
||||||
|
await api.artifacts.open_stream(artifact_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- 校验 artifact 所属 conversation / run / binding。
|
||||||
|
- 校验 MIME、大小、过期时间和权限。
|
||||||
|
- 大文件按 range/stream 读取。
|
||||||
|
- 工具大结果也应 artifact 化。
|
||||||
|
|
||||||
|
### 4.5 State API
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.state.get(scope="conversation", key="external.session_id")
|
||||||
|
await api.state.set(scope="conversation", key="summary.checkpoint", value=...)
|
||||||
|
```
|
||||||
|
|
||||||
|
State 是可选寄宿能力。自管 runtime 可以完全不用;依附 LangBot 的官方 runner 可以使用。
|
||||||
|
|
||||||
|
### 4.6 External harness context projection
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 这类 runtime 通常已经有自己的 session、工具 loop、MCP 加载、上下文压缩和工作目录。LangBot 不应把这类 runner 强行改造成“host prompt assembler”,而应提供可审计的事件和资源投影。
|
||||||
|
|
||||||
|
推荐 projection 形态:
|
||||||
|
|
||||||
|
- `agent-context.json`:结构化 JSON,包含 `run_id`、`event`、`actor`、`subject`、`input`、`delivery`、`resources`、`context`、`state`、`runtime`。
|
||||||
|
- `LANGBOT_CONTEXT.md`:人类可读摘要,用于 code-agent harness 快速理解当前 IM 事件。
|
||||||
|
- `resources`:只包含本次 run 授权后的模型、工具、知识库、artifact、state/storage 句柄,不暴露 Host 内部私有对象。
|
||||||
|
- `skills`:Host 或 binding 把已授权 skill 投影为目标 harness 可读目录,例如 Claude Code 的 `.claude/skills/<name>/SKILL.md`。
|
||||||
|
- `MCP config`:Host 或 binding 提供 scoped MCP 配置,runner adapter 转成目标 harness 的配置文件或 CLI 参数。
|
||||||
|
- `state pointers`:外部 session id、working directory、checkpoint 等小型 JSON 状态通过 Host state API 保存,例如 `external.session_id`、`external.working_directory`。
|
||||||
|
|
||||||
|
当前 Claude Code runner MVP 使用 schema `langbot.agent_runner.external_harness_context.v1`,并已通过 WebUI Debug Chat 验证 context 文件、skill 文件、MCP config 和 resume state 的基本链路。
|
||||||
|
|
||||||
|
这类 projection 是“把 LangBot 事实源和授权资源交给 harness”,不是“由 LangBot 决定最终模型上下文”。外部 harness 可以继续使用自己的 transcript、工具权限和压缩策略。
|
||||||
|
|
||||||
|
## 5. Runner manifest 中的上下文声明
|
||||||
|
|
||||||
|
建议增加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
context:
|
||||||
|
ownership: self_managed | host_bootstrap | hybrid
|
||||||
|
bootstrap: none | current_event | recent_tail | summary_tail
|
||||||
|
max_inline_events: 0
|
||||||
|
max_inline_bytes: 0
|
||||||
|
supports_history_pull: true
|
||||||
|
supports_history_search: true
|
||||||
|
supports_artifact_pull: true
|
||||||
|
owns_compaction: true
|
||||||
|
wants_static_context_refs: true
|
||||||
|
```
|
||||||
|
|
||||||
|
语义:
|
||||||
|
|
||||||
|
- `self_managed`: Host 不主动 inline 历史,只提供 event 和 handles。
|
||||||
|
- `host_bootstrap`: Host 为简单 runner inline 一个小窗口。
|
||||||
|
- `hybrid`: Host inline summary/tail,runner 仍可按需拉更多。
|
||||||
|
- `owns_compaction`: runner 负责压缩,host 不做语义摘要。
|
||||||
|
- `wants_static_context_refs`: host 用 ref/hash 描述静态内容,减少重复 payload。
|
||||||
|
|
||||||
|
## 6. KV cache 友好的上下文管理
|
||||||
|
|
||||||
|
如果目标是支持 Claude Code SDK、Codex、Pi Agent SDK 等 runtime,必须避免每轮由 LangBot 重组大块 prompt。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 稳定 session key:`workspace/bot/binding/runner/conversation/thread`。
|
||||||
|
- 静态内容使用 `ref + version/hash`: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。
|
||||||
|
|
||||||
|
## 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 APIs 消费这些能力。
|
||||||
|
|
||||||
|
LangBot core 不应内置官方 agent 的业务流程:
|
||||||
|
|
||||||
|
- 不内置 prompt 组装策略。
|
||||||
|
- 不内置 tool loop。
|
||||||
|
- 不内置 RAG 编排策略。
|
||||||
|
- 不内置 summary / compaction 策略。
|
||||||
|
- 不内置“local-agent 专用”的状态字段。
|
||||||
|
|
||||||
|
官方 local-agent 应作为“依附 LangBot 基础设施的复杂 runner 参考实现”存在:
|
||||||
|
|
||||||
|
- transcript / history 通过 `api.history.page()` 或 `api.history.search()` 读取。
|
||||||
|
- summary、checkpoint、外部 session id、用户偏好通过 `api.state` 或 `api.storage` 保存。
|
||||||
|
- 图片、文件、工具大结果通过 `api.artifacts` 读取。
|
||||||
|
- 模型、工具、知识库通过 `api.models`、`api.tools`、`api.knowledge` 调用。
|
||||||
|
|
||||||
|
这样 LangBot 保持为通用 agent host,不变成内置 agent 框架。
|
||||||
|
|
||||||
|
## 9. 当前实现需要调整
|
||||||
|
|
||||||
|
**已完成(当前分支)**:
|
||||||
|
|
||||||
|
- ✅ `max-round` 不再是协议字段,也不再是 Host / Pipeline 通用语义
|
||||||
|
- ✅ 新 runner 默认不收到历史窗口
|
||||||
|
- ✅ `AgentRunContext` 增加 `context` / cursor / access capabilities
|
||||||
|
- ✅ `AgentRunAPIProxy` 增加 history / events / artifacts / state API
|
||||||
|
- ✅ Host 增加持久 EventLog / Transcript / ArtifactStore / PersistentStateStore
|
||||||
|
- ✅ `run_from_query()` 委托到 event-first `run(event, binding)`
|
||||||
|
- ✅ Claude Code external harness smoke:context JSON / Markdown、skill、MCP config、`external.session_id` / `external.working_directory`
|
||||||
|
|
||||||
|
这样 LangBot 既能服务依附 host 基础设施的官方 runner,也能服务自带 memory/session/cache 的外部 agent runtime。
|
||||||
237
docs/agent-runner-pluginization/EVENT_BASED_AGENT.md
Normal file
237
docs/agent-runner-pluginization/EVENT_BASED_AGENT.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Event Based Agent 预留设计
|
||||||
|
|
||||||
|
> **注意**:本文档是 future design note,不是当前分支实现范围。
|
||||||
|
>
|
||||||
|
> EventGateway、EventRouter、Event subscription/notification 由其他分支实现。
|
||||||
|
> 本分支只预留 event-first 入口和 envelope/binding models。
|
||||||
|
> 2026-05-29 的 local-agent / Claude Code runner smoke 只验证本分支的 `run(event, binding)` 调度边界,不表示 EBA 分支已经完成联调。
|
||||||
|
|
||||||
|
本文档描述未来 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. Event Envelope
|
||||||
|
|
||||||
|
建议事件 envelope:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentEventEnvelope(BaseModel):
|
||||||
|
event_id: str
|
||||||
|
event_type: str
|
||||||
|
event_time: int | None
|
||||||
|
source: EventSource
|
||||||
|
workspace_id: str | None
|
||||||
|
bot_id: str | None
|
||||||
|
conversation_id: str | None
|
||||||
|
thread_id: str | None
|
||||||
|
actor: ActorRef | None
|
||||||
|
subject: SubjectRef | None
|
||||||
|
input: AgentInput
|
||||||
|
delivery: DeliveryContext
|
||||||
|
raw_ref: RawEventRef | None
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
顶层字段使用 LangBot 稳定协议名。平台原始事件名和原始 payload 放到 `metadata` 或 `raw_ref`,不直接成为 runner 的稳定依赖。
|
||||||
|
|
||||||
|
## 4. Event Source
|
||||||
|
|
||||||
|
事件来源可以包括:
|
||||||
|
|
||||||
|
- `platform_adapter`: 飞书、QQ、微信、Telegram 等 IM 平台。
|
||||||
|
- `webui`: Debug Chat、控制台操作。
|
||||||
|
- `http_api`: 外部系统调用 LangBot。
|
||||||
|
- `scheduler`: 定时任务。
|
||||||
|
- `system`: runtime、plugin、maintenance 事件。
|
||||||
|
|
||||||
|
同一个 event source 可以产生多个 event type。EventRouter 不应该写死平台 adapter 的类名。
|
||||||
|
|
||||||
|
## 5. Event Binding
|
||||||
|
|
||||||
|
EBA 中,AgentBinding 取代 Pipeline runner 配置成为触发关系:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentBinding(BaseModel):
|
||||||
|
binding_id: str
|
||||||
|
enabled: bool
|
||||||
|
event_types: list[str]
|
||||||
|
scope: BindingScope
|
||||||
|
filters: list[EventFilter]
|
||||||
|
runner_id: str
|
||||||
|
runner_config: dict[str, Any]
|
||||||
|
resource_policy: ResourcePolicy
|
||||||
|
state_policy: StatePolicy
|
||||||
|
delivery_policy: DeliveryPolicy
|
||||||
|
```
|
||||||
|
|
||||||
|
Binding scope 示例:
|
||||||
|
|
||||||
|
- workspace 全局。
|
||||||
|
- bot 级别。
|
||||||
|
- platform channel 级别。
|
||||||
|
- conversation / group / thread 级别。
|
||||||
|
- user / actor 级别。
|
||||||
|
|
||||||
|
旧 Pipeline 可以迁移为 `message.received` 的 binding source,但不是唯一 binding source。
|
||||||
|
|
||||||
|
## 6. EventRouter 调用链
|
||||||
|
|
||||||
|
目标调用链:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform Adapter / WebUI / API
|
||||||
|
-> Event Gateway normalize payload
|
||||||
|
-> EventLog append raw event
|
||||||
|
-> EventRouter resolve bindings
|
||||||
|
-> AgentRunOrchestrator.run(event, binding)
|
||||||
|
-> AgentRunContextBuilder.build(event, binding)
|
||||||
|
-> PluginRuntimeConnector.run_agent()
|
||||||
|
-> AgentRunResult stream
|
||||||
|
-> DeliveryController render / platform action
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- `run_from_event()` 必须复用现有 orchestrator 能力。
|
||||||
|
- 不能为 EBA 单独实现另一套 plugin runner 调用协议。
|
||||||
|
- 不能让非消息事件绕过 resource authorization。
|
||||||
|
- Delivery 和 platform action 要走统一权限模型。
|
||||||
|
- 外部 harness runner 也应通过同一套 envelope/binding/context/result 协议接入;EBA 不应为 Claude Code / Codex / Kimi Code 单独发明队列协议。
|
||||||
|
|
||||||
|
## 7. Delivery Context
|
||||||
|
|
||||||
|
Event 不一定回复到当前聊天窗口。需要显式 delivery:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DeliveryContext(BaseModel):
|
||||||
|
surface: str
|
||||||
|
reply_target: ReplyTarget | None
|
||||||
|
supports_streaming: bool
|
||||||
|
supports_edit: bool
|
||||||
|
supports_reaction: bool
|
||||||
|
max_message_size: int | None
|
||||||
|
platform_capabilities: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
消息事件通常带 reply target。系统事件可能没有默认 reply target,需要 runner 返回 `action.requested` 或由 binding 的 delivery policy 决定投递位置。
|
||||||
|
|
||||||
|
## 8. AgentRunResult 与平台动作
|
||||||
|
|
||||||
|
当前消息路径主要消费:
|
||||||
|
|
||||||
|
- `message.delta`
|
||||||
|
- `message.completed`
|
||||||
|
- `run.completed`
|
||||||
|
- `run.failed`
|
||||||
|
|
||||||
|
EBA 后需要预留:
|
||||||
|
|
||||||
|
- `action.requested`: 请求 host 执行平台动作。
|
||||||
|
- `artifact.created`: runner 生成文件或大结果。
|
||||||
|
- `delivery.requested`: 请求投递到某个 surface。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```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 是否允许。
|
||||||
|
- 是否需要人工审批。
|
||||||
|
|
||||||
|
本阶段如收到 `action.requested`,可以只记录 telemetry,不执行。
|
||||||
|
|
||||||
|
## 9. 与 Context 协议的关系
|
||||||
|
|
||||||
|
EBA 事件进入 AgentRunner 时仍使用 [AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md) 的原则:
|
||||||
|
|
||||||
|
- inline 当前事件。
|
||||||
|
- 大 payload 用 raw/artifact ref。
|
||||||
|
- 不默认 inline 完整 history。
|
||||||
|
- agent 按需通过 API 拉 history/event/artifact/state。
|
||||||
|
- Host 保留 EventLog 和权限 guardrail。
|
||||||
|
|
||||||
|
非消息事件可以被投影进 Transcript,但不能强制伪装为 user message。AgentRunner 可以根据 event type 自己决定是否把它纳入模型上下文。
|
||||||
|
|
||||||
|
## 10. 当前实现与目标差距
|
||||||
|
|
||||||
|
**当前分支已落地(Event-first 基础设施)**:
|
||||||
|
|
||||||
|
- ✅ `AgentRunOrchestrator` — event-first `run(event, binding)` 入口
|
||||||
|
- ✅ `AgentRunContextBuilder` — event-first context 构建
|
||||||
|
- ✅ `AgentEventEnvelope` 模型
|
||||||
|
- ✅ `AgentBinding` 模型
|
||||||
|
- ✅ `AgentRunResult` 基础消息流
|
||||||
|
- ✅ `ctx.event` 的最小消息事件封装
|
||||||
|
- ✅ `PipelineAdapter` — Query → Event + Binding 转换
|
||||||
|
- ✅ `run_from_query()` → `run(event, binding)` 委托
|
||||||
|
- ✅ EventLog / Transcript / ArtifactStore
|
||||||
|
- ✅ History / Event / Artifact / State pull APIs
|
||||||
|
- ✅ 当前消息事件 path 已用 `local-agent` 与 Claude Code external harness runner 做本地 smoke
|
||||||
|
|
||||||
|
**其他分支负责(非本分支范围)**:
|
||||||
|
|
||||||
|
- EventGateway 实现
|
||||||
|
- EventRouter 实现
|
||||||
|
- Event subscription / notification
|
||||||
|
- EventLog 持久化管理 UI
|
||||||
|
- AgentBinding 持久化 UI
|
||||||
|
- 平台动作执行 (`action.requested` 执行器)
|
||||||
|
|
||||||
|
**未来 EBA 完整落地需要**:
|
||||||
|
|
||||||
|
- EventGateway 完整实现
|
||||||
|
- EventRouter 与 BindingResolver 集成
|
||||||
|
- AgentBinding 持久模型和 UI
|
||||||
|
- DeliveryContext 完整实现
|
||||||
|
- platform action permission model 和执行器
|
||||||
|
- 真实平台事件接入
|
||||||
|
|
||||||
|
## 11. 落地顺序
|
||||||
|
|
||||||
|
1. 先把当前 Pipeline 消息入口适配成 `message.received` event。
|
||||||
|
2. 增加 `AgentBinding` 抽象,先由 Pipeline config 生成。
|
||||||
|
3. `AgentRunContextBuilder` 改为从 event + binding 构造 context。
|
||||||
|
4. 引入 EventLog / Transcript。
|
||||||
|
5. 增加非消息事件的协议测试,不接真实平台。
|
||||||
|
6. 再接入真实 EventRouter 和 platform action。
|
||||||
427
docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md
Normal file
427
docs/agent-runner-pluginization/HOST_SDK_INFRASTRUCTURE.md
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
# LangBot Host 与 SDK 基础设施设计
|
||||||
|
|
||||||
|
本文档描述 LangBot 和 SDK 为插件化 AgentRunner 共同提供的基础设施。它不以 Pipeline 为中心,也不以官方 local-agent 的实现方式为前提。
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
LangBot 要转为 agent host,而不是内置 runner 容器:
|
||||||
|
|
||||||
|
- 接收 IM、WebUI、API 和未来 EventRouter 产生的事件。
|
||||||
|
- 根据事件、bot、workspace、scope 解析应该调用的 agent binding。
|
||||||
|
- 发现、校验和调用插件提供的 AgentRunner。
|
||||||
|
- 为每次 run 提供受限资源、状态、存储、上下文引用和生命周期控制。
|
||||||
|
- 接收 AgentRunner 返回的事件流,并投递到 IM、WebUI 或其他 output surface。
|
||||||
|
|
||||||
|
SDK 要提供稳定协议:
|
||||||
|
|
||||||
|
- `AgentRunner` 组件定义。
|
||||||
|
- runner manifest / capabilities / permissions / config schema。
|
||||||
|
- `AgentRunContext` 输入 envelope。
|
||||||
|
- `AgentRunResult` 输出事件流。
|
||||||
|
- `AgentRunAPIProxy` 运行期受限 API。
|
||||||
|
|
||||||
|
## 2. 非目标
|
||||||
|
|
||||||
|
- 不把 Pipeline 当作长期架构中心。
|
||||||
|
- 不要求所有 AgentRunner 依赖 LangBot 的上下文管理。
|
||||||
|
- 不要求官方 local-agent 的旧行为反向塑造 host 协议。
|
||||||
|
- 不在 host 中实现通用 agentic prompt assembler。
|
||||||
|
- 不强制 runner 使用 LangBot state / storage;LangBot 只提供可选、受控的寄宿能力。
|
||||||
|
- **不实现 EventGateway**: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
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前状态**:
|
||||||
|
- `PipelineAdapter` 作为当前入口 adapter,将 Pipeline Query 转换为 `AgentEventEnvelope` + `AgentBinding`
|
||||||
|
- `run_from_query()` 内部委托到 `run(event, binding)`
|
||||||
|
- EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
|
||||||
|
- `local-agent` 与 Claude Code runner 已通过本地 WebUI smoke,验证同一条 `run(event, binding)` path 可服务 host-infra runner 与外部 harness runner
|
||||||
|
- EventGateway 由外部 event branch 实现
|
||||||
|
|
||||||
|
当前 Pipeline 只应接入在 Pipeline adapter 位置。它可以继续产生 `message.received`,但不应继续拥有 runner 选择、上下文裁剪和业务 agent 执行的核心语义。
|
||||||
|
|
||||||
|
## 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 触发。
|
||||||
|
- 后续非消息事件,例如入群、撤回、好友申请。
|
||||||
|
|
||||||
|
输出应是稳定 envelope,而不是 Pipeline Query 私有结构:
|
||||||
|
|
||||||
|
```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
|
||||||
|
delivery: DeliveryContext
|
||||||
|
raw_ref: RawEventRef | None
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前 adapter source**:`PipelineAdapter.query_to_event(query)` 从 Pipeline Query 生成 `AgentEventEnvelope`。
|
||||||
|
|
||||||
|
原始平台 payload 可以存为 raw event 或 artifact ref;不要把平台私有字段直接扩散到 AgentRunner 顶层协议。
|
||||||
|
|
||||||
|
### 4.2 Agent Binding
|
||||||
|
|
||||||
|
Agent binding 是”什么事件调用哪个 runner、带什么绑定配置”的持久配置。它替代长期依赖 Pipeline runner config 的角色。
|
||||||
|
|
||||||
|
建议模型:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentBinding(BaseModel):
|
||||||
|
binding_id: str
|
||||||
|
scope: BindingScope
|
||||||
|
event_types: list[str]
|
||||||
|
runner_id: str
|
||||||
|
runner_config: dict[str, Any]
|
||||||
|
resource_policy: ResourcePolicy
|
||||||
|
state_policy: StatePolicy
|
||||||
|
delivery_policy: DeliveryPolicy
|
||||||
|
enabled: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前 adapter source**:`PipelineAdapter.pipeline_config_to_binding(query, runner_id)` 从 Pipeline config 生成临时 `AgentBinding`。
|
||||||
|
|
||||||
|
Pipeline 当前可以被迁移为一种 binding source:
|
||||||
|
|
||||||
|
- Pipeline AI runner config -> `AgentBinding`
|
||||||
|
- Pipeline extension preference -> `resource_policy`
|
||||||
|
- Pipeline output settings -> `delivery_policy`
|
||||||
|
|
||||||
|
但新设计不应再把这些字段命名为 Pipeline 专属概念。
|
||||||
|
|
||||||
|
### 4.3 AgentRunnerRegistry
|
||||||
|
|
||||||
|
Registry 负责收集 runner descriptor:
|
||||||
|
|
||||||
|
- 插件 runtime 提供的 `AgentRunner`。
|
||||||
|
- 可能存在的 host adapter runner。
|
||||||
|
- 开发期本地插件 runner。
|
||||||
|
|
||||||
|
Descriptor 必须包含:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerDescriptor(BaseModel):
|
||||||
|
id: str
|
||||||
|
source: Literal["plugin", "host_adapter"]
|
||||||
|
label: I18nObject
|
||||||
|
description: I18nObject | None = None
|
||||||
|
capabilities: AgentRunnerCapabilities
|
||||||
|
permissions: AgentRunnerPermissions
|
||||||
|
config_schema: list[DynamicFormItemSchema]
|
||||||
|
plugin: PluginRef | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
`plugin:author/name/runner` 仍可作为稳定 id 格式。多个 binding 指向同一个 runner id 时,不创建多个插件实例。
|
||||||
|
|
||||||
|
### 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()` 这类 API 可以保留为 Pipeline adapter 入口,但内部应转换成 event + binding 后走统一 `run()`。
|
||||||
|
|
||||||
|
### 4.5 Resource Authorization
|
||||||
|
|
||||||
|
LangBot 在每次 run 前生成 `ctx.resources`。资源来自三层约束:
|
||||||
|
|
||||||
|
- runner manifest 声明的 permissions。
|
||||||
|
- binding/resource policy 允许的资源范围。
|
||||||
|
- 当前 event / actor / bot / workspace 的实际权限。
|
||||||
|
|
||||||
|
资源类型包括:
|
||||||
|
|
||||||
|
- models
|
||||||
|
- tools
|
||||||
|
- knowledge bases
|
||||||
|
- files / artifacts
|
||||||
|
- storage
|
||||||
|
- platform capabilities
|
||||||
|
- history / transcript access
|
||||||
|
|
||||||
|
运行期 action 必须再次通过 `run_id` 校验。SDK 侧本地校验只用于开发体验,host 侧校验才是安全边界。
|
||||||
|
|
||||||
|
### 4.6 State 与 Storage
|
||||||
|
|
||||||
|
LangBot 可以提供 host-owned state,让 AgentRunner 把状态寄宿在 LangBot:
|
||||||
|
|
||||||
|
- conversation state
|
||||||
|
- actor state
|
||||||
|
- subject state
|
||||||
|
- runner/binding state
|
||||||
|
- workspace state
|
||||||
|
|
||||||
|
但这不是强制。外部 agent runtime 可以维护自己的 session 和 memory。LangBot 只需要提供:
|
||||||
|
|
||||||
|
- 授权开关。
|
||||||
|
- scope key。
|
||||||
|
- get/set/list/delete API。
|
||||||
|
- 持久化 backend。
|
||||||
|
- 审计和清理策略。
|
||||||
|
|
||||||
|
当前进程内 state store 只能作为过渡实现,不能作为正式生产语义。
|
||||||
|
|
||||||
|
### 4.7 EventLog / Transcript / Artifact
|
||||||
|
|
||||||
|
LangBot 应提供事实源能力:
|
||||||
|
|
||||||
|
- `EventLog`: 保存原始事件、系统事件、工具调用、投递结果、错误。
|
||||||
|
- `Transcript`: 面向对话 UI / agent history 的消息投影。
|
||||||
|
- `ArtifactStore`: 保存大文件、多模态输入、工具大结果、平台附件。
|
||||||
|
|
||||||
|
AgentRunner 可以读取这些能力,但不能被迫使用 LangBot 作为唯一记忆系统。
|
||||||
|
|
||||||
|
### 4.8 Prompt / Instruction Package(占位)
|
||||||
|
|
||||||
|
旧 Pipeline 入口目前可以把 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 拉取 instruction package 后,仍由 runner 自己决定如何与 history、RAG、
|
||||||
|
tool 结果、memory 和当前输入组装最终模型 prompt。
|
||||||
|
- Host 不实现通用 agentic prompt assembler,也不把 Pipeline 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 可消费的形式,例如 context JSON/Markdown、MCP config、skill 目录、环境变量或 CLI 参数。
|
||||||
|
- 外部 harness 负责自己的 native session、tool loop、压缩、权限模式和 resume 机制。
|
||||||
|
|
||||||
|
当前 Claude Code runner MVP 已验证:
|
||||||
|
|
||||||
|
- LangBot event-first context 可以写入 `agent-context.json` / `LANGBOT_CONTEXT.md`。
|
||||||
|
- binding 中的 skill / MCP 配置可以投影到 Claude Code 原生目录和 CLI 参数。
|
||||||
|
- `external.session_id` 与 `external.working_directory` 可以通过 Host state 保存并用于 resume。
|
||||||
|
|
||||||
|
发布级路径隔离、secret 过滤、MCP allowlist、工具白名单、资源配额和 workspace 清理不属于当前协议闭环,详见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
|
||||||
|
## 5. SDK 侧协议
|
||||||
|
|
||||||
|
### 5.1 AgentRunner 组件
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunner(BaseComponent):
|
||||||
|
__kind__ = "AgentRunner"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_capabilities(cls) -> AgentRunnerCapabilities:
|
||||||
|
...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config_schema(cls) -> list[dict]:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def run(self, ctx: AgentRunContext) -> AsyncGenerator[AgentRunResult, None]:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Capabilities
|
||||||
|
|
||||||
|
建议能力声明:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
capabilities:
|
||||||
|
streaming: true
|
||||||
|
tool_calling: true
|
||||||
|
knowledge_retrieval: true
|
||||||
|
multimodal_input: true
|
||||||
|
event_context: true
|
||||||
|
platform_api: false
|
||||||
|
interrupt: true
|
||||||
|
stateful_session: true
|
||||||
|
self_managed_context: true
|
||||||
|
host_state: optional
|
||||||
|
```
|
||||||
|
|
||||||
|
`self_managed_context` 表示 runner 或外部 runtime 自己管理上下文。Host 不应给它强塞历史窗口,只提供当前事件和 context handles。
|
||||||
|
|
||||||
|
### 5.3 Permissions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
permissions:
|
||||||
|
models: ["invoke", "stream", "rerank"]
|
||||||
|
tools: ["detail", "call"]
|
||||||
|
knowledge_bases: ["list", "retrieve"]
|
||||||
|
history: ["page", "search"]
|
||||||
|
events: ["get", "page"]
|
||||||
|
artifacts: ["metadata", "read"]
|
||||||
|
storage: ["plugin", "workspace", "binding"]
|
||||||
|
files: ["config", "knowledge"]
|
||||||
|
platform_api: []
|
||||||
|
```
|
||||||
|
|
||||||
|
权限声明是 runner 需要的最大能力,实际可用资源仍由 binding 和当前运行上下文裁剪。
|
||||||
|
|
||||||
|
### 5.4 AgentRunContext
|
||||||
|
|
||||||
|
Context 顶层应是 event-first,而不是 Query-first:
|
||||||
|
|
||||||
|
```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
|
||||||
|
resources: AgentResources
|
||||||
|
context: ContextAccess
|
||||||
|
state: AgentRunState
|
||||||
|
runtime: AgentRuntimeContext
|
||||||
|
config: dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
`messages` 可以作为兼容字段或 bootstrap 字段,但不应继续是协议核心。
|
||||||
|
|
||||||
|
### 5.5 AgentRunResult
|
||||||
|
|
||||||
|
输出应是事件流:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunResult(BaseModel):
|
||||||
|
type: Literal[
|
||||||
|
"message.delta",
|
||||||
|
"message.completed",
|
||||||
|
"tool.call.started",
|
||||||
|
"tool.call.completed",
|
||||||
|
"state.updated",
|
||||||
|
"artifact.created",
|
||||||
|
"action.requested",
|
||||||
|
"run.completed",
|
||||||
|
"run.failed",
|
||||||
|
]
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前消息回复只消费 `message.delta` / `message.completed` / `run.failed`。平台动作执行等 EBA 和 platform API 权限落地后再启用。
|
||||||
|
|
||||||
|
### 5.6 AgentRunAPIProxy
|
||||||
|
|
||||||
|
Proxy 是 runner 访问 host 能力的唯一入口:
|
||||||
|
|
||||||
|
- model APIs
|
||||||
|
- tool APIs
|
||||||
|
- knowledge APIs
|
||||||
|
- state / storage APIs
|
||||||
|
- history / event APIs
|
||||||
|
- artifact APIs
|
||||||
|
- platform APIs
|
||||||
|
|
||||||
|
所有请求必须带 `run_id`,host 侧按 active run session 验证 runner identity 和 resource ACL。
|
||||||
|
|
||||||
|
## 6. 当前实现与目标差距
|
||||||
|
|
||||||
|
**已落地(当前分支)**:
|
||||||
|
|
||||||
|
- ✅ `AgentRunnerRegistry`
|
||||||
|
- ✅ `AgentRunOrchestrator` — event-first `run(event, binding)`
|
||||||
|
- ✅ `AgentRunContextBuilder` — event-first context
|
||||||
|
- ✅ `AgentResourceBuilder`
|
||||||
|
- ✅ `AgentRunSessionRegistry`
|
||||||
|
- ✅ `AgentRunAPIProxy` — model / tool / knowledge / history / event / artifact / state APIs
|
||||||
|
- ✅ `PipelineAdapter` — Query → Event + Binding
|
||||||
|
- ✅ `AgentBinding` 抽象
|
||||||
|
- ✅ `AgentEventEnvelope` 抽象
|
||||||
|
- ✅ `max-round` 从目标协议中移除;类似历史窗口参数若仍需要,应由具体 runner 的 manifest/config schema 暴露为 binding config
|
||||||
|
- ✅ `PersistentStateStore` — 持久化状态存储
|
||||||
|
- ✅ `EventLogStore` / `TranscriptStore` / `ArtifactStore`
|
||||||
|
- ✅ history / artifact / event 的受限拉取 API
|
||||||
|
- ✅ Claude Code external harness MVP:context/resource projection 与 host-owned resume state smoke
|
||||||
|
|
||||||
|
**其他分支负责(非本分支范围)**:
|
||||||
|
|
||||||
|
- EventGateway 实现
|
||||||
|
- EventRouter 实现
|
||||||
|
- AgentBinding 持久化 UI
|
||||||
|
- platform API 动作执行
|
||||||
|
- 发布级 security hardening
|
||||||
|
|
||||||
|
## 7. 落地顺序
|
||||||
|
|
||||||
|
**已完成**:
|
||||||
|
|
||||||
|
1. ✅ 固化 README 路由和专题文档边界。
|
||||||
|
2. ✅ 在 Host 中抽象 `AgentBinding`,由 Pipeline adapter 生成。
|
||||||
|
3. ✅ 将 `AgentRunContextBuilder` 改为 event-first。
|
||||||
|
4. ✅ 增加持久 transcript/event log/artifact/state 存储模型。
|
||||||
|
5. ✅ 扩展 `AgentRunAPIProxy` 的 history / artifact / state API。
|
||||||
|
6. ✅ 将 Pipeline-only 字段下沉到 Pipeline adapter。
|
||||||
|
7. ✅ 官方 runner 插件迁移完成(7 个插件)。
|
||||||
|
8. ✅ Claude Code runner MVP smoke:外部 harness context 投影和 state handoff。
|
||||||
|
|
||||||
|
**后续工作(其他分支)**:
|
||||||
|
|
||||||
|
- EventGateway 实现
|
||||||
|
- EventRouter 与 BindingResolver 集成
|
||||||
|
- 平台动作执行器
|
||||||
552
docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md
Normal file
552
docs/agent-runner-pluginization/IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
# Agent Runner 插件化当前实现与收尾计划
|
||||||
|
|
||||||
|
> 2026-05-29 状态说明:本文档是实现推进计划和历史上下文,不是最新验收结论的唯一来源。当前设计入口见 [README.md](./README.md),协议边界见 [PROTOCOL_V1.md](./PROTOCOL_V1.md),进度见 [PROGRESS.md](./PROGRESS.md),下一轮测试入口见 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md)。
|
||||||
|
|
||||||
|
本文档面向实现 agent,用来把当前 AgentRunner 插件化实现推进到可迁移状态。
|
||||||
|
|
||||||
|
当前代码已经不是从零开始的 PoC。LangBot 已经具备 registry、orchestrator、context/resource builder、result normalizer 和插件 runtime action。本计划重点描述剩余工作:补齐宿主通用能力、对齐旧内置 runner 行为、完成官方 runner 插件迁移验收。
|
||||||
|
|
||||||
|
## 1. 最终状态
|
||||||
|
|
||||||
|
LangBot 最终只保留 Agent Runner 的宿主能力:
|
||||||
|
|
||||||
|
- 发现 runner:`AgentRunnerRegistry`
|
||||||
|
- 选择 runner:Pipeline 配置和未来事件绑定配置
|
||||||
|
- 构造上下文:`AgentRunContext`
|
||||||
|
- 裁剪资源:模型、工具、知识库、文件、存储、平台能力
|
||||||
|
- 调度执行:`AgentRunOrchestrator`
|
||||||
|
- 归一结果:`AgentRunResult` -> 当前 Pipeline 的 `Message` / `MessageChunk`
|
||||||
|
- 隔离错误:插件异常、协议错误、超时、结果过大不能破坏主流程
|
||||||
|
- 迁移旧配置:把旧内置 runner 配置迁到官方 AgentRunner 插件配置
|
||||||
|
- 转发调用:插件 runtime 只维护已安装插件本身的运行实例,Pipeline 不创建插件实例或 runner 实例
|
||||||
|
|
||||||
|
LangBot 不再长期维护内置业务 runner 分支。`local-agent`、Dify、n8n、Coze、DashScope、Langflow、Tbox 等都迁到官方 AgentRunner 插件。
|
||||||
|
|
||||||
|
迁移期间允许旧 `RequestRunner` 文件继续存在,作为行为对齐基准和回退分析材料。它们不影响当前进度;真正的最终条件是主聊天执行路径不再依赖旧 runner。
|
||||||
|
|
||||||
|
## 1.1 当前状态快照
|
||||||
|
|
||||||
|
已完成或基本完成:
|
||||||
|
|
||||||
|
- `AgentRunnerDescriptor`、runner id 解析、registry。
|
||||||
|
- `AgentRunOrchestrator` 替换 `ChatMessageHandler` 内部 runner 调度。
|
||||||
|
- `AgentRunContextBuilder`、`AgentResourceBuilder`、`AgentResultNormalizer`。
|
||||||
|
- `ai.runner.id` + `ai.runner_config[id]` 的读取与旧配置映射。
|
||||||
|
- AgentRunner runtime action:`LIST_AGENT_RUNNERS`、`RUN_AGENT`。
|
||||||
|
- run-scoped proxy authorization:模型、工具、知识库、存储、文件。
|
||||||
|
- EventLog / Transcript / ArtifactStore / PersistentStateStore。
|
||||||
|
- Pipeline adapter 已委托到 event-first `run(event, binding)`。
|
||||||
|
- `local-agent` 与 Claude Code runner 已通过本地 WebUI smoke。
|
||||||
|
|
||||||
|
仍需收尾:
|
||||||
|
|
||||||
|
- Docs final QA 与安装/发布文档整理。
|
||||||
|
- timeout/deadline、取消、插件无输出、协议错误的端到端保护。
|
||||||
|
- 官方 runner 插件安装/预装/迁移缺失处理。
|
||||||
|
- 安全发布级 hardening:路径隔离、权限边界、secret、MCP/skill 投影策略、资源配额、审计。此项不阻塞当前协议闭环,详见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
- Codex / Kimi runner 全量接入、issue-centric 队列、复杂 workflow engine 和 EBA 分支完整联调。
|
||||||
|
|
||||||
|
## 2. 高层架构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pipeline MessageProcessor / future EventRouter
|
||||||
|
|
|
||||||
|
v
|
||||||
|
AgentRunOrchestrator
|
||||||
|
|
|
||||||
|
+--> AgentRunnerRegistry
|
||||||
|
| +--> plugin runtime LIST_AGENT_RUNNERS
|
||||||
|
| +--> descriptor cache / validation
|
||||||
|
|
|
||||||
|
+--> AgentRunContextBuilder
|
||||||
|
+--> AgentResourceBuilder
|
||||||
|
+--> AgentResultNormalizer
|
||||||
|
|
|
||||||
|
v
|
||||||
|
PluginRuntimeConnector.run_agent()
|
||||||
|
|
|
||||||
|
v
|
||||||
|
SDK Runtime RUN_AGENT -> plugin AgentRunner.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
关键约束:
|
||||||
|
|
||||||
|
- `ChatMessageHandler` 不解析 `plugin:*`,不实例化 wrapper,不知道 runner 组件细节。
|
||||||
|
- `PipelineService.get_pipeline_metadata()` 不直接访问插件 runtime,而是读取 registry。
|
||||||
|
- 旧 `RequestRunner` 只作为迁移参考,不作为最终运行路径。
|
||||||
|
- `AgentRunOrchestrator` 是 LangBot 侧运行编排层:负责 runner 绑定解析、资源授权、context envelope provisioning、run scope 注册、插件调用和结果归一化;不负责决定 Agent 的最终 prompt/window/压缩策略。
|
||||||
|
- 插件是无状态执行单元:多个 Pipeline 可以绑定同一个 runner id,并分别保存自己的 `ai.runner_config[id]`;运行时 LangBot 只把当前绑定配置放入 `ctx.config` 转发给同一个插件 runner。
|
||||||
|
- 禁止按 Pipeline 或 runner config 创建多个插件实例。需要跨请求持久化的状态必须走明确授权的 plugin storage / workspace storage / 外部服务,不能隐式保存在 per-pipeline 插件对象里。
|
||||||
|
- EBA 只做字段预留,不在本轮实现 EventBus、EventRouter、平台动作执行。
|
||||||
|
|
||||||
|
## 3. 新增 LangBot 模块
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/langbot/pkg/agent/
|
||||||
|
__init__.py
|
||||||
|
runner/
|
||||||
|
__init__.py
|
||||||
|
descriptor.py
|
||||||
|
errors.py
|
||||||
|
id.py
|
||||||
|
registry.py
|
||||||
|
context_builder.py
|
||||||
|
resource_builder.py
|
||||||
|
orchestrator.py
|
||||||
|
result_normalizer.py
|
||||||
|
config_migration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 descriptor.py
|
||||||
|
|
||||||
|
定义 LangBot 内部使用的 descriptor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerDescriptor(BaseModel):
|
||||||
|
id: str
|
||||||
|
source: Literal["plugin"]
|
||||||
|
label: dict[str, str]
|
||||||
|
description: dict[str, str] | None = None
|
||||||
|
plugin_author: str
|
||||||
|
plugin_name: str
|
||||||
|
runner_name: str
|
||||||
|
plugin_version: str | None = None
|
||||||
|
protocol_version: str = "1"
|
||||||
|
config_schema: list[dict[str, Any]] = []
|
||||||
|
capabilities: dict[str, bool] = {}
|
||||||
|
permissions: dict[str, list[str]] = {}
|
||||||
|
raw_manifest: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
`source == "builtin"` 不作为最终目标。如果实现阶段需要临时 adapter,必须标记为测试过渡代码,并在官方插件跑通后删除。
|
||||||
|
|
||||||
|
### 3.2 id.py
|
||||||
|
|
||||||
|
统一 runner id 解析和生成:
|
||||||
|
|
||||||
|
- 插件 runner id:`plugin:{author}/{plugin_name}/{runner_name}`
|
||||||
|
- `parse_runner_id(id)` 返回结构化对象
|
||||||
|
- 禁止业务代码手写字符串 split
|
||||||
|
- PoC 已存在的 `plugin:author/name/runner` 继续作为合法 id
|
||||||
|
|
||||||
|
### 3.3 registry.py
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 调用 `ap.plugin_connector.list_agent_runners(bound_plugins=None)` 拉取插件 runner
|
||||||
|
- 校验 manifest:
|
||||||
|
- `kind == AgentRunner`
|
||||||
|
- `metadata.name` 存在
|
||||||
|
- `metadata.label` 存在
|
||||||
|
- `spec.protocol_version` 兼容,默认 `1`
|
||||||
|
- `spec.config` 是 list,默认空
|
||||||
|
- `spec.capabilities` 是 dict,默认空
|
||||||
|
- `spec.permissions` 是 dict,默认空
|
||||||
|
- 输出 `AgentRunnerDescriptor`
|
||||||
|
- 缓存 discovery 结果,提供 `refresh()`
|
||||||
|
- 单个插件 manifest 失败只记录 warning,不影响其它 runner
|
||||||
|
|
||||||
|
刷新触发点:
|
||||||
|
|
||||||
|
- 插件安装、卸载、升级、重启后
|
||||||
|
- Pipeline metadata 请求时发现缓存为空
|
||||||
|
- 可选 TTL,优先保证正确性
|
||||||
|
|
||||||
|
### 3.4 context_builder.py / pipeline_adapter.py
|
||||||
|
|
||||||
|
`context_builder.py` 只负责从 `AgentEventEnvelope + AgentBinding` 构造 SDK v1 `AgentRunContext`。Pipeline Query 的读取、参数过滤和 prompt 提取属于 `PipelineAdapter`,但 PipelineAdapter 不再做历史窗口裁剪或 bootstrap 打包。
|
||||||
|
|
||||||
|
当前消息 Pipeline 进入 agent runner 的路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Query
|
||||||
|
-> PipelineAdapter.query_to_event(query)
|
||||||
|
-> PipelineAdapter.pipeline_config_to_binding(query, runner_id)
|
||||||
|
-> PipelineAdapter.build_adapter_context(query, binding)
|
||||||
|
-> AgentRunOrchestrator.run(event, binding, adapter_context=...)
|
||||||
|
-> AgentRunContextBuilder.build_context_from_event(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Protocol v1 context 的稳定字段:
|
||||||
|
|
||||||
|
- `run_id`: 新 UUID,不使用 query id 作为全局 run id
|
||||||
|
- `trigger.type`: 事件触发类型,例如 `message.received`
|
||||||
|
- `conversation`: conversation/thread/launcher/sender/bot/pipeline 投影
|
||||||
|
- `event`: 稳定事件上下文
|
||||||
|
- `actor`: 触发者
|
||||||
|
- `subject`: 当前消息、群、频道或其它事件主体
|
||||||
|
- `input`: 当前事件输入,不是历史消息窗口
|
||||||
|
- `delivery`: 输出 surface 和平台投递能力
|
||||||
|
- `resources`: 由 `resource_builder` 基于 binding policy 注入
|
||||||
|
- `state`: `PersistentStateStore` 读取的 host-managed scoped state snapshot
|
||||||
|
- `runtime`: host/version/workspace/bot/query/trace/deadline
|
||||||
|
- `config`: 当前 binding 对该 runner id 的配置,即 `runner_config`
|
||||||
|
- `bootstrap`: 可选扩展字段;LangBot Host 默认不填历史窗口
|
||||||
|
- `adapter`: Pipeline 或其它入口 adapter 的元数据
|
||||||
|
|
||||||
|
Pipeline adapter 的 `prompt` 和公开业务变量不进入顶层协议字段:
|
||||||
|
|
||||||
|
- filtered params -> `ctx.adapter.extra["params"]`
|
||||||
|
- legacy/effective prompt 可以暂存到 `ctx.adapter.extra["prompt"]`,但 official
|
||||||
|
runner 不应把它当作行为契约
|
||||||
|
- LangBot Host 不生成 `bootstrap.messages`、`adapter_messages` 或 context packaging 元数据
|
||||||
|
|
||||||
|
现阶段不要把新的压缩或 token-budget 裁剪塞回 Pipeline stage。Pipeline 只负责入口适配;完整历史和长期上下文由 EventLog / Transcript / pull APIs / future ContextCompressor 支撑。
|
||||||
|
|
||||||
|
### 3.4.1 Agentic context plan
|
||||||
|
|
||||||
|
EventLog / Transcript / Host pull APIs 已落地,`ContextCompressor` 仍是设计预留。
|
||||||
|
目标是让 Pipeline 逐步退化为入口 adapter,让 AgentRunner 层拥有上下文打包职责。
|
||||||
|
|
||||||
|
建议 Host 保持三类事实源和受限 API:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ConversationStore / EventLog
|
||||||
|
-> durable append-only raw messages, events, tool results, artifact refs
|
||||||
|
ConversationProjection
|
||||||
|
-> converts events into agent-readable conversation history
|
||||||
|
ContextCompressor
|
||||||
|
-> future optional service for summaries/checkpoints, requested and consumed by runners
|
||||||
|
```
|
||||||
|
|
||||||
|
关键原则:
|
||||||
|
|
||||||
|
- 完整历史属于 LangBot host,不属于插件实例。插件仍是 singleton/stateless。
|
||||||
|
- `ctx.bootstrap.messages` 不是 Host 默认下发的 working context。
|
||||||
|
- 每轮不能全量复制/序列化完整历史给插件 runtime;否则长会话会产生 O(n) 成本和跨进程 payload 膨胀。
|
||||||
|
- `max-round` 或类似窗口规则不属于 LangBot Host / Pipeline 语义。
|
||||||
|
- LiteLLM 接入后,模型窗口元信息应作为 resource/runtime metadata 暴露给 runner,由 runner 决定预算和压缩策略。
|
||||||
|
- `ContextCompressor` 生成的是派生 summary/checkpoint,不能覆盖或删除 raw history。
|
||||||
|
- 重启恢复依赖持久化 store 和 summary checkpoint,不依赖 `SessionManager` 里的进程内 conversation list。
|
||||||
|
|
||||||
|
未来需要的受限 API:
|
||||||
|
|
||||||
|
```python
|
||||||
|
api.get_conversation_messages(cursor: str | None, limit: int) -> HistoryPage
|
||||||
|
api.get_context_summary(scope: str = "conversation") -> ContextSummary | None
|
||||||
|
api.request_context_compaction(policy: dict) -> CompactionResult
|
||||||
|
```
|
||||||
|
|
||||||
|
这些 API 必须绑定 `run_id`、runner id、actor/subject scope 和资源权限;Host 需要限制
|
||||||
|
page size、总字节数、deadline 和可访问 conversation。
|
||||||
|
|
||||||
|
### 3.4.2 Large artifacts and tool collaboration
|
||||||
|
|
||||||
|
大文件、多模态输入和工具产物不要内联进 prompt、bootstrap 或 tool result。后续统一用
|
||||||
|
artifact/resource ref 协作:
|
||||||
|
|
||||||
|
- message/content 里只放小文本和必要摘要。
|
||||||
|
- 大文件、图片、音频、长工具输出返回 `artifact_id`、`mime_type`、`size`、`digest`、
|
||||||
|
`summary`、`expires_at`、`permissions`。
|
||||||
|
- `/tmp` 只能作为单次 run 的临时 staging,用于插件或工具短时间读写;它不是 durable store,
|
||||||
|
也不能作为重启恢复依据。
|
||||||
|
- box/object storage 是长期 artifact 的目标位置。当前分支尚未合并 box 能力,因此本轮只写文档预留,不实现 API。
|
||||||
|
- 工具之间传递大结果时应传 artifact ref,不传完整 blob。Agent 需要读取时走受限 proxy。
|
||||||
|
|
||||||
|
未来建议 API:
|
||||||
|
|
||||||
|
```python
|
||||||
|
api.get_artifact_metadata(artifact_id: str) -> ArtifactMetadata
|
||||||
|
api.open_artifact_stream(artifact_id: str) -> AsyncIterator[bytes]
|
||||||
|
api.read_artifact_range(artifact_id: str, offset: int, length: int) -> bytes
|
||||||
|
api.create_temp_artifact(name: str, content_type: str, ttl_seconds: int) -> ArtifactWriter
|
||||||
|
```
|
||||||
|
|
||||||
|
安全约束:
|
||||||
|
|
||||||
|
- Host 校验 artifact 是否属于当前 run、conversation、actor/subject scope 或授权资源。
|
||||||
|
- 默认不允许插件直接读任意本地路径,包括 `/tmp` 任意路径。
|
||||||
|
- 临时文件应有 TTL 和清理机制;box artifact 应有 retention policy。
|
||||||
|
- 多模态文件进入模型前,由 runner/context packager 决定传引用、摘要、缩略图还是实际 bytes。
|
||||||
|
|
||||||
|
### 3.5 resource_builder.py
|
||||||
|
|
||||||
|
执行前做三层裁剪:
|
||||||
|
|
||||||
|
1. runner manifest 声明的 `spec.permissions`
|
||||||
|
2. Pipeline 的 `extensions_preferences`
|
||||||
|
3. 当前 Pipeline runner 绑定配置中选择的资源范围
|
||||||
|
|
||||||
|
输出写入 `ctx.resources`,至少覆盖:
|
||||||
|
|
||||||
|
- models:可调用模型 UUID、类型、能力摘要。包括 LLM、fallback LLM、rerank 等 runner config schema 中选择的模型类资源。
|
||||||
|
- tools:可见工具 manifest,使用当前 bound plugins / MCP server 范围
|
||||||
|
- knowledge_bases:可检索知识库列表
|
||||||
|
- storage:plugin storage / workspace storage 权限摘要
|
||||||
|
- files:允许读取的配置文件、知识文件摘要
|
||||||
|
- platform_capabilities:本阶段只声明,不执行平台动作
|
||||||
|
|
||||||
|
注意:旧的 unrestricted proxy action 必须二次校验,不能只靠 context 声明。AgentRunner 可用资源应来自 `ctx.resources`,不是插件 runtime 的全局能力。
|
||||||
|
|
||||||
|
本阶段不接入 sandbox/skills,也不预留 runner 可见字段。后续相关分支合并后,
|
||||||
|
执行、文件、skill、MCP 等能力应先由 Host 侧封装成普通 tool,再通过
|
||||||
|
`ctx.resources.tools` 进入 runner;runner 不应识别或硬编码执行环境 provider。
|
||||||
|
|
||||||
|
资源裁剪要尽量通用,不应只写死 local-agent:
|
||||||
|
|
||||||
|
- `model-fallback-selector` 授权 primary/fallback LLM。
|
||||||
|
- `llm-model-selector` 授权 LLM。
|
||||||
|
- `rerank-model-selector` 授权 rerank 模型。
|
||||||
|
- `knowledge-base-multi-selector` 授权知识库。
|
||||||
|
- 后续新增 selector 时应在 resource builder 中统一扩展。
|
||||||
|
|
||||||
|
### 3.5.1 future EventRouter 预留
|
||||||
|
|
||||||
|
当前分支不实现 EBA EventRouter,但 AgentRunner 协议必须从现在开始兼容非消息事件。未来不要为消息撤回、群成员加入、好友申请各写一套 runner wrapper;统一入口应是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EventRouter -> AgentRunOrchestrator.run_from_event(event_request)
|
||||||
|
```
|
||||||
|
|
||||||
|
EBA 落地后,`ConversationStore` 不应只保存聊天消息,而应从 `EventLog` 投影生成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform Adapter
|
||||||
|
-> EventLog append raw event
|
||||||
|
-> ConversationProjection update message/history view when applicable
|
||||||
|
-> EventRouter resolve binding
|
||||||
|
-> AgentRunOrchestrator.run_from_event(event_request)
|
||||||
|
-> Context packager builds working context from projection + state + artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
这样消息事件、工具事件、群成员事件、好友申请事件可以共用同一套 run/session/state/resource
|
||||||
|
边界;非消息事件也不需要伪造成一条用户文本消息。
|
||||||
|
|
||||||
|
`event_request` 至少需要包含:
|
||||||
|
|
||||||
|
- `event_type`: 稳定协议名,例如 `message.recalled`、`group.member_joined`、`friend.request_received`
|
||||||
|
- `event_id` / `event_timestamp`
|
||||||
|
- `event_data`: 平台原始 payload 摘要和 source event type
|
||||||
|
- `actor`: 触发者,例如撤回操作者、新成员、好友申请人
|
||||||
|
- `subject`: 事件作用对象,例如被撤回消息、群/成员关系、好友申请
|
||||||
|
- `conversation`: 可选。群事件有 launcher 语义,好友申请可能还没有 conversation
|
||||||
|
- `input`: 可选结构化输入。非消息事件允许 `text=None`、`contents=[]`
|
||||||
|
- `binding`: 事件绑定解析出的 runner id、runner config、资源范围
|
||||||
|
|
||||||
|
先保留的稳定事件名:
|
||||||
|
|
||||||
|
- `message.received`
|
||||||
|
- `message.recalled`
|
||||||
|
- `group.member_joined`
|
||||||
|
- `friend.request_received`
|
||||||
|
|
||||||
|
这些事件名应作为插件协议的一部分保持稳定。平台原始事件名只能进入 `event_data`,不能成为 `ctx.event.event_type` 的公共契约。
|
||||||
|
|
||||||
|
### 3.6 result_normalizer.py
|
||||||
|
|
||||||
|
只接受 SDK v1 result:
|
||||||
|
|
||||||
|
- `message.delta`
|
||||||
|
- `message.completed`
|
||||||
|
- `tool.call.started`
|
||||||
|
- `tool.call.completed`
|
||||||
|
- `state.updated`
|
||||||
|
- `run.completed`
|
||||||
|
- `run.failed`
|
||||||
|
- `action.requested` 允许实验性返回,但本阶段只记录 telemetry,不执行
|
||||||
|
|
||||||
|
映射:
|
||||||
|
|
||||||
|
- `message.delta.data.chunk` -> `provider_message.MessageChunk`
|
||||||
|
- `message.completed.data.message` -> `provider_message.Message`
|
||||||
|
- `run.completed.data.message` -> `provider_message.Message`
|
||||||
|
- `run.failed` -> 抛出受控异常,让 `ChatMessageHandler` 使用现有错误策略
|
||||||
|
- 工具和状态事件默认不 yield 到 Pipeline,只记录 debug/telemetry
|
||||||
|
|
||||||
|
防护:
|
||||||
|
|
||||||
|
- 未知 type warning 后忽略
|
||||||
|
- 单 result 序列化大小限制
|
||||||
|
- provider message schema 校验失败转 `run.failed`
|
||||||
|
- 插件没有输出任何消息时,按 runner failed 处理
|
||||||
|
|
||||||
|
### 3.7 orchestrator.py
|
||||||
|
|
||||||
|
核心入口:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def run_from_query(query: pipeline_query.Query) -> AsyncGenerator[Message | MessageChunk, None]:
|
||||||
|
runner_id = resolve_runner_id(query.pipeline_config)
|
||||||
|
descriptor = await registry.get(runner_id, bound_plugins=query.variables.get("_pipeline_bound_plugins"))
|
||||||
|
ctx = await context_builder.from_query(query, descriptor)
|
||||||
|
async for raw in plugin_connector.run_agent(...):
|
||||||
|
async for message in result_normalizer.normalize(raw):
|
||||||
|
yield message
|
||||||
|
```
|
||||||
|
|
||||||
|
必须覆盖:
|
||||||
|
|
||||||
|
- runner id 不存在
|
||||||
|
- 插件系统关闭
|
||||||
|
- runner 不在 bound plugins 范围内
|
||||||
|
- 插件 runtime 断连
|
||||||
|
- runner 协议版本不兼容
|
||||||
|
- run 超时
|
||||||
|
- task cancellation
|
||||||
|
|
||||||
|
## 4. 配置模型直接切换
|
||||||
|
|
||||||
|
配置模型表达的是 Pipeline 到 runner id 的绑定,不表达插件实例。插件安装后由 plugin runtime 管理单个插件运行实例;不同 Pipeline 选择同一个 runner id 时,只是保存不同的 `runner_config[id]`,调用时随 `AgentRunContext.config` 传入。
|
||||||
|
|
||||||
|
目标格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ai": {
|
||||||
|
"runner": {
|
||||||
|
"id": "plugin:langbot/local-agent/default",
|
||||||
|
"expire-time": 0
|
||||||
|
},
|
||||||
|
"runner_config": {
|
||||||
|
"plugin:langbot/local-agent/default": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
兼容读取:
|
||||||
|
|
||||||
|
- 优先读 `ai.runner.id`
|
||||||
|
- 没有 `id` 时读旧 `ai.runner.runner`
|
||||||
|
- 旧内置 runner 名通过迁移表映射:
|
||||||
|
- `local-agent` -> `plugin:langbot/local-agent/default`
|
||||||
|
- `dify-service-api` -> `plugin:langbot/dify-agent/default`
|
||||||
|
- `n8n-service-api` -> `plugin:langbot/n8n-agent/default`
|
||||||
|
- `coze-api` -> `plugin:langbot/coze-agent/default`
|
||||||
|
- `dashscope-app-api` -> `plugin:langbot/dashscope-agent/default`
|
||||||
|
- `langflow-api` -> `plugin:langbot/langflow-agent/default`
|
||||||
|
- `tbox-app-api` -> `plugin:langbot/tbox-agent/default`
|
||||||
|
|
||||||
|
写入策略:
|
||||||
|
|
||||||
|
- 新 UI 只写 `ai.runner.id` 和 `ai.runner_config`
|
||||||
|
- 后端 update 接口接受旧字段,但保存时归一成新格式
|
||||||
|
- migration 最后统一落库
|
||||||
|
|
||||||
|
## 5. 需要修改的 LangBot 范围
|
||||||
|
|
||||||
|
必须修改:
|
||||||
|
|
||||||
|
- `src/langbot/pkg/core/app.py`
|
||||||
|
- 增加 `agent_runner_registry` / `agent_run_orchestrator` 属性
|
||||||
|
- `src/langbot/pkg/core/stages/build_app.py`
|
||||||
|
- 初始化 Agent 子系统
|
||||||
|
- `src/langbot/pkg/pipeline/process/handlers/chat.py`
|
||||||
|
- 删除 `PluginAgentRunnerWrapper`
|
||||||
|
- 删除内置 runner 查找逻辑
|
||||||
|
- 调用 orchestrator
|
||||||
|
- `src/langbot/pkg/api/http/service/pipeline.py`
|
||||||
|
- metadata 从 registry 生成
|
||||||
|
- `src/langbot/pkg/plugin/connector.py`
|
||||||
|
- `list_agent_runners()` / `run_agent()` 增加协议校验和 bound plugin 参数
|
||||||
|
- `src/langbot/pkg/plugin/handler.py`
|
||||||
|
- proxy action 二次权限校验
|
||||||
|
- `src/langbot/pkg/pipeline/preproc/preproc.py`
|
||||||
|
- 不再只为 `local-agent` 构造工具、知识库、模型
|
||||||
|
- 对所有 agent runner 保留 multimodal input
|
||||||
|
- `src/langbot/pkg/pipeline/pipelinemgr.py`
|
||||||
|
- runner name 监控改读 `runner.id`
|
||||||
|
- `src/langbot/templates/metadata/pipeline/ai.yaml`
|
||||||
|
- runner 字段从 `runner` 迁到 `id`
|
||||||
|
- `src/langbot/templates/default-pipeline-config.json`
|
||||||
|
- 默认 runner 改为官方 local-agent 插件 id
|
||||||
|
- `web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx`
|
||||||
|
- 当前 runner 改读 `ai.runner.id`
|
||||||
|
- runner 配置区改写入 `ai.runner_config[id]`
|
||||||
|
|
||||||
|
最终删除或停用:
|
||||||
|
|
||||||
|
- `src/langbot/pkg/provider/runner.py` 的业务注册路径
|
||||||
|
- `src/langbot/pkg/provider/runners/*` 的运行入口
|
||||||
|
|
||||||
|
可以暂时保留文件作为官方插件迁移参考,但不应被运行时引用。
|
||||||
|
|
||||||
|
## 6. 收尾实现顺序
|
||||||
|
|
||||||
|
### Step 1:补齐宿主上下文
|
||||||
|
|
||||||
|
- SDK `AgentRunContext` 保持 event-first:`event/input/delivery/resources/context/state/runtime/config/bootstrap/adapter`。
|
||||||
|
- LangBot context builder 只从 `AgentEventEnvelope + AgentBinding` 写入稳定协议字段。
|
||||||
|
- Pipeline adapter 可以把公开业务变量写入 `ctx.adapter.extra["params"]`;legacy/effective prompt 若保留在 `ctx.adapter.extra["prompt"]`,也只属于 adapter metadata。
|
||||||
|
- 保持 `ctx.config` 只表达静态绑定配置。
|
||||||
|
|
||||||
|
### Step 2:增强宿主 AgentRun proxy action
|
||||||
|
|
||||||
|
- `invoke_llm` / `invoke_llm_stream` 通过 `run_id/query_id` 找回当前 Query。
|
||||||
|
- 自动合并 model persisted `extra_args` 与 action-level override。
|
||||||
|
- 自动应用 pipeline `remove-think`,并允许 action 显式 override。
|
||||||
|
- `call_tool` 传回当前 Query,恢复旧工具调用上下文。
|
||||||
|
- `retrieve_knowledge` 保持 `bot_uuid`、`sender_id`、`session_name` 等 settings。
|
||||||
|
- `invoke_rerank` 使用 run-scoped model authorization。
|
||||||
|
|
||||||
|
### Step 3:泛化资源构建
|
||||||
|
|
||||||
|
- 按 manifest permissions + bound plugins/MCP + runner config schema 构造资源。
|
||||||
|
- 支持 primary/fallback LLM、rerank model、KB selector。
|
||||||
|
- 不把 local-agent 特例扩散到通用资源层。
|
||||||
|
|
||||||
|
### Step 4:local-agent parity
|
||||||
|
|
||||||
|
- 使用静态绑定配置 `ctx.config["prompt"]`,不读取 `ctx.adapter.extra["prompt"]`。
|
||||||
|
- 通过 Host history API 拉取 transcript,不读取 `ctx.bootstrap.messages` 或 adapter window 字段。
|
||||||
|
- 当前 user message 从 `ctx.input.contents` 构造,保留多模态内容。
|
||||||
|
- RAG 只替换/插入文本部分,不丢图片/文件。
|
||||||
|
- streaming/non-streaming 默认跟随 `runtime.metadata.streaming_supported`。
|
||||||
|
- 首轮 fallback 成功后,tool loop 固定使用 committed model。
|
||||||
|
- tool loop 继续传可用 tools,支持多步工具调用。
|
||||||
|
- rerank 通过授权模型资源调用。
|
||||||
|
|
||||||
|
### Step 5:端到端保护和测试
|
||||||
|
|
||||||
|
- 插件无输出时按 runner failed 处理。
|
||||||
|
- timeout/deadline 覆盖 plugin runtime、模型调用和外部 runner 调用。
|
||||||
|
- runner 协议错误转受控错误。
|
||||||
|
- 覆盖 local-agent 用户可见行为:普通回复、流式、工具、多步工具、KB、rerank、多模态、绑定 prompt、history API。
|
||||||
|
|
||||||
|
### Step 6:官方 runner 迁移
|
||||||
|
|
||||||
|
- 官方插件 ready 后移除内置 runner registry
|
||||||
|
- 删除或隔离 provider runners 的运行引用
|
||||||
|
- 测试旧 runner 名只能通过 migration 映射到插件 id
|
||||||
|
|
||||||
|
### Step 7:历史配置迁移
|
||||||
|
|
||||||
|
- 写 persistence migration
|
||||||
|
- 更新 default pipeline config
|
||||||
|
- 对已存在 Pipeline 执行旧字段到新字段迁移
|
||||||
|
- 对监控/日志里的 runner 字段改用新 id
|
||||||
|
|
||||||
|
## 7. 测试要求
|
||||||
|
|
||||||
|
单测:
|
||||||
|
|
||||||
|
- runner id parse / format
|
||||||
|
- registry manifest 校验、失败隔离、bound plugins 过滤
|
||||||
|
- context builder 从 query 生成完整 v1 context
|
||||||
|
- resource builder 三层裁剪
|
||||||
|
- result normalizer 对每种 result type 的映射
|
||||||
|
- 旧配置 resolve 和 migration
|
||||||
|
|
||||||
|
集成测试:
|
||||||
|
|
||||||
|
- fake AgentRunner 插件可被 Pipeline 选择
|
||||||
|
- streaming 输出仍能更新 message card
|
||||||
|
- 插件异常返回用户可理解错误,不中断 runtime
|
||||||
|
- runner 不在 bound plugins 时不可执行
|
||||||
|
- 未授权工具 / 知识库 / 模型 proxy 调用被拒绝
|
||||||
|
- 旧 `local-agent` Pipeline 配置迁到官方插件 id
|
||||||
|
|
||||||
|
## 8. 验收标准
|
||||||
|
|
||||||
|
- LangBot Pipeline 可以选择插件 AgentRunner 并完成非流式和流式回复。
|
||||||
|
- `ChatMessageHandler` 不包含插件 runner 解析和 wrapper。
|
||||||
|
- `PipelineService` 不直接拼插件 runner metadata。
|
||||||
|
- 所有 runner 配置使用 `ai.runner.id` + `ai.runner_config`。
|
||||||
|
- 插件 runtime 不为每个 Pipeline 或 runner 配置创建插件实例;`runner_config` 只作为绑定配置随 `ctx.config` 传入。
|
||||||
|
- 主聊天路径不再通过旧内置 runner 执行业务 runner。迁移期间旧文件可以保留。
|
||||||
|
- 插件只能访问 `ctx.resources` 授权的模型、工具、知识库和文件。
|
||||||
|
- 宿主 action 能为 AgentRunner 调用恢复必要 Query 语义,插件不需要拿裸 Query。
|
||||||
|
- 官方 `local-agent` 插件对外行为与旧内置 local-agent 对齐。
|
||||||
|
- EBA 相关字段只作为 context/result 预留,不执行平台动作。
|
||||||
329
docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md
Normal file
329
docs/agent-runner-pluginization/OFFICIAL_RUNNER_PLUGINS.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# 官方 AgentRunner 插件迁移计划
|
||||||
|
|
||||||
|
本文档描述内置 `RequestRunner` 迁出 LangBot 后,官方 runner 插件如何组织、迁移和验收。
|
||||||
|
它是 [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md) 和
|
||||||
|
[AGENT_CONTEXT_PROTOCOL.md](./AGENT_CONTEXT_PROTOCOL.md) 的下游落地计划,不是 LangBot
|
||||||
|
宿主协议的设计前提。
|
||||||
|
|
||||||
|
官方 `local-agent` 可以外移,也可以重写。设计重点不是保留旧内置 runner 的内部结构,
|
||||||
|
而是验证一个依附 LangBot host 基础设施的官方 agent 能否完整工作。同时,LangBot 的
|
||||||
|
host 协议必须服务 Claude Code SDK、Codex、Pi Agent SDK、外部 Agent 平台等自管
|
||||||
|
context/runtime 的 runner,不能被官方插件的实现细节绑死。
|
||||||
|
|
||||||
|
当前实现已经进入过渡阶段:
|
||||||
|
|
||||||
|
- LangBot 主聊天路径通过 `AgentRunOrchestrator` 调用插件化 `AgentRunner`。
|
||||||
|
- 旧 `src/langbot/pkg/provider/runners/*` 仍保留,作为迁移参考和回退分析材料;在官方插件迁移完成前不要求删除。
|
||||||
|
- 官方 runner 当前以独立插件目录/仓库推进,例如 `langbot-local-agent/` 和 `langbot-agent-runner/*-agent/`。不再要求先落地单一 monorepo。
|
||||||
|
- `claude-code-agent` 与 `codex-agent` 已作为外部 harness runner MVP 接入,用来验证 Claude Code / Codex / Kimi Code 这类自管 runtime 的边界。
|
||||||
|
|
||||||
|
## 1. 为什么新仓库
|
||||||
|
|
||||||
|
官方 runner 插件会和 LangBot 主仓库、SDK 仓库以不同节奏迭代:
|
||||||
|
|
||||||
|
- LangBot 主仓库只维护宿主协议和调度。
|
||||||
|
- SDK 仓库维护 AgentRunner 组件和 runtime 协议。
|
||||||
|
- 官方 runner 插件承载业务 runner 的具体实现和第三方平台适配。
|
||||||
|
|
||||||
|
不要把官方 runner 插件重新绑死在 LangBot 主仓库内。允许开发期使用本地路径插件,但运行边界必须保持为:
|
||||||
|
|
||||||
|
- LangBot 提供通用宿主能力:当前事件、context handles、资源授权、状态/存储、历史、artifact、模型/工具/知识库调用代理、结果归一。
|
||||||
|
- 插件消费这些公开能力,实现具体 runner 行为。
|
||||||
|
- LangBot 默认不把全量历史消息 inline 给 runner;runner 按需通过授权 API 拉取历史和 artifact。
|
||||||
|
- 旧内置 runner 只作为行为对齐的基准,不作为长期运行路径。
|
||||||
|
|
||||||
|
## 2. 仓库结构
|
||||||
|
|
||||||
|
当前推荐策略是“官方插件可独立发布,必要时共享 SDK helper”。开发期可以采用本地多目录布局:
|
||||||
|
|
||||||
|
```text
|
||||||
|
langbot-app/
|
||||||
|
langbot-local-agent/
|
||||||
|
manifest.yaml
|
||||||
|
components/agent_runner/default.yaml
|
||||||
|
components/agent_runner/default.py
|
||||||
|
pkg/
|
||||||
|
tests/
|
||||||
|
langbot-agent-runner/
|
||||||
|
claude-code-agent/
|
||||||
|
codex-agent/
|
||||||
|
n8n-agent/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
后续可以把多个官方 runner 聚合进 monorepo,也可以继续独立发布。这个选择不影响协议设计;协议边界由 SDK 和 LangBot 宿主保证。
|
||||||
|
|
||||||
|
如果多个 runner 出现重复逻辑,优先沉淀到 SDK 或一个明确的共享 helper 包,不要把宿主私有结构泄漏给插件。
|
||||||
|
|
||||||
|
## 3. 插件命名和 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`。
|
||||||
|
|
||||||
|
## 4. 迁移优先级
|
||||||
|
|
||||||
|
### Batch 1:打通协议
|
||||||
|
|
||||||
|
1. `local-agent`
|
||||||
|
2. `claude-code-agent`
|
||||||
|
3. `codex-agent`
|
||||||
|
4. `dify-agent`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- `local-agent` 覆盖模型、工具、知识库、流式、会话历史,是能力最完整的基准。
|
||||||
|
- `claude-code-agent` / `codex-agent` 代表 Claude Code / Codex / Kimi Code 这类本地或外部 code-agent harness:它们通常自带 session、tool loop、上下文压缩和权限模型,LangBot 主要提供 IM 事件、资源投影、审计和状态指针。
|
||||||
|
- `dify-agent` 代表外部 Agent 平台调用,配置和错误处理能验证传统 service API runner 的迁移方式。
|
||||||
|
|
||||||
|
### Batch 2:迁移外部 workflow runner
|
||||||
|
|
||||||
|
1. `n8n-agent`
|
||||||
|
2. `langflow-agent`
|
||||||
|
|
||||||
|
这批主要验证 webhook/workflow 输入输出、timeout、外部 conversation id。
|
||||||
|
|
||||||
|
### Batch 3:迁移平台 Agent API
|
||||||
|
|
||||||
|
1. `coze-agent`
|
||||||
|
2. `dashscope-agent`
|
||||||
|
3. `tbox-agent`
|
||||||
|
|
||||||
|
这批主要验证平台特有响应格式、引用资料、文件/图片输入。
|
||||||
|
|
||||||
|
## 5. 每个官方插件的组件要求
|
||||||
|
|
||||||
|
每个插件至少包含:
|
||||||
|
|
||||||
|
```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:
|
||||||
|
config: []
|
||||||
|
capabilities:
|
||||||
|
streaming: true
|
||||||
|
tool_calling: false
|
||||||
|
knowledge_retrieval: false
|
||||||
|
multimodal_input: false
|
||||||
|
event_context: true
|
||||||
|
platform_api: false
|
||||||
|
interrupt: false
|
||||||
|
stateful_session: true
|
||||||
|
permissions:
|
||||||
|
models: []
|
||||||
|
tools: []
|
||||||
|
knowledge_bases: []
|
||||||
|
storage: ["plugin"]
|
||||||
|
files: []
|
||||||
|
platform_api: []
|
||||||
|
execution:
|
||||||
|
python:
|
||||||
|
path: ./main.py
|
||||||
|
attr: DefaultAgentRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. local-agent 插件方向
|
||||||
|
|
||||||
|
`local-agent` 是官方插件中的重要消费者,但不是宿主协议的设计中心。它可以选择复用
|
||||||
|
旧实现,也可以完全重写。它需要证明:一个主要依附 LangBot host 能力的 agent runner
|
||||||
|
可以通过公开协议完成模型、工具、知识库、状态、history、artifact、上下文压缩和消息投递。
|
||||||
|
|
||||||
|
LangBot core 不应为了 local-agent 保留业务编排逻辑。local-agent 的 prompt 组装、history
|
||||||
|
拉取、summary/checkpoint、tool loop、RAG 编排、fallback、多模态处理都应在插件内完成。
|
||||||
|
|
||||||
|
迁移或重写时需要覆盖旧内置 runner 的用户可见能力:
|
||||||
|
|
||||||
|
- model primary/fallback 选择
|
||||||
|
- prompt
|
||||||
|
- knowledge-bases
|
||||||
|
- rerank-model
|
||||||
|
- rerank-top-k
|
||||||
|
- function calling
|
||||||
|
- streaming
|
||||||
|
- multimodal input
|
||||||
|
- conversation history
|
||||||
|
- monitoring metadata
|
||||||
|
|
||||||
|
与 LangBot 主仓库的责任边界:
|
||||||
|
|
||||||
|
- LangBot 构造当前事件、结构化输入、资源授权、context handles、state/storage 能力和 delivery 能力
|
||||||
|
- LangBot 不默认 inline 全量历史,不替插件组装最终模型上下文
|
||||||
|
- 插件负责选择模型、拼请求、调用 LLM、处理 tool call loop、输出 result stream
|
||||||
|
- 插件不能绕过 `ctx.resources` 调用未授权模型、工具或知识库
|
||||||
|
|
||||||
|
为了保持旧内置 runner 的用户可见行为,`local-agent` 插件应消费宿主处理后的有效输入和
|
||||||
|
受限 API,而不是读取宿主内部私有结构:
|
||||||
|
|
||||||
|
- `ctx.event` / `ctx.input`:当前结构化输入,必须保留图片、文件等多模态内容。
|
||||||
|
- `ctx.context`:history cursor、inline policy、可用 context API。
|
||||||
|
- `AgentRunAPIProxy.history`:按需读取 transcript,而不是依赖 host 每轮强塞历史窗口。
|
||||||
|
- `AgentRunAPIProxy.artifacts`:按需读取图片、文件、工具大结果。
|
||||||
|
- `AgentRunAPIProxy.state` / storage:保存 summary、外部 conversation id、用户偏好等可选状态。
|
||||||
|
- `ctx.resources`:已授权模型、工具、知识库、文件和 storage。
|
||||||
|
- `ctx.runtime.metadata.streaming_supported`:当前 adapter 是否能消费流式输出。
|
||||||
|
- 宿主代理 action:模型、工具、知识库、rerank 调用必须通过 `run_id` 校验资源权限。
|
||||||
|
|
||||||
|
`local-agent` 不应消费 Pipeline adapter 生成的历史窗口,也不应读取
|
||||||
|
`ctx.adapter.extra.prompt`。它应从绑定配置读取静态 `prompt`,并通过 Host
|
||||||
|
history API 拉取 transcript。Pipeline adapter 不保留 Host-side window 兼容逻辑。
|
||||||
|
|
||||||
|
建议 local-agent manifest 使用 hybrid 或 self-managed context:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
context:
|
||||||
|
ownership: hybrid
|
||||||
|
bootstrap: current_event
|
||||||
|
max_inline_events: 0
|
||||||
|
max_inline_bytes: 0
|
||||||
|
supports_history_pull: true
|
||||||
|
supports_history_search: true
|
||||||
|
supports_artifact_pull: true
|
||||||
|
owns_compaction: true
|
||||||
|
wants_static_context_refs: true
|
||||||
|
```
|
||||||
|
|
||||||
|
这表示:LangBot 只给当前事件和 context handles;local-agent 自己决定是否拉取历史、是否搜索、
|
||||||
|
何时摘要、如何构造最终 prompt。
|
||||||
|
|
||||||
|
### 6.1 Native Execution / Skills 后续接入
|
||||||
|
|
||||||
|
本阶段不把 sandbox/skills 做成 AgentRunner 协议字段,也不预留 runner 可见字段。
|
||||||
|
后续 sandbox/skills 分支合并后,命令执行、文件操作、skill、MCP managed process
|
||||||
|
等能力应先由 LangBot Host 封装成 scoped tools,再通过 `ctx.resources.tools`
|
||||||
|
暴露给 runner。
|
||||||
|
|
||||||
|
这让 local-agent 只消费授权后的 Host 基础设施,而不是直接持有宿主机执行能力。
|
||||||
|
Claude Code / Codex 这类外部 harness runner 仍可先保留自己的执行模型,但要在文档和
|
||||||
|
配置中明确它们是否使用 LangBot 提供的工具投影。
|
||||||
|
|
||||||
|
## 7. 外部 runner 插件要求
|
||||||
|
|
||||||
|
外部平台 runner 迁移时遵循:
|
||||||
|
|
||||||
|
- 旧配置字段尽量保持同名,便于 migration 复制
|
||||||
|
- 输出统一转换为 `AgentRunResult`
|
||||||
|
- 外部 API timeout 从 runner config 读取
|
||||||
|
- 平台 conversation id 存 plugin storage 或 context runtime state,不能依赖 LangBot 内置 conversation uuid 私有结构
|
||||||
|
- 流式支持按平台能力声明,没有流式就只发 `message.completed`
|
||||||
|
|
||||||
|
### 7.1 Code-agent harness runner 要求
|
||||||
|
|
||||||
|
Claude Code、Codex、Kimi Code 这类 runner 不一定通过 LangBot 的模型/工具 loop 执行。它们可以依赖自己的 harness,但仍必须遵守 LangBot 的宿主边界:
|
||||||
|
|
||||||
|
- 输入来自 `ctx.event` / `ctx.input`,不能直接依赖 Pipeline 私有 `Query`。
|
||||||
|
- LangBot 授权后的资源应被投影为 harness 可读的 context 文件、MCP 配置、skill 目录、环境变量或 CLI 参数。
|
||||||
|
- 外部 session id、workspace、checkpoint 等跨轮次指针应写入 Host state 或 plugin storage;插件实例本身保持无状态。
|
||||||
|
- CLI / subprocess runner 必须处理 timeout、取消、空输出、非零退出和 stderr 映射。
|
||||||
|
- 如果外部 harness 选择使用 LangBot 托管执行能力,它应通过 scoped MCP/tool
|
||||||
|
投影消费 Host 授权资源;否则它属于 external harness mode,不能声称具备
|
||||||
|
LangBot-managed 执行隔离。
|
||||||
|
- 外部 harness 的 permission mode、allowed/disallowed tools、MCP 配置只是一层执行约束;LangBot 仍负责调用前的资源授权、路径策略、secret 过滤和审计。发布级要求见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
|
||||||
|
### 7.2 SDK-owned LangBot MCP bridge
|
||||||
|
|
||||||
|
Claude Code / Codex 这类外部 harness 不能直接持有 Python 进程内的
|
||||||
|
`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 生成;runner 插件不各自手写 LangBot tool schema。
|
||||||
|
- stdio MCP proxy 只把外部 harness 的 MCP 调用转发回当前 run 的本地 bridge。
|
||||||
|
- run 结束后 bridge 关闭;这不是 LangBot 主程序全局 MCP server。
|
||||||
|
|
||||||
|
第一批工具保持很小:当前事件快照、history page、knowledge retrieve、authorized tool call。后续新增工具必须先进入 SDK-owned annotated surface,再由 MCP adapter 自动投影。
|
||||||
|
|
||||||
|
## 8. Claude Code runner 当前形态
|
||||||
|
|
||||||
|
当前 `claude-code-agent` 是最小可运行 MVP,用来证明外部 harness runner 可以接入同一套 AgentRunner 协议。
|
||||||
|
|
||||||
|
### 8.1 基本行为
|
||||||
|
|
||||||
|
- 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`
|
||||||
|
- 默认状态:如果 Claude Code 返回 `session_id`,runner 通过 `state.updated` 写回 `external.session_id`
|
||||||
|
- 工作目录:优先使用 binding config 的 `working-directory`,其次使用 Host state 中的 `external.working_directory`
|
||||||
|
|
||||||
|
### 8.2 Context / skill / MCP 投影
|
||||||
|
|
||||||
|
Claude Code runner 当前把 LangBot event-first context 投影给外部 harness:
|
||||||
|
|
||||||
|
- 写入 `agent-context.json`,schema 为 `langbot.agent_runner.external_harness_context.v1`
|
||||||
|
- 写入 `LANGBOT_CONTEXT.md`,作为人类可读摘要
|
||||||
|
- 将 prompt prefix 指向 context 文件路径
|
||||||
|
- 可把 binding 提供的 `skills-json` 写入 Claude Code 原生 `.claude/skills/<name>/SKILL.md`
|
||||||
|
- 可把 binding 提供的 `mcp-config-json` 写成每次 run 的 MCP config,并通过 `--mcp-config` / `--strict-mcp-config` 传给 Claude Code
|
||||||
|
- 可通过 `enable-langbot-mcp=true` 启用 SDK-owned per-run LangBot MCP bridge,使 Claude Code 通过 MCP 调用受限的 `AgentRunAPIProxy` 能力
|
||||||
|
|
||||||
|
这些投影目前由 runner adapter 完成;长期更理想的形态是 LangBot Host 负责生成 scoped resource projection,runner 只负责适配 Claude Code 的原生目录和 CLI 参数。
|
||||||
|
|
||||||
|
### 8.3 已验证能力
|
||||||
|
|
||||||
|
2026-05-29 本地验证:
|
||||||
|
|
||||||
|
- WebUI Debug Chat 能通过 Pipeline adapter 调用 `claude-code-agent`
|
||||||
|
- Claude Code 能读取 LangBot context 文件并按指令输出 sentinel
|
||||||
|
- Skill 文件可以投影到 `.claude/skills/`
|
||||||
|
- MCP config 可以通过 binding config 投影为 Claude Code CLI 参数
|
||||||
|
- SDK-owned per-run LangBot MCP bridge 可以被真实 Claude Code CLI 调用,并通过 `langbot_get_current_event` 读取当前 run_id
|
||||||
|
- `external.session_id` 与 `external.working_directory` 可以写入 host-owned state,用于后续 resume
|
||||||
|
- `codex-agent` 可通过 WebUI Debug Chat 调用本机 Codex CLI,读取 LangBot event context,并把 Codex `thread_id` 写入 host-owned state
|
||||||
|
- SDK-owned per-run LangBot MCP bridge 可以被真实 Codex CLI 调用,并通过 `langbot_get_current_event` 读取当前 run_id
|
||||||
|
- 对需要代理的本地运行环境,`codex-agent` 可通过 binding config 的 `environment-json` 显式传递非 secret 环境变量
|
||||||
|
|
||||||
|
下一轮测试入口见 [PHASE1_QA_ACCEPTANCE_MATRIX.md](./PHASE1_QA_ACCEPTANCE_MATRIX.md)。
|
||||||
|
|
||||||
|
### 8.4 当前限制
|
||||||
|
|
||||||
|
- 不是发布级安全边界实现。
|
||||||
|
- 默认只做本地 CLI 调用,不实现完整执行隔离或 workspace 生命周期。
|
||||||
|
- 不实现 issue-centric 队列、复杂 workflow engine 或长期任务调度。
|
||||||
|
- 不代表 Codex 发布级能力或 Kimi runner 已完成;当前只验证外部 harness runner 的协议形态。
|
||||||
|
|
||||||
|
## 9. 发布和安装策略
|
||||||
|
|
||||||
|
最终 LangBot 安装或升级时需要保证官方 runner 插件可用。可选方案:
|
||||||
|
|
||||||
|
1. 首次启动检测缺失官方 runner 插件并提示安装。
|
||||||
|
2. 打包发行版时预装官方 runner 插件。
|
||||||
|
3. 在 migration 前检查对应插件是否存在,不存在则自动安装或阻止迁移。
|
||||||
|
|
||||||
|
建议实现顺序:
|
||||||
|
|
||||||
|
- 开发阶段使用本地路径插件。
|
||||||
|
- 发布前支持 marketplace 安装。
|
||||||
|
- 历史配置 migration 只在官方插件可用时执行。
|
||||||
|
- 迁移期间保留旧内置 runner 文件,直到对应官方插件通过 parity 验收。
|
||||||
|
|
||||||
|
## 10. 验收标准
|
||||||
|
|
||||||
|
- 每个旧 runner 都有对应官方 AgentRunner 插件。
|
||||||
|
- 旧 runner 配置能无损复制到新 `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 能通过当前 Pipeline 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` 表示 binding 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 或自管上下文读取历史,不依赖 bootstrap 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. 如需要代理,使用 binding 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 已完成。
|
||||||
157
docs/agent-runner-pluginization/PROGRESS.md
Normal file
157
docs/agent-runner-pluginization/PROGRESS.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Agent Runner 插件化实现进度
|
||||||
|
|
||||||
|
本文档跟踪 Agent Runner 插件化的实现状态,便于快速了解当前进度。
|
||||||
|
|
||||||
|
## 总体进度
|
||||||
|
|
||||||
|
**当前阶段**: 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` |
|
||||||
|
| `PipelineAdapter` | ✅ | `pkg/agent/runner/pipeline_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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未完成但仍属本分支收尾
|
||||||
|
|
||||||
|
以下项目属于本分支收尾工作:
|
||||||
|
|
||||||
|
- [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) — 总体设计
|
||||||
|
- [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 后续门槛
|
||||||
|
- [IMPLEMENTATION_PLAN.md](./IMPLEMENTATION_PLAN.md) — 具体实施细节
|
||||||
702
docs/agent-runner-pluginization/PROTOCOL_V1.md
Normal file
702
docs/agent-runner-pluginization/PROTOCOL_V1.md
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
# LangBot AgentRunner Protocol v1
|
||||||
|
|
||||||
|
本文档定义 LangBot Host 与插件 SDK / Runtime / AgentRunner 之间的协议合同。它优先描述”稳定接口应是什么”,不描述具体落地任务。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
**Protocol v1 已在当前分支落地**:
|
||||||
|
|
||||||
|
- ✅ SDK 定义 `AgentRunnerManifest`、`AgentRunContext`、`AgentRunResult`、`AgentRunAPIProxy`
|
||||||
|
- ✅ Runtime 支持 `LIST_AGENT_RUNNERS` 和 `RUN_AGENT`
|
||||||
|
- ✅ Host 支持 `run_id` session authorization
|
||||||
|
- ✅ Host 能从当前 Pipeline 入口生成 event-first context
|
||||||
|
- ✅ `messages` 降级为 optional bootstrap
|
||||||
|
- ✅ `max-round` 不出现在协议实体中,也不属于 Host / Pipeline 语义;类似参数若存在,由 runner 自己解释 `ctx.config`
|
||||||
|
- ✅ Proxy 覆盖 model、tool、knowledge、state/storage
|
||||||
|
- ✅ History / Event / Artifact / State API 已落地
|
||||||
|
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
|
||||||
|
- ✅ `local-agent` 与 Claude Code runner 已通过本地 WebUI smoke,验证 host-infra runner 与外部 harness runner 共享同一协议路径
|
||||||
|
|
||||||
|
## 1. 协议目标
|
||||||
|
|
||||||
|
Protocol v1 要解决四件事:
|
||||||
|
|
||||||
|
- LangBot 如何发现插件提供的 AgentRunner。
|
||||||
|
- LangBot 如何把一次事件调用封装成 `AgentRunContext`。
|
||||||
|
- AgentRunner 如何以事件流形式返回运行结果。
|
||||||
|
- AgentRunner 如何通过受限 API 访问 LangBot host 能力。
|
||||||
|
|
||||||
|
Protocol v1 不定义:
|
||||||
|
|
||||||
|
- LangBot 内部如何持久化 AgentBinding。
|
||||||
|
- AgentRunner 内部如何组装 prompt、压缩历史、管理 memory。
|
||||||
|
- 官方 local-agent 的具体实现。
|
||||||
|
- Pipeline 的长期配置模型。
|
||||||
|
- 发布级安全 hardening 的完整实现;当前只定义 Host 侧资源、权限、状态和审计边界,release gate 见 [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。 |
|
||||||
|
|
||||||
|
`AgentBinding` 只影响 Host 构造出的 `ctx.config`、`ctx.resources`、`ctx.context` 和 `ctx.delivery`。SDK 不需要知道 binding 的持久化形态。
|
||||||
|
|
||||||
|
外部 harness runner(Claude Code、Codex、Kimi Code 等)仍然是 `AgentRunner`。Protocol v1 只要求它们消费 event-first `AgentRunContext`、返回 `AgentRunResult`,并通过 Host 授权的 state/storage/artifact APIs 保存跨轮次指针。它们内部可以继续使用自己的 session、tool loop、MCP、上下文压缩和权限模型。
|
||||||
|
|
||||||
|
## 3. Discovery 协议
|
||||||
|
|
||||||
|
### 3.1 LIST_AGENT_RUNNERS
|
||||||
|
|
||||||
|
Host 调用 Plugin Runtime 获取当前插件暴露的 runner 列表。该请求不需要额外 payload。
|
||||||
|
|
||||||
|
Runtime 返回:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ListAgentRunnersResponse(BaseModel):
|
||||||
|
runners: list[AgentRunnerManifest]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 AgentRunnerManifest
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerManifest(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
label: I18nObject
|
||||||
|
description: I18nObject | None = None
|
||||||
|
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` 只能放展示、诊断、非稳定扩展信息。
|
||||||
|
|
||||||
|
### 3.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 APIs。
|
||||||
|
- `knowledge_retrieval`: runner 可能调用 Host knowledge APIs。
|
||||||
|
- `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 历史。
|
||||||
|
|
||||||
|
### 3.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 裁剪。
|
||||||
|
|
||||||
|
### 3.5 Context Policy
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunnerContextPolicy(BaseModel):
|
||||||
|
ownership: Literal["self_managed", "host_bootstrap", "hybrid"] = "self_managed"
|
||||||
|
bootstrap: Literal["none", "current_event", "recent_tail", "summary_tail"] = "current_event"
|
||||||
|
max_inline_events: int = 0
|
||||||
|
max_inline_bytes: int = 0
|
||||||
|
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 APIs 拉取更多上下文。
|
||||||
|
- `max-round` 或类似窗口参数不属于 Protocol v1 字段,也不属于 Pipeline / Host 通用语义;如果某个 runner 需要,应由 runner 自己解释 `ctx.config`。
|
||||||
|
|
||||||
|
## 4. Run 协议
|
||||||
|
|
||||||
|
### 4.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` 为准。
|
||||||
|
|
||||||
|
### 4.2 AgentRunContext
|
||||||
|
|
||||||
|
```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] = {}
|
||||||
|
bootstrap: BootstrapContext | None = None
|
||||||
|
adapter: AdapterContext | None = None
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
核心约束:
|
||||||
|
|
||||||
|
- `event` 是必选字段,Protocol v1 是 event-first。
|
||||||
|
- `input` 表示当前事件的主输入,不等于历史消息。
|
||||||
|
- `bootstrap` 是可选字段;LangBot Host 默认不填历史窗口。
|
||||||
|
- `adapter` 只放 Pipeline adapter 字段,runner 不应依赖它做长期能力。
|
||||||
|
- `config` 是 Host binding config,不是插件实例状态。
|
||||||
|
|
||||||
|
### 4.3 AgentTrigger
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentTrigger(BaseModel):
|
||||||
|
type: str
|
||||||
|
source: Literal["platform", "webui", "api", "scheduler", "system", "pipeline_adapter"]
|
||||||
|
timestamp: int | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
`trigger.type` 应与 `event.event_type` 一致或更粗粒度。例如 Pipeline 兼容入口触发消息时:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message.received",
|
||||||
|
"source": "pipeline_adapter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.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`。
|
||||||
|
- 平台原始事件名放入 `source_event_type`。
|
||||||
|
- 大型原始 payload 必须放入 `raw_ref` 或 artifact,不应直接塞入 `data`。
|
||||||
|
|
||||||
|
### 4.5 Actor / Subject / Conversation
|
||||||
|
|
||||||
|
```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。
|
||||||
|
|
||||||
|
### 4.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` 是平台兼容字段,不应成为长期稳定依赖。
|
||||||
|
|
||||||
|
### 4.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`。
|
||||||
|
|
||||||
|
### 4.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
|
||||||
|
```
|
||||||
|
|
||||||
|
`ContextAccess` 告诉 runner:Host inline 了什么、没有 inline 什么、如果需要更多上下文应该通过哪些 API 拉取。
|
||||||
|
它不是 Host 的业务上下文编排策略,而是 runner 按需读取上下文的入口说明。
|
||||||
|
|
||||||
|
```python
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.9 BootstrapContext
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BootstrapContext(BaseModel):
|
||||||
|
messages: list[Message] = []
|
||||||
|
summary: str | None = None
|
||||||
|
artifacts: list[ArtifactRef] = []
|
||||||
|
metadata: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- `bootstrap.messages` 不是 LangBot Host 的默认行为。
|
||||||
|
- 自管 context runner 默认应收到空 bootstrap。
|
||||||
|
- Host 不应为了”帮 agent 更聪明”而自动拼接完整 transcript。
|
||||||
|
- 类似历史窗口策略应由具体 runner 自己解释 binding config,并通过 Host history API 拉取历史;new/official runners 不应依赖 Pipeline adapter 下发历史窗口。
|
||||||
|
|
||||||
|
### 4.10 RuntimeContext
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRuntimeContext(BaseModel):
|
||||||
|
host: str = "langbot"
|
||||||
|
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。
|
||||||
|
|
||||||
|
### 4.11 State
|
||||||
|
|
||||||
|
```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 也可以完全自管状态。
|
||||||
|
|
||||||
|
## 5. 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` 访问这些能力。
|
||||||
|
|
||||||
|
## 6. Result Stream
|
||||||
|
|
||||||
|
### 6.1 AgentRunResult
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentRunResult(BaseModel):
|
||||||
|
run_id: str
|
||||||
|
type: str
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
sequence: int | None = None
|
||||||
|
timestamp: int | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 稳定 result types
|
||||||
|
|
||||||
|
| type | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `message.delta` | 流式消息片段。 |
|
||||||
|
| `message.completed` | 完整消息。 |
|
||||||
|
| `tool.call.started` | runner 开始工具调用的可观测事件。 |
|
||||||
|
| `tool.call.completed` | runner 完成工具调用的可观测事件。 |
|
||||||
|
| `artifact.created` | runner 生成 artifact。 |
|
||||||
|
| `state.updated` | runner 请求更新 host-owned state。 |
|
||||||
|
| `action.requested` | runner 请求 Host 执行平台动作。 |
|
||||||
|
| `run.completed` | run 正常结束。 |
|
||||||
|
| `run.failed` | run 失败。 |
|
||||||
|
|
||||||
|
Host 必须忽略未知 result type 并记录 warning,除非该 type 明确要求强校验。
|
||||||
|
|
||||||
|
### 6.3 message.delta
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message.delta",
|
||||||
|
"data": {
|
||||||
|
"chunk": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "hel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 message.completed
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "message.completed",
|
||||||
|
"data": {
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "hello"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 state.updated
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "state.updated",
|
||||||
|
"data": {
|
||||||
|
"scope": "conversation",
|
||||||
|
"key": "external.session_id",
|
||||||
|
"value": "abc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Host 必须校验 scope、key、value 大小和 JSON 可序列化性。
|
||||||
|
|
||||||
|
### 6.6 action.requested
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "action.requested",
|
||||||
|
"data": {
|
||||||
|
"action": "message.edit",
|
||||||
|
"target": {"message_id": "..."},
|
||||||
|
"payload": {"text": "..."}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Protocol v1 只定义表达方式。Host 是否执行 action 取决于 platform API 能力、binding policy、审批策略和实现阶段。
|
||||||
|
|
||||||
|
## 7. AgentRunAPIProxy
|
||||||
|
|
||||||
|
所有 proxy action 必须携带 `run_id`。Host 必须校验:
|
||||||
|
|
||||||
|
- active run session 存在。
|
||||||
|
- caller plugin identity 匹配。
|
||||||
|
- resource 在本次 `ctx.resources` 中授权。
|
||||||
|
- scope 不越界。
|
||||||
|
- payload size / rate limit / deadline 合法。
|
||||||
|
|
||||||
|
### 7.1 Model APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Tool APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.tools.get_detail(tool_name)
|
||||||
|
await api.tools.call(tool_name, parameters)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Knowledge APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.knowledge.retrieve(kb_id, query_text, top_k=5, filters=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 History APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
History API 返回 Transcript projection,不返回原始平台 payload。
|
||||||
|
|
||||||
|
### 7.5 Event APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.events.get(event_id)
|
||||||
|
await api.events.page(before_cursor=None, limit=50)
|
||||||
|
```
|
||||||
|
|
||||||
|
Event API 返回稳定 event envelope 或受限 raw ref,不默认返回大 payload。
|
||||||
|
|
||||||
|
### 7.6 Artifact APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.artifacts.metadata(artifact_id)
|
||||||
|
await api.artifacts.read_range(artifact_id, offset=0, length=65536)
|
||||||
|
await api.artifacts.open_stream(artifact_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifact API 必须支持大小限制、MIME 校验、过期时间和授权范围。
|
||||||
|
|
||||||
|
### 7.7 State / Storage APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
建议区分:
|
||||||
|
|
||||||
|
- `state`: 小型 JSON 状态,适合 conversation / actor / runner / binding。
|
||||||
|
- `storage`: blob 或较大数据,适合插件私有数据、workspace 数据、checkpoint。
|
||||||
|
|
||||||
|
### 7.8 Platform APIs
|
||||||
|
|
||||||
|
```python
|
||||||
|
await api.platform.request_action(action, target, payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
平台 API 是受限能力。默认不开放。需要 runner manifest、binding policy、用户审批策略同时允许。
|
||||||
|
|
||||||
|
## 8. 错误模型
|
||||||
|
|
||||||
|
Host API 错误统一返回:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentAPIError(BaseModel):
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
retryable: bool = False
|
||||||
|
details: dict[str, Any] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议 code:
|
||||||
|
|
||||||
|
| 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 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。
|
||||||
|
|
||||||
|
## 10. Security 与 Guardrail
|
||||||
|
|
||||||
|
Protocol v1 的安全边界在 Host:
|
||||||
|
|
||||||
|
- Runner 不能直接访问未授权 model/tool/kb/history/artifact/storage。
|
||||||
|
- SDK 本地校验只提升开发体验,不能替代 Host 校验。
|
||||||
|
- 所有 resource id 对 runner 来说都是 opaque。
|
||||||
|
- 默认只能访问当前 conversation / thread 的 history。
|
||||||
|
- 跨会话、workspace 级 history 或 storage 必须额外授权。
|
||||||
|
- 大 payload 必须 artifact 化。
|
||||||
|
- Host 必须记录 run_id、runner_id、action、resource、scope、result。
|
||||||
|
|
||||||
|
对外部 harness runner,边界进一步拆分为:
|
||||||
|
|
||||||
|
- Host 在调用前完成 binding/resource policy 裁剪、路径策略、secret 过滤和审计记录。
|
||||||
|
- Runner plugin 把授权后的 context/resource projection 适配为目标 harness 的 context 文件、MCP 配置、skill 目录、环境变量或 CLI 参数。
|
||||||
|
- Claude Code / Codex / Kimi Code 等外部 harness 的 native permission mode、allowed/disallowed tools 和执行隔离策略只是额外执行约束,不能替代 Host 侧授权。
|
||||||
|
- 外部 session id、working directory、checkpoint 等跨轮次指针应作为小型 JSON state 保存,例如 `external.session_id`、`external.working_directory`。
|
||||||
|
|
||||||
|
完整路径隔离、MCP allowlist、secret redaction、配额、workspace 清理和发布级安全测试不属于当前 Protocol v1 smoke 闭环,详见 [SECURITY_HARDENING.md](./SECURITY_HARDENING.md)。
|
||||||
|
|
||||||
|
Host 不负责业务编排:
|
||||||
|
|
||||||
|
- 不拼接全量历史。
|
||||||
|
- 不替 runner 做业务 prompt assembly。
|
||||||
|
- 不内置 agent memory 策略。
|
||||||
|
- 不内置 tool loop 业务流程。
|
||||||
|
- 不内置上下文压缩策略。
|
||||||
|
|
||||||
|
这些能力可以由官方或第三方 AgentRunner 插件实现,并通过公开 Host APIs 消费 LangBot 的状态、历史、存储、artifact、模型、工具和知识库能力。
|
||||||
|
|
||||||
|
## 11. Pipeline Adapter
|
||||||
|
|
||||||
|
Pipeline 是当前入口 adapter,不是协议中心。
|
||||||
|
|
||||||
|
**当前分支已实现**:
|
||||||
|
|
||||||
|
- ✅ `PipelineAdapter.query_to_event(query)` — 从 `Query` 构造 `AgentEventEnvelope`
|
||||||
|
- ✅ `PipelineAdapter.pipeline_config_to_binding(query, runner_id)` — 从 Pipeline config 构造临时 AgentBinding
|
||||||
|
- ✅ `run_from_query()` 委托到 `run(event, binding)`
|
||||||
|
- ✅ runner-specific config 从 Pipeline 当前绑定配置透传到 `AgentBinding.runner_config` / `ctx.config`
|
||||||
|
- ✅ Query-only 字段放入 `adapter` context
|
||||||
|
|
||||||
|
Pipeline adapter 负责:
|
||||||
|
|
||||||
|
- 从 `Query` 构造 `AgentEventContext`。
|
||||||
|
- 从 Pipeline config 构造临时 AgentBinding。
|
||||||
|
- 从当前 runner binding config 构造 `ctx.config`。
|
||||||
|
- 保留必要的 legacy adapter metadata,但不定义历史窗口、prompt 组装或 agentic context 策略。
|
||||||
|
- 后续若需要传递 preprocessing / hook 后的有效指令,应通过 Host prompt/instruction
|
||||||
|
package pull API 暴露能力位和引用,而不是继续把 prompt 推入 `ctx.adapter.extra`。
|
||||||
|
- 将 Query-only 字段放入 `adapter`。
|
||||||
|
|
||||||
|
Runner 不应长期依赖 `adapter`。新 runner 应只依赖 event-first context 和 Host APIs。
|
||||||
|
|
||||||
|
## 12. 最小 v1 完成标准
|
||||||
|
|
||||||
|
Protocol v1 已在当前分支完成:
|
||||||
|
|
||||||
|
- ✅ SDK 定义 `AgentRunnerManifest`、`AgentRunContext`、`AgentRunResult`、`AgentRunAPIProxy`
|
||||||
|
- ✅ Runtime 支持 `LIST_AGENT_RUNNERS` 和 `RUN_AGENT`
|
||||||
|
- ✅ Host 支持 `run_id` session authorization
|
||||||
|
- ✅ Host 能从当前 Pipeline 入口生成 event-first context
|
||||||
|
- ✅ `messages` 降级为 optional bootstrap
|
||||||
|
- ✅ `max-round` 不出现在协议实体中,也不属于 Host / Pipeline 语义
|
||||||
|
- ✅ Proxy 至少覆盖 model、tool、knowledge、state/storage
|
||||||
|
- ✅ History / event / artifact API 已落地
|
||||||
|
- ✅ EventLog / Transcript / ArtifactStore / PersistentStateStore 已落地
|
||||||
|
- ✅ 外部 harness runner 最小 smoke 已落地:Claude Code runner 能消费 event-first context、返回消息、写回 `external.session_id` / `external.working_directory`
|
||||||
|
|
||||||
|
## 13. 开放问题
|
||||||
|
|
||||||
|
- `AgentBinding` 是否需要进入 SDK 文档作为只读诊断信息,还是完全 Host 内部。
|
||||||
|
- `TranscriptItem` 的最小字段集如何定义。
|
||||||
|
- ArtifactStore 是否复用现有 BinaryStorage backend,还是引入独立实体。
|
||||||
|
- State 与 Storage 的边界是否需要更强类型。
|
||||||
|
- `platform_api` action 的审批模型如何表达。
|
||||||
|
- 多 runner 并发处理同一 event 时,result delivery 的冲突策略如何定义。
|
||||||
|
- Host 侧 scoped MCP / skill / workspace projection 是否需要从 runner config 上移为一等 resource projection API。
|
||||||
125
docs/agent-runner-pluginization/README.md
Normal file
125
docs/agent-runner-pluginization/README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Agent Runner 插件化文档入口
|
||||||
|
|
||||||
|
本文档是 agent-runner 插件化工作的路由页。具体设计拆到独立文档中维护,避免把 LangBot 宿主架构、SDK 协议、上下文管理、EBA 预留和官方 runner 迁移混在同一份 README 里。
|
||||||
|
|
||||||
|
## 本分支目标
|
||||||
|
|
||||||
|
**本分支目标:AgentRunner 外化 / 插件化基础设施**
|
||||||
|
|
||||||
|
本分支只做 LangBot 作为 Agent Host 的基础能力建设:
|
||||||
|
|
||||||
|
- LangBot 与 SDK 的稳定协议合同(Protocol v1)
|
||||||
|
- Host-side `AgentEventEnvelope` / `AgentBinding` 模型
|
||||||
|
- `run(event, binding)` event-first 入口
|
||||||
|
- `PipelineAdapter`:Pipeline 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 入口。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
**当前 Pipeline 是入口 adapter,不再是 agent runner 设计核心。**
|
||||||
|
|
||||||
|
当前主入口仍可由 Pipeline 触发,但内部已转换成 event-first path:
|
||||||
|
|
||||||
|
1. `run_from_query()` 使用 `PipelineAdapter.query_to_event(query)` 转换为 `AgentEventEnvelope`
|
||||||
|
2. `run_from_query()` 使用 `PipelineAdapter.pipeline_config_to_binding(query, runner_id)` 转换为 `AgentBinding`
|
||||||
|
3. `run_from_query()` 委托到 `run(event, binding, bound_plugins, adapter_context)`
|
||||||
|
|
||||||
|
Pipeline path 已获得 event-first host capabilities:
|
||||||
|
- EventLog / Transcript 写入
|
||||||
|
- ArtifactStore 注册
|
||||||
|
- PersistentStateStore 状态持久化
|
||||||
|
- History / Event / Artifact / State pull APIs 可用
|
||||||
|
|
||||||
|
## 设计文档
|
||||||
|
|
||||||
|
| 文档 | 关注点 |
|
||||||
|
| --- | --- |
|
||||||
|
| [PROTOCOL_V1.md](./PROTOCOL_V1.md) | LangBot Host 与 SDK / Runtime / AgentRunner 的协议合同:run context、result stream、proxy actions、错误和 adapter 边界。 |
|
||||||
|
| [HOST_SDK_INFRASTRUCTURE.md](./HOST_SDK_INFRASTRUCTURE.md) | LangBot 宿主能力、SDK 协议、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 策略。
|
||||||
|
|
||||||
|
`max-round` 这类历史窗口参数不应作为目标协议继续扩展;如果某个 runner 仍需要类似策略,应由该 runner 的 manifest/config schema 暴露为 binding config。
|
||||||
|
|
||||||
|
详见 [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 完整能力,只预留:**
|
||||||
|
- event-first envelope (`AgentEventEnvelope`)
|
||||||
|
- AgentBinding model
|
||||||
|
- `run(event, binding)` 入口
|
||||||
|
- PipelineAdapter(当前 AgentEventEnvelope / AgentBinding 的 Pipeline 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、能力和权限。
|
||||||
|
- 插件本身按单实例、无状态执行单元理解;不同绑定不创建多个插件实例。
|
||||||
|
- 绑定只保存 runner id 和绑定配置,不代表插件实例状态。
|
||||||
|
- LangBot 可以提供 host-owned state / storage 能力,让 runner 把状态寄宿在 LangBot;但这应该是授权能力,不是强制要求。
|
||||||
|
- 官方 runner 插件是协议消费者,不是协议设计的优先约束。
|
||||||
|
- Pipeline 是当前入口 adapter,不是未来架构中心。
|
||||||
|
- EventGateway 是 future integration point,由外部 event branch 提供。
|
||||||
|
- Runtime control plane 是 v2 Host capability layer,不阻塞当前 AgentRunner v1 主线;agent 管控面插件应构建在该 Host 能力层之上。
|
||||||
225
docs/agent-runner-pluginization/RUNTIME_CONTROL_PLANE_V2.md
Normal file
225
docs/agent-runner-pluginization/RUNTIME_CONTROL_PLANE_V2.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Agent Runtime Control Plane V2
|
||||||
|
|
||||||
|
本文档记录后续 Agent Platform / runtime 管控面的设计方向。它是当前讨论中的 **v2 文档**,但这里的 v2 指 Host capability layer / runtime control plane,不是 `AgentRunner Protocol v2`,也不属于当前 AgentRunner Protocol v1 插件化主线的交付范围。
|
||||||
|
|
||||||
|
## 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 和受控错误回流
|
||||||
|
- binding 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 是核心前端的一部分,还是完全由管理插件提供。
|
||||||
73
docs/agent-runner-pluginization/SECURITY_HARDENING.md
Normal file
73
docs/agent-runner-pluginization/SECURITY_HARDENING.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Agent Runner Security Hardening
|
||||||
|
|
||||||
|
本文档记录 agent-runner 插件化进入生产发布前需要补齐的安全与稳定加固项。
|
||||||
|
|
||||||
|
## 状态
|
||||||
|
|
||||||
|
**当前结论:暂不塞进本阶段 agent-runner plugin 协议闭环。**
|
||||||
|
|
||||||
|
本阶段目标是验证 LangBot 可以通过统一的 `run(event, binding)` 协议接入 `local-agent` 与外部 harness runner(如 Claude Code runner),并能传递事件、上下文、资源句柄、状态和结果流。
|
||||||
|
|
||||||
|
安全发布级 hardening 是后续 release gate,不应阻塞当前协议闭环,但必须作为进入生产默认启用前的验收条件。
|
||||||
|
|
||||||
|
## 责任边界
|
||||||
|
|
||||||
|
### 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 下发的 binding 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 的完整实现。
|
||||||
|
|
||||||
1944
docs/service-api-openapi.json
Normal file
1944
docs/service-api-openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,53 +0,0 @@
|
|||||||
from v1 import client
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class TestDifyClient:
|
|
||||||
async def test_chat_messages(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(
|
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
async for chunk in cln.chat_messages(
|
|
||||||
inputs={}, query='调用工具查看现在几点?', user='test'
|
|
||||||
):
|
|
||||||
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_upload_file(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(
|
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
file_bytes = open('img.png', 'rb').read()
|
|
||||||
|
|
||||||
print(type(file_bytes))
|
|
||||||
|
|
||||||
file = ('img2.png', file_bytes, 'image/png')
|
|
||||||
|
|
||||||
resp = await cln.upload_file(file=file, user='test')
|
|
||||||
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_workflow_run(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(
|
|
||||||
api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL')
|
|
||||||
)
|
|
||||||
|
|
||||||
# resp = await cln.workflow_run(inputs={}, user="test")
|
|
||||||
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
# print(resp)
|
|
||||||
chunks = []
|
|
||||||
|
|
||||||
ignored_events = ['text_chunk']
|
|
||||||
async for chunk in cln.workflow_run(inputs={}, user='test'):
|
|
||||||
if chunk['event'] in ignored_events:
|
|
||||||
continue
|
|
||||||
chunks.append(chunk)
|
|
||||||
print(json.dumps(chunks, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(TestDifyClient().test_chat_messages())
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import base64
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from typing import Callable
|
|
||||||
import dingtalk_stream
|
|
||||||
from .EchoHandler import EchoTextHandler
|
|
||||||
from .dingtalkevent import DingTalkEvent
|
|
||||||
import httpx
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
class DingTalkClient:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
client_id: str,
|
|
||||||
client_secret: str,
|
|
||||||
robot_name: str,
|
|
||||||
robot_code: str,
|
|
||||||
markdown_card: bool,
|
|
||||||
logger: None,
|
|
||||||
):
|
|
||||||
"""初始化 WebSocket 连接并自动启动"""
|
|
||||||
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
|
||||||
self.client = dingtalk_stream.DingTalkStreamClient(self.credential)
|
|
||||||
self.key = client_id
|
|
||||||
self.secret = client_secret
|
|
||||||
# 在 DingTalkClient 中传入自己作为参数,避免循环导入
|
|
||||||
self.EchoTextHandler = EchoTextHandler(self)
|
|
||||||
self.client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler)
|
|
||||||
self._message_handlers = {
|
|
||||||
'example': [],
|
|
||||||
}
|
|
||||||
self.access_token = ''
|
|
||||||
self.robot_name = robot_name
|
|
||||||
self.robot_code = robot_code
|
|
||||||
self.access_token_expiry_time = ''
|
|
||||||
self.markdown_card = markdown_card
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def get_access_token(self):
|
|
||||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
|
||||||
headers = {'Content-Type': 'application/json'}
|
|
||||||
data = {'appKey': self.key, 'appSecret': self.secret}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
try:
|
|
||||||
response = await client.post(url, json=data, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
response_data = response.json()
|
|
||||||
self.access_token = response_data.get('accessToken')
|
|
||||||
expires_in = int(response_data.get('expireIn', 7200))
|
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error("failed to get access token in dingtalk")
|
|
||||||
|
|
||||||
async def is_token_expired(self):
|
|
||||||
"""检查token是否过期"""
|
|
||||||
if self.access_token_expiry_time is None:
|
|
||||||
return True
|
|
||||||
return time.time() > self.access_token_expiry_time
|
|
||||||
|
|
||||||
async def check_access_token(self):
|
|
||||||
if not self.access_token or await self.is_token_expired():
|
|
||||||
return False
|
|
||||||
return bool(self.access_token and self.access_token.strip())
|
|
||||||
|
|
||||||
async def download_image(self, download_code: str):
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
|
||||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
|
||||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
result = response.json()
|
|
||||||
download_url = result.get('downloadUrl')
|
|
||||||
else:
|
|
||||||
await self.logger.error(f"failed to get download url: {response.json()}")
|
|
||||||
|
|
||||||
if download_url:
|
|
||||||
return await self.download_url_to_base64(download_url)
|
|
||||||
|
|
||||||
async def download_url_to_base64(self, download_url):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(download_url)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
file_bytes = response.content
|
|
||||||
base64_str = base64.b64encode(file_bytes).decode('utf-8') # 返回字符串格式
|
|
||||||
return base64_str
|
|
||||||
else:
|
|
||||||
await self.logger.error(f"failed to get files: {response.json()}")
|
|
||||||
|
|
||||||
async def get_audio_url(self, download_code: str):
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
|
||||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
|
||||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
result = response.json()
|
|
||||||
download_url = result.get('downloadUrl')
|
|
||||||
if download_url:
|
|
||||||
return await self.download_url_to_base64(download_url)
|
|
||||||
else:
|
|
||||||
await self.logger.error(f"failed to get audio: {response.json()}")
|
|
||||||
else:
|
|
||||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
|
||||||
|
|
||||||
async def update_incoming_message(self, message):
|
|
||||||
"""异步更新 DingTalkClient 中的 incoming_message"""
|
|
||||||
message_data = await self.get_message(message)
|
|
||||||
if message_data:
|
|
||||||
event = DingTalkEvent.from_payload(message_data)
|
|
||||||
if event:
|
|
||||||
await self._handle_message(event)
|
|
||||||
|
|
||||||
async def send_message(self, content: str, incoming_message,at:bool):
|
|
||||||
if self.markdown_card:
|
|
||||||
if at:
|
|
||||||
self.EchoTextHandler.reply_markdown(
|
|
||||||
title='@'+incoming_message.sender_nick+' '+content,
|
|
||||||
text='@'+incoming_message.sender_nick+' '+content,
|
|
||||||
incoming_message=incoming_message,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.EchoTextHandler.reply_markdown(
|
|
||||||
title=content,
|
|
||||||
text=content,
|
|
||||||
incoming_message=incoming_message,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.EchoTextHandler.reply_text(content, incoming_message)
|
|
||||||
|
|
||||||
async def get_incoming_message(self):
|
|
||||||
"""获取收到的消息"""
|
|
||||||
return await self.EchoTextHandler.get_incoming_message()
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
def decorator(func: Callable[[DingTalkEvent], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def _handle_message(self, event: DingTalkEvent):
|
|
||||||
"""
|
|
||||||
处理消息事件。
|
|
||||||
"""
|
|
||||||
msg_type = event.conversation
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
|
|
||||||
async def get_message(self, incoming_message: dingtalk_stream.chatbot.ChatbotMessage):
|
|
||||||
try:
|
|
||||||
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
|
|
||||||
message_data = {
|
|
||||||
'IncomingMessage': incoming_message,
|
|
||||||
}
|
|
||||||
if str(incoming_message.conversation_type) == '1':
|
|
||||||
message_data['conversation_type'] = 'FriendMessage'
|
|
||||||
elif str(incoming_message.conversation_type) == '2':
|
|
||||||
message_data['conversation_type'] = 'GroupMessage'
|
|
||||||
|
|
||||||
if incoming_message.message_type == 'richText':
|
|
||||||
data = incoming_message.rich_text_content.to_dict()
|
|
||||||
for item in data['richText']:
|
|
||||||
if 'text' in item:
|
|
||||||
message_data['Content'] = item['text']
|
|
||||||
if incoming_message.get_image_list()[0]:
|
|
||||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
|
||||||
message_data['Type'] = 'text'
|
|
||||||
|
|
||||||
elif incoming_message.message_type == 'text':
|
|
||||||
message_data['Content'] = incoming_message.get_text_list()[0]
|
|
||||||
|
|
||||||
message_data['Type'] = 'text'
|
|
||||||
elif incoming_message.message_type == 'picture':
|
|
||||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
|
||||||
|
|
||||||
message_data['Type'] = 'image'
|
|
||||||
elif incoming_message.message_type == 'audio':
|
|
||||||
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
|
|
||||||
|
|
||||||
message_data['Type'] = 'audio'
|
|
||||||
|
|
||||||
copy_message_data = message_data.copy()
|
|
||||||
del copy_message_data['IncomingMessage']
|
|
||||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
|
||||||
except Exception as e:
|
|
||||||
if self.logger:
|
|
||||||
await self.logger.error(f"Error in get_message: {traceback.format_exc()}")
|
|
||||||
else:
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def send_proactive_message_to_one(self, target_id: str, content: str):
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'x-acs-dingtalk-access-token': self.access_token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'robotCode': self.robot_code,
|
|
||||||
'userIds': [target_id],
|
|
||||||
'msgKey': 'sampleText',
|
|
||||||
'msgParam': json.dumps({'content': content}),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
|
||||||
raise Exception(f"failed to send proactive massage to person: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'x-acs-dingtalk-access-token': self.access_token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'robotCode': self.robot_code,
|
|
||||||
'openConversationId': target_id,
|
|
||||||
'msgKey': 'sampleText',
|
|
||||||
'msgParam': json.dumps({'content': content}),
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
|
||||||
raise Exception(f"failed to send proactive massage to group: {traceback.format_exc()}")
|
|
||||||
|
|
||||||
async def start(self):
|
|
||||||
"""启动 WebSocket 连接,监听消息"""
|
|
||||||
await self.client.start()
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import time
|
|
||||||
from quart import request
|
|
||||||
import httpx
|
|
||||||
from quart import Quart
|
|
||||||
from typing import Callable, Dict, Any
|
|
||||||
from pkg.platform.types import events as platform_events
|
|
||||||
from .qqofficialevent import QQOfficialEvent
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
||||||
|
|
||||||
|
|
||||||
def handle_validation(body: dict, bot_secret: str):
|
|
||||||
# bot正确的secert是32位的,此处仅为了适配演示demo
|
|
||||||
while len(bot_secret) < 32:
|
|
||||||
bot_secret = bot_secret * 2
|
|
||||||
bot_secret = bot_secret[:32]
|
|
||||||
# 实际使用场景中以上三行内容可清除
|
|
||||||
|
|
||||||
seed_bytes = bot_secret.encode()
|
|
||||||
|
|
||||||
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
|
|
||||||
|
|
||||||
msg = body['d']['event_ts'] + body['d']['plain_token']
|
|
||||||
msg_bytes = msg.encode()
|
|
||||||
|
|
||||||
signature = signing_key.sign(msg_bytes)
|
|
||||||
|
|
||||||
signature_hex = signature.hex()
|
|
||||||
|
|
||||||
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
|
||||||
def __init__(self, secret: str, token: str, app_id: str, 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 +0,0 @@
|
|||||||
from .client import WeChatPadClient
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
|
|
||||||
from libs.wechatpad_api.util.http_util import async_request, post_json
|
|
||||||
|
|
||||||
|
|
||||||
class MessageApi:
|
|
||||||
def __init__(self, base_url, token):
|
|
||||||
self.base_url = base_url
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
def post_text(self, to_wxid, content, ats: list= []):
|
|
||||||
'''
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app_id: 微信id
|
|
||||||
to_wxid: 发送方的微信id
|
|
||||||
content: 内容
|
|
||||||
ats: at
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
'''
|
|
||||||
url = self.base_url + "/message/SendTextMessage"
|
|
||||||
"""发送文字消息"""
|
|
||||||
json_data = {
|
|
||||||
"MsgItem": [
|
|
||||||
{
|
|
||||||
"AtWxIDList": ats,
|
|
||||||
"ImageContent": "",
|
|
||||||
"MsgType": 0,
|
|
||||||
"TextContent": content,
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return post_json(base_url=url, token=self.token, data=json_data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def post_image(self, to_wxid, img_url, ats: list= []):
|
|
||||||
"""发送图片消息"""
|
|
||||||
# 这里好像可以尝试发送多个暂时未测试
|
|
||||||
json_data = {
|
|
||||||
"MsgItem": [
|
|
||||||
{
|
|
||||||
"AtWxIDList": ats,
|
|
||||||
"ImageContent": img_url,
|
|
||||||
"MsgType": 0,
|
|
||||||
"TextContent": '',
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
url = self.base_url + "/message/SendImageMessage"
|
|
||||||
return post_json(base_url=url, token=self.token, data=json_data)
|
|
||||||
|
|
||||||
def post_voice(self, to_wxid, voice_data, voice_forma, voice_duration):
|
|
||||||
"""发送语音消息"""
|
|
||||||
json_data = {
|
|
||||||
"ToUserName": to_wxid,
|
|
||||||
"VoiceData": voice_data,
|
|
||||||
"VoiceFormat": voice_forma,
|
|
||||||
"VoiceSecond": voice_duration
|
|
||||||
}
|
|
||||||
url = self.base_url + "/message/SendVoice"
|
|
||||||
return post_json(base_url=url, token=self.token, data=json_data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def post_name_card(self, alias, to_wxid, nick_name, name_card_wxid, flag):
|
|
||||||
"""发送名片消息"""
|
|
||||||
param = {
|
|
||||||
"CardAlias": alias,
|
|
||||||
"CardFlag": flag,
|
|
||||||
"CardNickName": nick_name,
|
|
||||||
"CardWxId": name_card_wxid,
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
url = f"{self.base_url}/message/ShareCardMessage"
|
|
||||||
return post_json(base_url=url, token=self.token, data=param)
|
|
||||||
|
|
||||||
def post_emoji(self, to_wxid, emoji_md5, emoji_size:int=0):
|
|
||||||
"""发送emoji消息"""
|
|
||||||
json_data = {
|
|
||||||
"EmojiList": [
|
|
||||||
{
|
|
||||||
"EmojiMd5": emoji_md5,
|
|
||||||
"EmojiSize": emoji_size,
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
url = f"{self.base_url}/message/SendEmojiMessage"
|
|
||||||
return post_json(base_url=url, token=self.token, data=json_data)
|
|
||||||
|
|
||||||
def post_app_msg(self, to_wxid,xml_data, contenttype:int=0):
|
|
||||||
"""发送appmsg消息"""
|
|
||||||
json_data = {
|
|
||||||
"AppList": [
|
|
||||||
{
|
|
||||||
"ContentType": contenttype,
|
|
||||||
"ContentXML": xml_data,
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
url = f"{self.base_url}/message/SendAppMessage"
|
|
||||||
return post_json(base_url=url, token=self.token, data=json_data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def revoke_msg(self, to_wxid, msg_id, new_msg_id, create_time):
|
|
||||||
"""撤回消息"""
|
|
||||||
param = {
|
|
||||||
"ClientMsgId": msg_id,
|
|
||||||
"CreateTime": create_time,
|
|
||||||
"NewMsgId": new_msg_id,
|
|
||||||
"ToUserName": to_wxid
|
|
||||||
}
|
|
||||||
url = f"{self.base_url}/message/RevokeMsg"
|
|
||||||
return post_json(base_url=url, token=self.token, data=param)
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import requests
|
|
||||||
|
|
||||||
def post_json(base_url, token, data=None):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
url = base_url + f'?key={token}'
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(url, json=data, headers=headers, timeout=60)
|
|
||||||
response.raise_for_status()
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
raise RuntimeError(response.text)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"http请求失败, url={url}, exception={e}")
|
|
||||||
raise RuntimeError(str(e))
|
|
||||||
|
|
||||||
def get_json(base_url, token):
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
url = base_url + f'?key={token}'
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers, timeout=60)
|
|
||||||
response.raise_for_status()
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
raise RuntimeError(response.text)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"http请求失败, url={url}, exception={e}")
|
|
||||||
raise RuntimeError(str(e))
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
|
|
||||||
async def async_request(
|
|
||||||
base_url: str,
|
|
||||||
token_key: str,
|
|
||||||
method: str = 'POST',
|
|
||||||
params: dict = None,
|
|
||||||
# headers: dict = None,
|
|
||||||
data: dict = None,
|
|
||||||
json: dict = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
通用异步请求函数
|
|
||||||
|
|
||||||
:param base_url: 请求URL
|
|
||||||
:param token_key: 请求token
|
|
||||||
:param method: HTTP方法 (GET, POST, PUT, DELETE等)
|
|
||||||
:param params: URL查询参数
|
|
||||||
# :param headers: 请求头
|
|
||||||
:param data: 表单数据
|
|
||||||
:param json: JSON数据
|
|
||||||
:return: 响应文本
|
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
url = f"{base_url}?key={token_key}"
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.request(
|
|
||||||
method=method,
|
|
||||||
url=url,
|
|
||||||
params=params,
|
|
||||||
headers=headers,
|
|
||||||
data=data,
|
|
||||||
json=json
|
|
||||||
) as response:
|
|
||||||
response.raise_for_status() # 如果状态码不是200,抛出异常
|
|
||||||
result = await response.json()
|
|
||||||
# print(result)
|
|
||||||
return result
|
|
||||||
# if result.get('Code') == 200:
|
|
||||||
#
|
|
||||||
# return await result
|
|
||||||
# else:
|
|
||||||
# raise RuntimeError("请求失败",response.text)
|
|
||||||
|
|
||||||
100
main.py
100
main.py
@@ -1,99 +1,3 @@
|
|||||||
import asyncio
|
import langbot.__main__
|
||||||
# LangBot 终端启动入口
|
|
||||||
# 在此层级解决依赖项检查。
|
|
||||||
# LangBot/main.py
|
|
||||||
|
|
||||||
asciiart = r"""
|
langbot.__main__.main()
|
||||||
_ ___ _
|
|
||||||
| | __ _ _ _ __ _| _ ) ___| |_
|
|
||||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
|
||||||
|____\__,_|_||_\__, |___/\___/\__|
|
|
||||||
|___/
|
|
||||||
|
|
||||||
⭐️ Open Source 开源地址: https://github.com/RockChinQ/LangBot
|
|
||||||
📖 Documentation 文档地址: https://docs.langbot.app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
|
||||||
print(asciiart)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
|
|
||||||
from pkg.core.bootutils import deps
|
|
||||||
|
|
||||||
missing_deps = await deps.check_deps()
|
|
||||||
|
|
||||||
if missing_deps:
|
|
||||||
print('以下依赖包未安装,将自动安装,请完成后重启程序:')
|
|
||||||
print(
|
|
||||||
'These dependencies are missing, they will be installed automatically, please restart the program after completion:'
|
|
||||||
)
|
|
||||||
for dep in missing_deps:
|
|
||||||
print('-', dep)
|
|
||||||
await deps.install_deps(missing_deps)
|
|
||||||
print('已自动安装缺失的依赖包,请重启程序。')
|
|
||||||
print('The missing dependencies have been installed automatically, please restart the program.')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# check plugin deps
|
|
||||||
await deps.precheck_plugin_deps()
|
|
||||||
|
|
||||||
# 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
|
||||||
import pydantic.version
|
|
||||||
|
|
||||||
if pydantic.version.VERSION < '2.0':
|
|
||||||
import pydantic
|
|
||||||
|
|
||||||
sys.modules['pydantic.v1'] = pydantic
|
|
||||||
|
|
||||||
# 检查配置文件
|
|
||||||
|
|
||||||
from pkg.core.bootutils import files
|
|
||||||
|
|
||||||
generated_files = await files.generate_files()
|
|
||||||
|
|
||||||
if generated_files:
|
|
||||||
print('以下文件不存在,已自动生成:')
|
|
||||||
print('Following files do not exist and have been automatically generated:')
|
|
||||||
for file in generated_files:
|
|
||||||
print('-', file)
|
|
||||||
|
|
||||||
from pkg.core import boot
|
|
||||||
|
|
||||||
await boot.main(loop)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 必须大于 3.10.1
|
|
||||||
if sys.version_info < (3, 10, 1):
|
|
||||||
print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version)
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Your Python version is not supported. Please exit the program by pressing any key.')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Check if the current directory is the LangBot project root directory
|
|
||||||
invalid_pwd = False
|
|
||||||
|
|
||||||
if not os.path.exists('main.py'):
|
|
||||||
invalid_pwd = True
|
|
||||||
else:
|
|
||||||
with open('main.py', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
if 'LangBot/main.py' not in content:
|
|
||||||
invalid_pwd = True
|
|
||||||
if invalid_pwd:
|
|
||||||
print('请在 LangBot 项目根目录下以命令形式运行此程序。')
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Please run this program in the LangBot project root directory in command form.')
|
|
||||||
print('Press any key to exit...')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
|
|
||||||
loop.run_until_complete(main_entry(loop))
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import typing
|
|
||||||
import enum
|
|
||||||
import quart
|
|
||||||
import traceback
|
|
||||||
from quart.typing import RouteCallable
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
|
|
||||||
|
|
||||||
preregistered_groups: list[type[RouterGroup]] = []
|
|
||||||
"""RouterGroup 的预注册列表"""
|
|
||||||
|
|
||||||
|
|
||||||
def group_class(name: str, path: str) -> None:
|
|
||||||
"""注册一个 RouterGroup"""
|
|
||||||
|
|
||||||
def decorator(cls: typing.Type[RouterGroup]) -> typing.Type[RouterGroup]:
|
|
||||||
cls.name = name
|
|
||||||
cls.path = path
|
|
||||||
preregistered_groups.append(cls)
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class AuthType(enum.Enum):
|
|
||||||
"""认证类型"""
|
|
||||||
|
|
||||||
NONE = 'none'
|
|
||||||
USER_TOKEN = 'user-token'
|
|
||||||
|
|
||||||
|
|
||||||
class RouterGroup(abc.ABC):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
path: str
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
quart_app: quart.Quart
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application, quart_app: quart.Quart) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
self.quart_app = quart_app
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def route(
|
|
||||||
self,
|
|
||||||
rule: str,
|
|
||||||
auth_type: AuthType = AuthType.USER_TOKEN,
|
|
||||||
**options: typing.Any,
|
|
||||||
) -> typing.Callable[[RouteCallable], RouteCallable]: # decorator
|
|
||||||
"""注册一个路由"""
|
|
||||||
|
|
||||||
def decorator(f: RouteCallable) -> RouteCallable:
|
|
||||||
nonlocal rule
|
|
||||||
rule = self.path + rule
|
|
||||||
|
|
||||||
async def handler_error(*args, **kwargs):
|
|
||||||
if auth_type == AuthType.USER_TOKEN:
|
|
||||||
# 从Authorization头中获取token
|
|
||||||
token = quart.request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
return self.http_status(401, -1, '未提供有效的用户令牌')
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_email = await self.ap.user_service.verify_jwt_token(token)
|
|
||||||
|
|
||||||
# check if this account exists
|
|
||||||
user = await self.ap.user_service.get_user_by_email(user_email)
|
|
||||||
if not user:
|
|
||||||
return self.http_status(401, -1, '用户不存在')
|
|
||||||
|
|
||||||
# 检查f是否接受user_email参数
|
|
||||||
if 'user_email' in f.__code__.co_varnames:
|
|
||||||
kwargs['user_email'] = user_email
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(401, -1, str(e))
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await f(*args, **kwargs)
|
|
||||||
except Exception: # 自动 500
|
|
||||||
traceback.print_exc()
|
|
||||||
# return self.http_status(500, -2, str(e))
|
|
||||||
return self.http_status(500, -2, 'internal server error')
|
|
||||||
|
|
||||||
new_f = handler_error
|
|
||||||
new_f.__name__ = (self.name + rule).replace('/', '__')
|
|
||||||
new_f.__doc__ = f.__doc__
|
|
||||||
|
|
||||||
self.quart_app.route(rule, **options)(new_f)
|
|
||||||
return f
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def success(self, data: typing.Any = None) -> quart.Response:
|
|
||||||
"""返回一个 200 响应"""
|
|
||||||
return quart.jsonify(
|
|
||||||
{
|
|
||||||
'code': 0,
|
|
||||||
'msg': 'ok',
|
|
||||||
'data': data,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def fail(self, code: int, msg: str) -> quart.Response:
|
|
||||||
"""返回一个异常响应"""
|
|
||||||
|
|
||||||
return quart.jsonify(
|
|
||||||
{
|
|
||||||
'code': code,
|
|
||||||
'msg': msg,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def http_status(self, status: int, code: int, msg: str) -> quart.Response:
|
|
||||||
"""返回一个指定状态码的响应"""
|
|
||||||
return self.fail(code, msg), status
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
import mimetypes
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('pipelines', '/api/v1/pipelines')
|
|
||||||
class PipelinesRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'pipelines': await self.ap.pipeline_service.get_pipelines()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
pipeline_uuid = await self.ap.pipeline_service.create_pipeline(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': pipeline_uuid})
|
|
||||||
|
|
||||||
@self.route('/_/metadata', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'configs': await self.ap.pipeline_service.get_pipeline_metadata()})
|
|
||||||
|
|
||||||
@self.route('/<pipeline_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(pipeline_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
pipeline = await self.ap.pipeline_service.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return self.http_status(404, -1, 'pipeline not found')
|
|
||||||
|
|
||||||
return self.success(data={'pipeline': pipeline})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.pipeline_service.delete_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,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,109 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from .....core import taskmgr
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('plugins', '/api/v1/plugins')
|
|
||||||
class PluginsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
plugins = self.ap.plugin_mgr.plugins()
|
|
||||||
|
|
||||||
plugins_data = [plugin.model_dump() for plugin in plugins]
|
|
||||||
|
|
||||||
return self.success(data={'plugins': plugins_data})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/toggle',
|
|
||||||
methods=['PUT'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
target_enabled = data.get('target_enabled')
|
|
||||||
await self.ap.plugin_mgr.update_plugin_switch(plugin_name, target_enabled)
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/update',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_mgr.update_plugin(plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-update-{plugin_name}',
|
|
||||||
label=f'更新插件 {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>',
|
|
||||||
methods=['GET', 'DELETE'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
return self.success(data={'plugin': plugin.model_dump()})
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_mgr.uninstall_plugin(plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-remove-{plugin_name}',
|
|
||||||
label=f'删除插件 {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/config',
|
|
||||||
methods=['GET', 'PUT'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
plugin = self.ap.plugin_mgr.get_plugin(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'config': plugin.plugin_config})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.plugin_mgr.set_plugin_config(plugin, data)
|
|
||||||
|
|
||||||
return self.success(data={})
|
|
||||||
|
|
||||||
@self.route('/reorder', methods=['PUT'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
await self.ap.plugin_mgr.reorder_plugins(data.get('plugins'))
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
short_source_str = data['source'][-8:]
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_mgr.install_plugin(data['source'], task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-github',
|
|
||||||
label=f'安装插件 ...{short_source_str}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
|
||||||
class LLMModelsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'])
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'models': await self.ap.model_service.get_llm_models()})
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
model_uuid = await self.ap.model_service.create_llm_model(json_data)
|
|
||||||
|
|
||||||
return self.success(data={'uuid': model_uuid})
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>', methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
model = await self.ap.model_service.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return self.http_status(404, -1, 'model not found')
|
|
||||||
|
|
||||||
return self.success(data={'model': model})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.model_service.update_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
await self.ap.model_service.delete_llm_model(model_uuid)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/<model_uuid>/test', methods=['POST'])
|
|
||||||
async def _(model_uuid: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.model_service.test_llm_model(model_uuid, json_data)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
from .....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('system', '/api/v1/system')
|
|
||||||
class SystemRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'version': constants.semantic_version,
|
|
||||||
'debug': constants.debug_mode,
|
|
||||||
'enabled_platform_count': len(self.ap.platform_mgr.get_running_adapters()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
task_type = quart.request.args.get('type')
|
|
||||||
|
|
||||||
if task_type == '':
|
|
||||||
task_type = None
|
|
||||||
|
|
||||||
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type))
|
|
||||||
|
|
||||||
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(task_id: str) -> str:
|
|
||||||
task = self.ap.task_mgr.get_task_by_id(int(task_id))
|
|
||||||
|
|
||||||
if task is None:
|
|
||||||
return self.http_status(404, 404, 'Task not found')
|
|
||||||
|
|
||||||
return self.success(data=task.to_dict())
|
|
||||||
|
|
||||||
@self.route('/reload', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
scope = json_data.get('scope')
|
|
||||||
|
|
||||||
await self.ap.reload(scope=scope)
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/_debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
py_code = await quart.request.data
|
|
||||||
|
|
||||||
ap = self.ap
|
|
||||||
|
|
||||||
return self.success(data=exec(py_code, {'ap': ap}))
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import quart
|
|
||||||
import argon2
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('user', '/api/v1/user')
|
|
||||||
class UserRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
|
|
||||||
|
|
||||||
if await self.ap.user_service.is_initialized():
|
|
||||||
return self.fail(1, '系统已初始化')
|
|
||||||
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
password = json_data['password']
|
|
||||||
|
|
||||||
await self.ap.user_service.create_user(user_email, password)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.fail(1, '用户名或密码错误')
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
token = await self.ap.user_service.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import bot as persistence_bot
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
class BotService:
|
|
||||||
"""机器人服务"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_bots(self) -> list[dict]:
|
|
||||||
"""获取所有机器人"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot) for bot in bots]
|
|
||||||
|
|
||||||
async def get_bot(self, bot_uuid: str) -> dict | None:
|
|
||||||
"""获取机器人"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
bot = result.first()
|
|
||||||
|
|
||||||
if bot is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot)
|
|
||||||
|
|
||||||
async def create_bot(self, bot_data: dict) -> str:
|
|
||||||
"""创建机器人"""
|
|
||||||
# TODO: 检查配置信息格式
|
|
||||||
bot_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# checkout the default pipeline
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None:
|
|
||||||
bot_data['use_pipeline_uuid'] = pipeline.uuid
|
|
||||||
bot_data['use_pipeline_name'] = pipeline.name
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(bot_data))
|
|
||||||
|
|
||||||
bot = await self.get_bot(bot_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.platform_mgr.load_bot(bot)
|
|
||||||
|
|
||||||
return bot_data['uuid']
|
|
||||||
|
|
||||||
async def update_bot(self, bot_uuid: str, bot_data: dict) -> None:
|
|
||||||
"""更新机器人"""
|
|
||||||
if 'uuid' in bot_data:
|
|
||||||
del bot_data['uuid']
|
|
||||||
|
|
||||||
# set use_pipeline_name
|
|
||||||
if 'use_pipeline_uuid' in bot_data:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == bot_data['use_pipeline_uuid']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None:
|
|
||||||
bot_data['use_pipeline_name'] = pipeline.name
|
|
||||||
else:
|
|
||||||
raise Exception('Pipeline not found')
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_bot.Bot).values(bot_data).where(persistence_bot.Bot.uuid == bot_uuid)
|
|
||||||
)
|
|
||||||
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
|
||||||
|
|
||||||
# select from db
|
|
||||||
bot = await self.get_bot(bot_uuid)
|
|
||||||
|
|
||||||
runtime_bot = await self.ap.platform_mgr.load_bot(bot)
|
|
||||||
|
|
||||||
if runtime_bot.enable:
|
|
||||||
await runtime_bot.run()
|
|
||||||
|
|
||||||
async def delete_bot(self, bot_uuid: str) -> None:
|
|
||||||
"""删除机器人"""
|
|
||||||
await self.ap.platform_mgr.remove_bot(bot_uuid)
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_bot.Bot).where(persistence_bot.Bot.uuid == bot_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def list_event_logs(
|
|
||||||
self, bot_uuid: str, from_index: int, max_count: int
|
|
||||||
) -> typing.Tuple[list[dict], int, int, int]:
|
|
||||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
|
||||||
if runtime_bot is None:
|
|
||||||
raise Exception('Bot not found')
|
|
||||||
|
|
||||||
logs, total_count = await runtime_bot.logger.get_logs(from_index, max_count)
|
|
||||||
|
|
||||||
return [log.to_json() for log in logs], total_count
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import model as persistence_model
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
from ....provider.modelmgr import requester as model_requester
|
|
||||||
from ....provider import entities as llm_entities
|
|
||||||
|
|
||||||
|
|
||||||
class ModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_llm_models(self) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model) for model in models]
|
|
||||||
|
|
||||||
async def create_llm_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_model.LLMModel).values(**model_data))
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
# check if default pipeline has no model bound
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
|
||||||
pipeline_config = pipeline.config
|
|
||||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
|
||||||
pipeline_data = {'config': pipeline_config}
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_llm_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
|
|
||||||
|
|
||||||
async def update_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.LLMModel)
|
|
||||||
.where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
async def delete_llm_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.llm_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_llm_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_llm_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
|
|
||||||
|
|
||||||
await runtime_llm_model.requester.invoke_llm(
|
|
||||||
query=None,
|
|
||||||
model=runtime_llm_model,
|
|
||||||
messages=[llm_entities.Message(role='user', content='Hello, world!')],
|
|
||||||
funcs=[],
|
|
||||||
extra_args={},
|
|
||||||
)
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import json
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
default_stage_order = [
|
|
||||||
'GroupRespondRuleCheckStage', # 群响应规则检查
|
|
||||||
'BanSessionCheckStage', # 封禁会话检查
|
|
||||||
'PreContentFilterStage', # 内容过滤前置阶段
|
|
||||||
'PreProcessor', # 预处理器
|
|
||||||
'ConversationMessageTruncator', # 会话消息截断器
|
|
||||||
'RequireRateLimitOccupancy', # 请求速率限制占用
|
|
||||||
'MessageProcessor', # 处理器
|
|
||||||
'ReleaseRateLimitOccupancy', # 释放速率限制占用
|
|
||||||
'PostContentFilterStage', # 内容过滤后置阶段
|
|
||||||
'ResponseWrapper', # 响应包装器
|
|
||||||
'LongTextProcessStage', # 长文本处理
|
|
||||||
'SendResponseBackStage', # 发送响应
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_pipeline_metadata(self) -> dict:
|
|
||||||
return [
|
|
||||||
self.ap.pipeline_config_meta_trigger.data,
|
|
||||||
self.ap.pipeline_config_meta_safety.data,
|
|
||||||
self.ap.pipeline_config_meta_ai.data,
|
|
||||||
self.ap.pipeline_config_meta_output.data,
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipelines(self) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_pipeline.LegacyPipeline))
|
|
||||||
|
|
||||||
pipelines = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
for pipeline in pipelines
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_pipeline(self, pipeline_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = result.first()
|
|
||||||
|
|
||||||
if pipeline is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
|
||||||
|
|
||||||
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
|
|
||||||
pipeline_data['uuid'] = str(uuid.uuid4())
|
|
||||||
pipeline_data['for_version'] = self.ap.ver_mgr.get_current_version()
|
|
||||||
pipeline_data['stages'] = default_stage_order.copy()
|
|
||||||
pipeline_data['is_default'] = default
|
|
||||||
pipeline_data['config'] = json.load(open('templates/default-pipeline-config.json', 'r', encoding='utf-8'))
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
return pipeline_data['uuid']
|
|
||||||
|
|
||||||
async def update_pipeline(self, pipeline_uuid: str, pipeline_data: dict) -> None:
|
|
||||||
if 'uuid' in pipeline_data:
|
|
||||||
del pipeline_data['uuid']
|
|
||||||
if 'for_version' in pipeline_data:
|
|
||||||
del pipeline_data['for_version']
|
|
||||||
if 'stages' in pipeline_data:
|
|
||||||
del pipeline_data['stages']
|
|
||||||
if 'is_default' in pipeline_data:
|
|
||||||
del pipeline_data['is_default']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
|
||||||
.where(persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid)
|
|
||||||
.values(**pipeline_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
pipeline = await self.get_pipeline(pipeline_uuid)
|
|
||||||
|
|
||||||
if 'name' in pipeline_data:
|
|
||||||
from ....entity.persistence import bot as persistence_bot
|
|
||||||
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_bot.Bot).where(persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
for bot in bots:
|
|
||||||
bot_data = {'use_pipeline_name': pipeline_data['name']}
|
|
||||||
await self.ap.bot_service.update_bot(bot.uuid, bot_data)
|
|
||||||
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
await self.ap.pipeline_mgr.load_pipeline(pipeline)
|
|
||||||
|
|
||||||
async def delete_pipeline(self, pipeline_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
import argon2
|
|
||||||
import jwt
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import user
|
|
||||||
from ....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def is_initialized(self) -> bool:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list is not None and len(result_list) > 0
|
|
||||||
|
|
||||||
async def create_user(self, user_email: str, password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_user_by_email(self, user_email: str) -> user.User | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list[0] if result_list is not None and len(result_list) > 0 else None
|
|
||||||
|
|
||||||
async def authenticate(self, user_email: str, password: str) -> str | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
|
|
||||||
if result_list is None or len(result_list) == 0:
|
|
||||||
raise ValueError('用户不存在')
|
|
||||||
|
|
||||||
user_obj = result_list[0]
|
|
||||||
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, password)
|
|
||||||
|
|
||||||
return await self.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
async def generate_jwt_token(self, user_email: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
jwt_expire = self.ap.instance_config.data['system']['jwt']['expire']
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
'user': user_email,
|
|
||||||
'iss': 'LangBot-' + constants.edition,
|
|
||||||
'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire),
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt.encode(payload, jwt_secret, algorithm='HS256')
|
|
||||||
|
|
||||||
async def verify_jwt_token(self, token: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
|
|
||||||
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from ..core import app, entities as core_entities
|
|
||||||
from . import entities, operator, errors
|
|
||||||
from ..utils import importutil
|
|
||||||
|
|
||||||
# 引入所有算子以便注册
|
|
||||||
from . import operators
|
|
||||||
|
|
||||||
importutil.import_modules_in_pkg(operators)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandManager:
|
|
||||||
"""命令管理器"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
cmd_list: list[operator.CommandOperator]
|
|
||||||
"""
|
|
||||||
运行时命令列表,扁平存储,各个对象包含对应的子节点引用
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
# 设置各个类的路径
|
|
||||||
def set_path(cls: operator.CommandOperator, ancestors: list[str]):
|
|
||||||
cls.path = '.'.join(ancestors + [cls.name])
|
|
||||||
for op in operator.preregistered_operators:
|
|
||||||
if op.parent_class == cls:
|
|
||||||
set_path(op, ancestors + [cls.name])
|
|
||||||
|
|
||||||
for cls in operator.preregistered_operators:
|
|
||||||
if cls.parent_class is None:
|
|
||||||
set_path(cls, [])
|
|
||||||
|
|
||||||
# 应用命令权限配置
|
|
||||||
for cls in operator.preregistered_operators:
|
|
||||||
if cls.path in self.ap.instance_config.data['command']['privilege']:
|
|
||||||
cls.lowest_privilege = self.ap.instance_config.data['command']['privilege'][cls.path]
|
|
||||||
|
|
||||||
# 实例化所有类
|
|
||||||
self.cmd_list = [cls(self.ap) for cls in operator.preregistered_operators]
|
|
||||||
|
|
||||||
# 设置所有类的子节点
|
|
||||||
for cmd in self.cmd_list:
|
|
||||||
cmd.children = [child for child in self.cmd_list if child.parent_class == cmd.__class__]
|
|
||||||
|
|
||||||
# 初始化所有类
|
|
||||||
for cmd in self.cmd_list:
|
|
||||||
await cmd.initialize()
|
|
||||||
|
|
||||||
async def _execute(
|
|
||||||
self,
|
|
||||||
context: entities.ExecuteContext,
|
|
||||||
operator_list: list[operator.CommandOperator],
|
|
||||||
operator: operator.CommandOperator = None,
|
|
||||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
"""执行命令"""
|
|
||||||
|
|
||||||
found = False
|
|
||||||
if len(context.crt_params) > 0: # 查找下一个参数是否对应此节点的某个子节点名
|
|
||||||
for oper in operator_list:
|
|
||||||
if (context.crt_params[0] == oper.name or context.crt_params[0] in oper.alias) and (
|
|
||||||
oper.parent_class is None or oper.parent_class == operator.__class__
|
|
||||||
):
|
|
||||||
found = True
|
|
||||||
|
|
||||||
context.crt_command = context.crt_params[0]
|
|
||||||
context.crt_params = context.crt_params[1:]
|
|
||||||
|
|
||||||
async for ret in self._execute(context, oper.children, oper):
|
|
||||||
yield ret
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found: # 如果下一个参数未在此节点的子节点中找到,则执行此节点或者报错
|
|
||||||
if operator is None:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandNotFoundError(context.crt_params[0]))
|
|
||||||
else:
|
|
||||||
if operator.lowest_privilege > context.privilege:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandPrivilegeError(operator.name))
|
|
||||||
else:
|
|
||||||
async for ret in operator.execute(context):
|
|
||||||
yield ret
|
|
||||||
|
|
||||||
async def execute(
|
|
||||||
self,
|
|
||||||
command_text: str,
|
|
||||||
query: core_entities.Query,
|
|
||||||
session: core_entities.Session,
|
|
||||||
) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
"""执行命令"""
|
|
||||||
|
|
||||||
privilege = 1
|
|
||||||
|
|
||||||
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
|
|
||||||
privilege = 2
|
|
||||||
|
|
||||||
ctx = entities.ExecuteContext(
|
|
||||||
query=query,
|
|
||||||
session=session,
|
|
||||||
command_text=command_text,
|
|
||||||
command='',
|
|
||||||
crt_command='',
|
|
||||||
params=command_text.split(' '),
|
|
||||||
crt_params=command_text.split(' '),
|
|
||||||
privilege=privilege,
|
|
||||||
)
|
|
||||||
|
|
||||||
async for ret in self._execute(ctx, self.cmd_list):
|
|
||||||
yield ret
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
import pydantic.v1 as pydantic
|
|
||||||
|
|
||||||
from ..core import entities as core_entities
|
|
||||||
from . import errors
|
|
||||||
from ..platform.types import message as platform_message
|
|
||||||
|
|
||||||
|
|
||||||
class CommandReturn(pydantic.BaseModel):
|
|
||||||
"""命令返回值"""
|
|
||||||
|
|
||||||
text: typing.Optional[str] = None
|
|
||||||
"""文本
|
|
||||||
"""
|
|
||||||
|
|
||||||
image: typing.Optional[platform_message.Image] = None
|
|
||||||
"""弃用"""
|
|
||||||
|
|
||||||
image_url: typing.Optional[str] = None
|
|
||||||
"""图片链接
|
|
||||||
"""
|
|
||||||
|
|
||||||
error: typing.Optional[errors.CommandError] = None
|
|
||||||
"""错误
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
|
|
||||||
class ExecuteContext(pydantic.BaseModel):
|
|
||||||
"""单次命令执行上下文"""
|
|
||||||
|
|
||||||
query: core_entities.Query
|
|
||||||
"""本次消息的请求对象"""
|
|
||||||
|
|
||||||
session: core_entities.Session
|
|
||||||
"""本次消息所属的会话对象"""
|
|
||||||
|
|
||||||
command_text: str
|
|
||||||
"""命令完整文本"""
|
|
||||||
|
|
||||||
command: str
|
|
||||||
"""命令名称"""
|
|
||||||
|
|
||||||
crt_command: str
|
|
||||||
"""当前命令
|
|
||||||
|
|
||||||
多级命令中crt_command为当前命令,command为根命令。
|
|
||||||
例如:!plugin on Webwlkr
|
|
||||||
处理到plugin时,command为plugin,crt_command为plugin
|
|
||||||
处理到on时,command为plugin,crt_command为on
|
|
||||||
"""
|
|
||||||
|
|
||||||
params: list[str]
|
|
||||||
"""命令参数
|
|
||||||
|
|
||||||
整个命令以空格分割后的参数列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
crt_params: list[str]
|
|
||||||
"""当前命令参数
|
|
||||||
|
|
||||||
多级命令中crt_params为当前命令参数,params为根命令参数。
|
|
||||||
例如:!plugin on Webwlkr
|
|
||||||
处理到plugin时,params为['on', 'Webwlkr'],crt_params为['on', 'Webwlkr']
|
|
||||||
处理到on时,params为['on', 'Webwlkr'],crt_params为['Webwlkr']
|
|
||||||
"""
|
|
||||||
|
|
||||||
privilege: int
|
|
||||||
"""发起人权限"""
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
class CommandError(Exception):
|
|
||||||
def __init__(self, message: str = None):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message
|
|
||||||
|
|
||||||
|
|
||||||
class CommandNotFoundError(CommandError):
|
|
||||||
def __init__(self, message: str = None):
|
|
||||||
super().__init__('未知命令: ' + message)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandPrivilegeError(CommandError):
|
|
||||||
def __init__(self, message: str = None):
|
|
||||||
super().__init__('权限不足: ' + message)
|
|
||||||
|
|
||||||
|
|
||||||
class ParamNotEnoughError(CommandError):
|
|
||||||
def __init__(self, message: str = None):
|
|
||||||
super().__init__('参数不足: ' + message)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandOperationError(CommandError):
|
|
||||||
def __init__(self, message: str = None):
|
|
||||||
super().__init__('操作失败: ' + message)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='cmd', help='显示命令列表', usage='!cmd\n!cmd <命令名称>')
|
|
||||||
class CmdOperator(operator.CommandOperator):
|
|
||||||
"""命令列表"""
|
|
||||||
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
"""执行"""
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
reply_str = '当前所有命令: \n\n'
|
|
||||||
|
|
||||||
for cmd in self.ap.cmd_mgr.cmd_list:
|
|
||||||
if cmd.parent_class is None:
|
|
||||||
reply_str += f'{cmd.name}: {cmd.help}\n'
|
|
||||||
|
|
||||||
reply_str += '\n使用 !cmd <命令名称> 查看命令的详细帮助'
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str.strip())
|
|
||||||
|
|
||||||
else:
|
|
||||||
cmd_name = context.crt_params[0]
|
|
||||||
|
|
||||||
cmd = None
|
|
||||||
|
|
||||||
for _cmd in self.ap.cmd_mgr.cmd_list:
|
|
||||||
if (cmd_name == _cmd.name or cmd_name in _cmd.alias) and (_cmd.parent_class is None):
|
|
||||||
cmd = _cmd
|
|
||||||
break
|
|
||||||
|
|
||||||
if cmd is None:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandNotFoundError(cmd_name))
|
|
||||||
else:
|
|
||||||
reply_str = f'{cmd.name}: {cmd.help}\n\n'
|
|
||||||
reply_str += f'使用方法: \n{cmd.usage}'
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str.strip())
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='del', help='删除当前会话的历史记录', usage='!del <序号>\n!del all')
|
|
||||||
class DelOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if context.session.conversations:
|
|
||||||
delete_index = 0
|
|
||||||
if len(context.crt_params) > 0:
|
|
||||||
try:
|
|
||||||
delete_index = int(context.crt_params[0])
|
|
||||||
except Exception:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('索引必须是整数'))
|
|
||||||
return
|
|
||||||
|
|
||||||
if delete_index < 0 or delete_index >= len(context.session.conversations):
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('索引超出范围'))
|
|
||||||
return
|
|
||||||
|
|
||||||
# 倒序
|
|
||||||
to_delete_index = len(context.session.conversations) - 1 - delete_index
|
|
||||||
|
|
||||||
if context.session.conversations[to_delete_index] == context.session.using_conversation:
|
|
||||||
context.session.using_conversation = None
|
|
||||||
|
|
||||||
del context.session.conversations[to_delete_index]
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=f'已删除对话: {delete_index}')
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='all', help='删除此会话的所有历史记录', parent_class=DelOperator)
|
|
||||||
class DelAllOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
context.session.conversations = []
|
|
||||||
context.session.using_conversation = None
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text='已删除所有对话')
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from typing import AsyncGenerator
|
|
||||||
|
|
||||||
from .. import operator, entities
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='func', help='查看所有已注册的内容函数', usage='!func')
|
|
||||||
class FuncOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
reply_str = '当前已启用的内容函数: \n\n'
|
|
||||||
|
|
||||||
index = 1
|
|
||||||
|
|
||||||
all_functions = await self.ap.tool_mgr.get_all_functions(
|
|
||||||
plugin_enabled=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for func in all_functions:
|
|
||||||
reply_str += '{}. {}:\n{}\n\n'.format(
|
|
||||||
index,
|
|
||||||
func.name,
|
|
||||||
func.description,
|
|
||||||
)
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='help', help='显示帮助', usage='!help\n!help <命令名称>')
|
|
||||||
class HelpOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
help = 'LangBot - 大语言模型原生即时通信机器人平台\n链接:https://langbot.app'
|
|
||||||
|
|
||||||
help += '\n发送命令 !cmd 可查看命令列表'
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=help)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='last', help='切换到前一个对话', usage='!last')
|
|
||||||
class LastOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if context.session.conversations:
|
|
||||||
# 找到当前会话的上一个会话
|
|
||||||
for index in range(len(context.session.conversations) - 1, -1, -1):
|
|
||||||
if context.session.conversations[index] == context.session.using_conversation:
|
|
||||||
if index == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('已经是第一个对话了'))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
context.session.using_conversation = context.session.conversations[index - 1]
|
|
||||||
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
yield entities.CommandReturn(
|
|
||||||
text=f'已切换到上一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].readable_str()}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='list', help='列出此会话中的所有历史对话', usage='!list\n!list <页码>')
|
|
||||||
class ListOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
page = 0
|
|
||||||
|
|
||||||
if len(context.crt_params) > 0:
|
|
||||||
try:
|
|
||||||
page = int(context.crt_params[0] - 1)
|
|
||||||
except Exception:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('页码应为整数'))
|
|
||||||
return
|
|
||||||
|
|
||||||
record_per_page = 10
|
|
||||||
|
|
||||||
content = ''
|
|
||||||
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
using_conv_index = 0
|
|
||||||
|
|
||||||
for conv in context.session.conversations[::-1]:
|
|
||||||
time_str = conv.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
if conv == context.session.using_conversation:
|
|
||||||
using_conv_index = index
|
|
||||||
|
|
||||||
if index >= page * record_per_page and index < (page + 1) * record_per_page:
|
|
||||||
content += (
|
|
||||||
f'{index} {time_str}: {conv.messages[0].readable_str() if len(conv.messages) > 0 else "无内容"}\n'
|
|
||||||
)
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
if content == '':
|
|
||||||
content = '无'
|
|
||||||
else:
|
|
||||||
if context.session.using_conversation is None:
|
|
||||||
content += '\n当前处于新会话'
|
|
||||||
else:
|
|
||||||
content += f'\n当前会话: {using_conv_index} {context.session.using_conversation.create_time.strftime("%Y-%m-%d %H:%M:%S")}: {context.session.using_conversation.messages[0].readable_str() if len(context.session.using_conversation.messages) > 0 else "无内容"}'
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=f'第 {page + 1} 页 (时间倒序):\n{content}')
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='next', help='切换到后一个对话', usage='!next')
|
|
||||||
class NextOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if context.session.conversations:
|
|
||||||
# 找到当前会话的下一个会话
|
|
||||||
for index in range(len(context.session.conversations)):
|
|
||||||
if context.session.conversations[index] == context.session.using_conversation:
|
|
||||||
if index == len(context.session.conversations) - 1:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('已经是最后一个对话了'))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
context.session.using_conversation = context.session.conversations[index + 1]
|
|
||||||
time_str = context.session.using_conversation.create_time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
yield entities.CommandReturn(
|
|
||||||
text=f'已切换到后一个对话: {index} {time_str}: {context.session.using_conversation.messages[0].content}'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
import typing
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(
|
|
||||||
name='plugin',
|
|
||||||
help='插件操作',
|
|
||||||
usage='!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>',
|
|
||||||
)
|
|
||||||
class PluginOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
plugin_list = self.ap.plugin_mgr.plugins()
|
|
||||||
reply_str = '所有插件({}):\n'.format(len(plugin_list))
|
|
||||||
idx = 0
|
|
||||||
for plugin in plugin_list:
|
|
||||||
reply_str += '\n#{} {} {}\n{}\nv{}\n作者: {}\n'.format(
|
|
||||||
(idx + 1),
|
|
||||||
plugin.plugin_name,
|
|
||||||
'[已禁用]' if not plugin.enabled else '',
|
|
||||||
plugin.plugin_description,
|
|
||||||
plugin.plugin_version,
|
|
||||||
plugin.plugin_author,
|
|
||||||
)
|
|
||||||
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str)
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='get', help='安装插件', privilege=2, parent_class=PluginOperator)
|
|
||||||
class PluginGetOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件仓库地址'))
|
|
||||||
else:
|
|
||||||
repo = context.crt_params[0]
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text='正在安装插件...')
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.plugin_mgr.install_plugin(repo)
|
|
||||||
yield entities.CommandReturn(text='插件安装成功,请重启程序以加载插件')
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件安装失败: ' + str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='update', help='更新插件', privilege=2, parent_class=PluginOperator)
|
|
||||||
class PluginUpdateOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
|
||||||
else:
|
|
||||||
plugin_name = context.crt_params[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
|
|
||||||
|
|
||||||
if plugin_container is not None:
|
|
||||||
yield entities.CommandReturn(text='正在更新插件...')
|
|
||||||
await self.ap.plugin_mgr.update_plugin(plugin_name)
|
|
||||||
yield entities.CommandReturn(text='插件更新成功,请重启程序以加载插件')
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: 未找到插件'))
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='all', help='更新所有插件', privilege=2, parent_class=PluginUpdateOperator)
|
|
||||||
class PluginUpdateAllOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
try:
|
|
||||||
plugins = [p.plugin_name for p in self.ap.plugin_mgr.plugins()]
|
|
||||||
|
|
||||||
if plugins:
|
|
||||||
yield entities.CommandReturn(text='正在更新插件...')
|
|
||||||
updated = []
|
|
||||||
try:
|
|
||||||
for plugin_name in plugins:
|
|
||||||
await self.ap.plugin_mgr.update_plugin(plugin_name)
|
|
||||||
updated.append(plugin_name)
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
|
||||||
yield entities.CommandReturn(text='已更新插件: {}'.format(', '.join(updated)))
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(text='没有可更新的插件')
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件更新失败: ' + str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='del', help='删除插件', privilege=2, parent_class=PluginOperator)
|
|
||||||
class PluginDelOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
|
||||||
else:
|
|
||||||
plugin_name = context.crt_params[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
plugin_container = self.ap.plugin_mgr.get_plugin_by_name(plugin_name)
|
|
||||||
|
|
||||||
if plugin_container is not None:
|
|
||||||
yield entities.CommandReturn(text='正在删除插件...')
|
|
||||||
await self.ap.plugin_mgr.uninstall_plugin(plugin_name)
|
|
||||||
yield entities.CommandReturn(text='插件删除成功,请重启程序以加载插件')
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: 未找到插件'))
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件删除失败: ' + str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='on', help='启用插件', privilege=2, parent_class=PluginOperator)
|
|
||||||
class PluginEnableOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
|
||||||
else:
|
|
||||||
plugin_name = context.crt_params[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, True):
|
|
||||||
yield entities.CommandReturn(text='已启用插件: {}'.format(plugin_name))
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(
|
|
||||||
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e)))
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='off', help='禁用插件', privilege=2, parent_class=PluginOperator)
|
|
||||||
class PluginDisableOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
if len(context.crt_params) == 0:
|
|
||||||
yield entities.CommandReturn(error=errors.ParamNotEnoughError('请提供插件名称'))
|
|
||||||
else:
|
|
||||||
plugin_name = context.crt_params[0]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if await self.ap.plugin_mgr.update_plugin_switch(plugin_name, False):
|
|
||||||
yield entities.CommandReturn(text='已禁用插件: {}'.format(plugin_name))
|
|
||||||
else:
|
|
||||||
yield entities.CommandReturn(
|
|
||||||
error=errors.CommandError('插件状态修改失败: 未找到插件 {}'.format(plugin_name))
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('插件状态修改失败: ' + str(e)))
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='prompt', help='查看当前对话的前文', usage='!prompt')
|
|
||||||
class PromptOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
"""执行"""
|
|
||||||
if context.session.using_conversation is None:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandOperationError('当前没有对话'))
|
|
||||||
else:
|
|
||||||
reply_str = '当前对话所有内容:\n\n'
|
|
||||||
|
|
||||||
for msg in context.session.using_conversation.messages:
|
|
||||||
reply_str += f'{msg.role}: {msg.content}\n'
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities, errors
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='resend', help='重发当前会话的最后一条消息', usage='!resend')
|
|
||||||
class ResendOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
# 回滚到最后一条用户message前
|
|
||||||
if context.session.using_conversation is None:
|
|
||||||
yield entities.CommandReturn(error=errors.CommandError('当前没有对话'))
|
|
||||||
else:
|
|
||||||
conv_msg = context.session.using_conversation.messages
|
|
||||||
|
|
||||||
# 倒序一直删到最后一条用户message
|
|
||||||
while len(conv_msg) > 0 and conv_msg[-1].role != 'user':
|
|
||||||
conv_msg.pop()
|
|
||||||
|
|
||||||
if len(conv_msg) > 0:
|
|
||||||
# 删除最后一条用户message
|
|
||||||
conv_msg.pop()
|
|
||||||
|
|
||||||
# 不重发了,提示用户已删除就行了
|
|
||||||
yield entities.CommandReturn(text='已删除最后一次请求记录')
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='reset', help='重置当前会话', usage='!reset')
|
|
||||||
class ResetOperator(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
"""执行"""
|
|
||||||
context.session.using_conversation = None
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text='已重置当前会话')
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='update', help='更新程序', usage='!update', privilege=2)
|
|
||||||
class UpdateCommand(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
yield entities.CommandReturn(text='不再支持通过命令更新,请查看 LangBot 文档。')
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .. import operator, entities
|
|
||||||
|
|
||||||
|
|
||||||
@operator.operator_class(name='version', help='显示版本信息', usage='!version')
|
|
||||||
class VersionCommand(operator.CommandOperator):
|
|
||||||
async def execute(self, context: entities.ExecuteContext) -> typing.AsyncGenerator[entities.CommandReturn, None]:
|
|
||||||
reply_str = f'当前版本: \n{self.ap.ver_mgr.get_current_version()}'
|
|
||||||
|
|
||||||
try:
|
|
||||||
if await self.ap.ver_mgr.is_new_version_available():
|
|
||||||
reply_str += '\n\n有新版本可用。'
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
yield entities.CommandReturn(text=reply_str.strip())
|
|
||||||
241
pkg/core/app.py
241
pkg/core/app.py
@@ -1,241 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..platform import botmgr as im_mgr
|
|
||||||
from ..provider.session import sessionmgr as llm_session_mgr
|
|
||||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
|
||||||
from ..provider.tools import toolmgr as llm_tool_mgr
|
|
||||||
from ..config import manager as config_mgr
|
|
||||||
from ..command import cmdmgr
|
|
||||||
from ..plugin import manager as plugin_mgr
|
|
||||||
from ..pipeline import pool
|
|
||||||
from ..pipeline import controller, pipelinemgr
|
|
||||||
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
|
|
||||||
from ..persistence import mgr as persistencemgr
|
|
||||||
from ..api.http.controller import main as http_controller
|
|
||||||
from ..api.http.service import user as user_service
|
|
||||||
from ..api.http.service import model as model_service
|
|
||||||
from ..api.http.service import pipeline as pipeline_service
|
|
||||||
from ..api.http.service import bot as bot_service
|
|
||||||
from ..discover import engine as discover_engine
|
|
||||||
from ..storage import mgr as storagemgr
|
|
||||||
from ..utils import logcache
|
|
||||||
from . import taskmgr
|
|
||||||
from . import entities as core_entities
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
"""运行时应用对象和上下文"""
|
|
||||||
|
|
||||||
event_loop: asyncio.AbstractEventLoop = None
|
|
||||||
|
|
||||||
# asyncio_tasks: list[asyncio.Task] = []
|
|
||||||
task_mgr: taskmgr.AsyncTaskManager = None
|
|
||||||
|
|
||||||
discover: discover_engine.ComponentDiscoveryEngine = None
|
|
||||||
|
|
||||||
platform_mgr: im_mgr.PlatformManager = None
|
|
||||||
|
|
||||||
cmd_mgr: cmdmgr.CommandManager = None
|
|
||||||
|
|
||||||
sess_mgr: llm_session_mgr.SessionManager = None
|
|
||||||
|
|
||||||
model_mgr: llm_model_mgr.ModelManager = None
|
|
||||||
|
|
||||||
# TODO 移动到 pipeline 里
|
|
||||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
|
||||||
|
|
||||||
# ======= 配置管理器 =======
|
|
||||||
|
|
||||||
command_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
pipeline_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
platform_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
provider_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
system_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
instance_config: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# ======= 元数据配置管理器 =======
|
|
||||||
|
|
||||||
sensitive_meta: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
pipeline_config_meta_trigger: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_safety: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_ai: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_output: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
plugin_mgr: plugin_mgr.PluginManager = None
|
|
||||||
|
|
||||||
query_pool: pool.QueryPool = None
|
|
||||||
|
|
||||||
ctrl: controller.Controller = None
|
|
||||||
|
|
||||||
pipeline_mgr: pipelinemgr.PipelineManager = None
|
|
||||||
|
|
||||||
ver_mgr: version_mgr.VersionManager = None
|
|
||||||
|
|
||||||
ann_mgr: announce_mgr.AnnouncementManager = None
|
|
||||||
|
|
||||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
|
||||||
|
|
||||||
logger: logging.Logger = None
|
|
||||||
|
|
||||||
persistence_mgr: persistencemgr.PersistenceManager = None
|
|
||||||
|
|
||||||
http_ctrl: http_controller.HTTPController = None
|
|
||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
|
||||||
|
|
||||||
storage_mgr: storagemgr.StorageMgr = None
|
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
|
||||||
|
|
||||||
model_service: model_service.ModelsService = None
|
|
||||||
|
|
||||||
pipeline_service: pipeline_service.PipelineService = None
|
|
||||||
|
|
||||||
bot_service: bot_service.BotService = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
try:
|
|
||||||
await self.plugin_mgr.initialize_plugins()
|
|
||||||
|
|
||||||
# 后续可能会允许动态重启其他任务
|
|
||||||
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
|
|
||||||
async def never_ending():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.platform_mgr.run(),
|
|
||||||
name='platform-manager',
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.ctrl.run(),
|
|
||||||
name='query-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.http_ctrl.run(),
|
|
||||||
name='http-api-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
never_ending(),
|
|
||||||
name='never-ending-task',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.print_web_access_info()
|
|
||||||
await self.task_mgr.wait_all()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'应用运行致命异常: {e}')
|
|
||||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
async def print_web_access_info(self):
|
|
||||||
"""打印访问 webui 的提示"""
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join('.', 'web/out')):
|
|
||||||
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh')
|
|
||||||
self.logger.warning(
|
|
||||||
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
host_ip = '127.0.0.1'
|
|
||||||
|
|
||||||
port = self.instance_config.data['api']['port']
|
|
||||||
|
|
||||||
tips = f"""
|
|
||||||
=======================================
|
|
||||||
✨ Access WebUI / 访问管理面板
|
|
||||||
|
|
||||||
🏠 Local Address: http://{host_ip}:{port}/
|
|
||||||
🌐 Public Address: http://<Your Public IP>:{port}/
|
|
||||||
|
|
||||||
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
|
||||||
=======================================
|
|
||||||
""".strip()
|
|
||||||
for line in tips.split('\n'):
|
|
||||||
self.logger.info(line)
|
|
||||||
|
|
||||||
async def reload(
|
|
||||||
self,
|
|
||||||
scope: core_entities.LifecycleControlScope,
|
|
||||||
):
|
|
||||||
match scope:
|
|
||||||
case core_entities.LifecycleControlScope.PLATFORM.value:
|
|
||||||
self.logger.info('执行热重载 scope=' + scope)
|
|
||||||
await self.platform_mgr.shutdown()
|
|
||||||
|
|
||||||
self.platform_mgr = im_mgr.PlatformManager(self)
|
|
||||||
|
|
||||||
await self.platform_mgr.initialize()
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.platform_mgr.run(),
|
|
||||||
name='platform-manager',
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
case core_entities.LifecycleControlScope.PLUGIN.value:
|
|
||||||
self.logger.info('执行热重载 scope=' + scope)
|
|
||||||
await self.plugin_mgr.destroy_plugins()
|
|
||||||
|
|
||||||
# 删除 sys.module 中所有的 plugins/* 下的模块
|
|
||||||
for mod in list(sys.modules.keys()):
|
|
||||||
if mod.startswith('plugins.'):
|
|
||||||
del sys.modules[mod]
|
|
||||||
|
|
||||||
self.plugin_mgr = plugin_mgr.PluginManager(self)
|
|
||||||
await self.plugin_mgr.initialize()
|
|
||||||
|
|
||||||
await self.plugin_mgr.initialize_plugins()
|
|
||||||
|
|
||||||
await self.plugin_mgr.load_plugins()
|
|
||||||
await self.plugin_mgr.initialize_plugins()
|
|
||||||
case core_entities.LifecycleControlScope.PROVIDER.value:
|
|
||||||
self.logger.info('执行热重载 scope=' + scope)
|
|
||||||
|
|
||||||
await self.tool_mgr.shutdown()
|
|
||||||
|
|
||||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(self)
|
|
||||||
await llm_model_mgr_inst.initialize()
|
|
||||||
self.model_mgr = llm_model_mgr_inst
|
|
||||||
|
|
||||||
llm_session_mgr_inst = llm_session_mgr.SessionManager(self)
|
|
||||||
await llm_session_mgr_inst.initialize()
|
|
||||||
self.sess_mgr = llm_session_mgr_inst
|
|
||||||
|
|
||||||
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(self)
|
|
||||||
await llm_tool_mgr_inst.initialize()
|
|
||||||
self.tool_mgr = llm_tool_mgr_inst
|
|
||||||
case _:
|
|
||||||
pass
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import enum
|
|
||||||
import typing
|
|
||||||
import datetime
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import pydantic.v1 as pydantic
|
|
||||||
|
|
||||||
from ..provider import entities as llm_entities
|
|
||||||
from ..provider.modelmgr import requester
|
|
||||||
from ..provider.tools import entities as tools_entities
|
|
||||||
from ..platform import adapter as msadapter
|
|
||||||
from ..platform.types import message as platform_message
|
|
||||||
from ..platform.types import events as platform_events
|
|
||||||
|
|
||||||
|
|
||||||
class LifecycleControlScope(enum.Enum):
|
|
||||||
APPLICATION = 'application'
|
|
||||||
PLATFORM = 'platform'
|
|
||||||
PLUGIN = 'plugin'
|
|
||||||
PROVIDER = 'provider'
|
|
||||||
|
|
||||||
|
|
||||||
class LauncherTypes(enum.Enum):
|
|
||||||
"""一个请求的发起者类型"""
|
|
||||||
|
|
||||||
PERSON = 'person'
|
|
||||||
"""私聊"""
|
|
||||||
|
|
||||||
GROUP = 'group'
|
|
||||||
"""群聊"""
|
|
||||||
|
|
||||||
|
|
||||||
class Query(pydantic.BaseModel):
|
|
||||||
"""一次请求的信息封装"""
|
|
||||||
|
|
||||||
query_id: int
|
|
||||||
"""请求ID,添加进请求池时生成"""
|
|
||||||
|
|
||||||
launcher_type: LauncherTypes
|
|
||||||
"""会话类型,platform处理阶段设置"""
|
|
||||||
|
|
||||||
launcher_id: typing.Union[int, str]
|
|
||||||
"""会话ID,platform处理阶段设置"""
|
|
||||||
|
|
||||||
sender_id: typing.Union[int, str]
|
|
||||||
"""发送者ID,platform处理阶段设置"""
|
|
||||||
|
|
||||||
message_event: platform_events.MessageEvent
|
|
||||||
"""事件,platform收到的原始事件"""
|
|
||||||
|
|
||||||
message_chain: platform_message.MessageChain
|
|
||||||
"""消息链,platform收到的原始消息链"""
|
|
||||||
|
|
||||||
bot_uuid: typing.Optional[str] = None
|
|
||||||
"""机器人UUID。"""
|
|
||||||
|
|
||||||
pipeline_uuid: typing.Optional[str] = None
|
|
||||||
"""流水线UUID。"""
|
|
||||||
|
|
||||||
pipeline_config: typing.Optional[dict[str, typing.Any]] = None
|
|
||||||
"""流水线配置,由 Pipeline 在运行开始时设置。"""
|
|
||||||
|
|
||||||
adapter: msadapter.MessagePlatformAdapter
|
|
||||||
"""消息平台适配器对象,单个app中可能启用了多个消息平台适配器,此对象表明发起此query的适配器"""
|
|
||||||
|
|
||||||
session: typing.Optional[Session] = None
|
|
||||||
"""会话对象,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
messages: typing.Optional[list[llm_entities.Message]] = []
|
|
||||||
"""历史消息列表,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
prompt: typing.Optional[llm_entities.Prompt] = None
|
|
||||||
"""情景预设内容,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
user_message: typing.Optional[llm_entities.Message] = None
|
|
||||||
"""此次请求的用户消息对象,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
variables: typing.Optional[dict[str, typing.Any]] = None
|
|
||||||
"""变量,由前置处理器阶段设置。在prompt中嵌入或由 Runner 传递到 LLMOps 平台。"""
|
|
||||||
|
|
||||||
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
|
|
||||||
"""使用的对话模型,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]] = None
|
|
||||||
"""使用的函数,由前置处理器阶段设置"""
|
|
||||||
|
|
||||||
resp_messages: (
|
|
||||||
typing.Optional[list[llm_entities.Message]] | typing.Optional[list[platform_message.MessageChain]]
|
|
||||||
) = []
|
|
||||||
"""由Process阶段生成的回复消息对象列表"""
|
|
||||||
|
|
||||||
resp_message_chain: typing.Optional[list[platform_message.MessageChain]] = None
|
|
||||||
"""回复消息链,从resp_messages包装而得"""
|
|
||||||
|
|
||||||
# ======= 内部保留 =======
|
|
||||||
current_stage: typing.Optional['pkg.pipeline.pipelinemgr.StageInstContainer'] = None
|
|
||||||
"""当前所处阶段"""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
# ========== 插件可调用的 API(请求 API) ==========
|
|
||||||
|
|
||||||
def set_variable(self, key: str, value: typing.Any):
|
|
||||||
"""设置变量"""
|
|
||||||
if self.variables is None:
|
|
||||||
self.variables = {}
|
|
||||||
self.variables[key] = value
|
|
||||||
|
|
||||||
def get_variable(self, key: str) -> typing.Any:
|
|
||||||
"""获取变量"""
|
|
||||||
if self.variables is None:
|
|
||||||
return None
|
|
||||||
return self.variables.get(key)
|
|
||||||
|
|
||||||
def get_variables(self) -> dict[str, typing.Any]:
|
|
||||||
"""获取所有变量"""
|
|
||||||
if self.variables is None:
|
|
||||||
return {}
|
|
||||||
return self.variables
|
|
||||||
|
|
||||||
|
|
||||||
class Conversation(pydantic.BaseModel):
|
|
||||||
"""对话,包含于 Session 中,一个 Session 可以有多个历史 Conversation,但只有一个当前使用的 Conversation"""
|
|
||||||
|
|
||||||
prompt: llm_entities.Prompt
|
|
||||||
|
|
||||||
messages: list[llm_entities.Message]
|
|
||||||
|
|
||||||
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
|
||||||
|
|
||||||
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
|
||||||
|
|
||||||
use_llm_model: typing.Optional[requester.RuntimeLLMModel] = None
|
|
||||||
|
|
||||||
use_funcs: typing.Optional[list[tools_entities.LLMFunction]]
|
|
||||||
|
|
||||||
uuid: typing.Optional[str] = None
|
|
||||||
"""该对话的 uuid,在创建时不会自动生成。而是当使用 Dify API 等由外部管理对话信息的服务时,用于绑定外部的会话。具体如何使用,取决于 Runner。"""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
|
|
||||||
|
|
||||||
class Session(pydantic.BaseModel):
|
|
||||||
"""会话,一个 Session 对应一个 {launcher_type.value}_{launcher_id}"""
|
|
||||||
|
|
||||||
launcher_type: LauncherTypes
|
|
||||||
|
|
||||||
launcher_id: typing.Union[int, str]
|
|
||||||
|
|
||||||
sender_id: typing.Optional[typing.Union[int, str]] = 0
|
|
||||||
|
|
||||||
use_prompt_name: typing.Optional[str] = 'default'
|
|
||||||
|
|
||||||
using_conversation: typing.Optional[Conversation] = None
|
|
||||||
|
|
||||||
conversations: typing.Optional[list[Conversation]] = pydantic.Field(default_factory=list)
|
|
||||||
|
|
||||||
create_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
|
||||||
|
|
||||||
update_time: typing.Optional[datetime.datetime] = pydantic.Field(default_factory=datetime.datetime.now)
|
|
||||||
|
|
||||||
semaphore: typing.Optional[asyncio.Semaphore] = None
|
|
||||||
"""当前会话的信号量,用于限制并发"""
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
arbitrary_types_allowed = True
|
|
||||||
@@ -1,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,108 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
from .. import stage, app
|
|
||||||
from ...utils import version, proxy, announce
|
|
||||||
from ...pipeline import pool, controller, pipelinemgr
|
|
||||||
from ...plugin import manager as plugin_mgr
|
|
||||||
from ...command import cmdmgr
|
|
||||||
from ...provider.session import sessionmgr as llm_session_mgr
|
|
||||||
from ...provider.modelmgr import modelmgr as llm_model_mgr
|
|
||||||
from ...provider.tools import toolmgr as llm_tool_mgr
|
|
||||||
from ...platform import botmgr as im_mgr
|
|
||||||
from ...persistence import mgr as persistencemgr
|
|
||||||
from ...api.http.controller import main as http_controller
|
|
||||||
from ...api.http.service import user as user_service
|
|
||||||
from ...api.http.service import model as model_service
|
|
||||||
from ...api.http.service import pipeline as pipeline_service
|
|
||||||
from ...api.http.service import bot as bot_service
|
|
||||||
from ...discover import engine as discover_engine
|
|
||||||
from ...storage import mgr as storagemgr
|
|
||||||
from ...utils import logcache
|
|
||||||
from .. import taskmgr
|
|
||||||
|
|
||||||
|
|
||||||
@stage.stage_class('BuildAppStage')
|
|
||||||
class BuildAppStage(stage.BootingStage):
|
|
||||||
"""构建应用阶段"""
|
|
||||||
|
|
||||||
async def run(self, ap: app.Application):
|
|
||||||
"""构建app对象的各个组件对象并初始化"""
|
|
||||||
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
|
|
||||||
|
|
||||||
discover = discover_engine.ComponentDiscoveryEngine(ap)
|
|
||||||
discover.discover_blueprint('components.yaml')
|
|
||||||
ap.discover = discover
|
|
||||||
|
|
||||||
proxy_mgr = proxy.ProxyManager(ap)
|
|
||||||
await proxy_mgr.initialize()
|
|
||||||
ap.proxy_mgr = proxy_mgr
|
|
||||||
|
|
||||||
ver_mgr = version.VersionManager(ap)
|
|
||||||
await ver_mgr.initialize()
|
|
||||||
ap.ver_mgr = ver_mgr
|
|
||||||
|
|
||||||
# 发送公告
|
|
||||||
ann_mgr = announce.AnnouncementManager(ap)
|
|
||||||
ap.ann_mgr = ann_mgr
|
|
||||||
|
|
||||||
ap.query_pool = pool.QueryPool()
|
|
||||||
|
|
||||||
log_cache = logcache.LogCache()
|
|
||||||
ap.log_cache = log_cache
|
|
||||||
|
|
||||||
storage_mgr_inst = storagemgr.StorageMgr(ap)
|
|
||||||
await storage_mgr_inst.initialize()
|
|
||||||
ap.storage_mgr = storage_mgr_inst
|
|
||||||
|
|
||||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
|
||||||
ap.persistence_mgr = persistence_mgr_inst
|
|
||||||
await persistence_mgr_inst.initialize()
|
|
||||||
|
|
||||||
plugin_mgr_inst = plugin_mgr.PluginManager(ap)
|
|
||||||
await plugin_mgr_inst.initialize()
|
|
||||||
ap.plugin_mgr = plugin_mgr_inst
|
|
||||||
await plugin_mgr_inst.load_plugins()
|
|
||||||
|
|
||||||
cmd_mgr_inst = cmdmgr.CommandManager(ap)
|
|
||||||
await cmd_mgr_inst.initialize()
|
|
||||||
ap.cmd_mgr = cmd_mgr_inst
|
|
||||||
|
|
||||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(ap)
|
|
||||||
await llm_model_mgr_inst.initialize()
|
|
||||||
ap.model_mgr = llm_model_mgr_inst
|
|
||||||
|
|
||||||
llm_session_mgr_inst = llm_session_mgr.SessionManager(ap)
|
|
||||||
await llm_session_mgr_inst.initialize()
|
|
||||||
ap.sess_mgr = llm_session_mgr_inst
|
|
||||||
|
|
||||||
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap)
|
|
||||||
await llm_tool_mgr_inst.initialize()
|
|
||||||
ap.tool_mgr = llm_tool_mgr_inst
|
|
||||||
|
|
||||||
im_mgr_inst = im_mgr.PlatformManager(ap=ap)
|
|
||||||
await im_mgr_inst.initialize()
|
|
||||||
ap.platform_mgr = im_mgr_inst
|
|
||||||
|
|
||||||
pipeline_mgr = pipelinemgr.PipelineManager(ap)
|
|
||||||
await pipeline_mgr.initialize()
|
|
||||||
ap.pipeline_mgr = pipeline_mgr
|
|
||||||
|
|
||||||
http_ctrl = http_controller.HTTPController(ap)
|
|
||||||
await http_ctrl.initialize()
|
|
||||||
ap.http_ctrl = http_ctrl
|
|
||||||
|
|
||||||
user_service_inst = user_service.UserService(ap)
|
|
||||||
ap.user_service = user_service_inst
|
|
||||||
|
|
||||||
model_service_inst = model_service.ModelsService(ap)
|
|
||||||
ap.model_service = model_service_inst
|
|
||||||
|
|
||||||
pipeline_service_inst = pipeline_service.PipelineService(ap)
|
|
||||||
ap.pipeline_service = pipeline_service_inst
|
|
||||||
|
|
||||||
bot_service_inst = bot_service.BotService(ap)
|
|
||||||
ap.bot_service = bot_service_inst
|
|
||||||
|
|
||||||
ctrl = controller.Controller(ap)
|
|
||||||
ap.ctrl = ctrl
|
|
||||||
@@ -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):
|
|
||||||
"""加载配置文件阶段"""
|
|
||||||
|
|
||||||
async def run(self, ap: app.Application):
|
|
||||||
"""启动"""
|
|
||||||
|
|
||||||
# ======= 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,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,14 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
|
|
||||||
|
|
||||||
from .. import database
|
|
||||||
|
|
||||||
|
|
||||||
@database.manager_class('sqlite')
|
|
||||||
class SQLiteDatabaseManager(database.BaseDatabaseManager):
|
|
||||||
"""SQLite 数据库管理类"""
|
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
sqlite_path = 'data/langbot.db'
|
|
||||||
self.engine = sqlalchemy_asyncio.create_async_engine(f'sqlite+aiosqlite:///{sqlite_path}')
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
from .. import migration
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ...entity.persistence import pipeline as persistence_pipeline
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class(2)
|
|
||||||
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
|
||||||
"""引用消息合并配置"""
|
|
||||||
|
|
||||||
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 '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):
|
|
||||||
"""降级"""
|
|
||||||
pass
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 内容过滤器的抽象类
|
|
||||||
from __future__ import annotations
|
|
||||||
import abc
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from ...core import app, entities as core_entities
|
|
||||||
from . import entities
|
|
||||||
|
|
||||||
|
|
||||||
preregistered_filters: list[typing.Type[ContentFilter]] = []
|
|
||||||
|
|
||||||
|
|
||||||
def filter_class(
|
|
||||||
name: str,
|
|
||||||
) -> typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]:
|
|
||||||
"""内容过滤器类装饰器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): 过滤器名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
typing.Callable[[typing.Type[ContentFilter]], typing.Type[ContentFilter]]: 装饰器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(cls: typing.Type[ContentFilter]) -> typing.Type[ContentFilter]:
|
|
||||||
assert issubclass(cls, ContentFilter)
|
|
||||||
|
|
||||||
cls.name = name
|
|
||||||
|
|
||||||
preregistered_filters.append(cls)
|
|
||||||
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class ContentFilter(metaclass=abc.ABCMeta):
|
|
||||||
"""内容过滤器抽象类"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
@property
|
|
||||||
def enable_stages(self):
|
|
||||||
"""启用的阶段
|
|
||||||
|
|
||||||
默认为消息请求AI前后的两个阶段。
|
|
||||||
|
|
||||||
entity.EnableStage.PRE: 消息请求AI前,此时需要检查的内容是用户的输入消息。
|
|
||||||
entity.EnableStage.POST: 消息请求AI后,此时需要检查的内容是AI的回复消息。
|
|
||||||
"""
|
|
||||||
return [entities.EnableStage.PRE, entities.EnableStage.POST]
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
"""初始化过滤器"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def process(self, query: core_entities.Query, message: str = None, image_url=None) -> entities.FilterResult:
|
|
||||||
"""处理消息
|
|
||||||
|
|
||||||
分为前后阶段,具体取决于 enable_stages 的值。
|
|
||||||
对于内容过滤器来说,不需要考虑消息所处的阶段,只需要检查消息内容即可。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message (str): 需要检查的内容
|
|
||||||
image_url (str): 要检查的图片的 URL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
entities.FilterResult: 过滤结果,具体内容请查看 entities.FilterResult 类的文档
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user