mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-07 10:07:15 +00:00
Compare commits
725 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59538fd5cd | |||
| 235b33c335 | |||
| c588f9dfc1 | |||
| d770de7f7f | |||
| cc7a13158e | |||
| 7de7c7f714 | |||
| 85923e3d7b | |||
| c7c6c5dc51 | |||
| b056646696 | |||
| 096ec1a8ce | |||
| 2618e06492 | |||
| 5c8c0eb17b | |||
| ccc51522cf | |||
| 6a99a83f2d | |||
| ebab5343cf | |||
| b97cc800d3 | |||
| 48905ea080 | |||
| ddb77fc43c | |||
| 5b2826fa49 | |||
| 20636ac432 | |||
| af42602547 | |||
| 53b20e2b13 | |||
| 1242dc2d21 | |||
| 04628d93cb | |||
| 9c22a1521c | |||
| c8d5039580 | |||
| 85d8d9304e | |||
| 76471af179 | |||
| 59b2a7cd51 | |||
| a43978ff24 | |||
| e3417dd20b | |||
| 2982e7c553 | |||
| e1e14e9269 | |||
| 1c128a1524 | |||
| 8ad1203fd5 | |||
| 144bec371c | |||
| 74a18191dd | |||
| a15c98eb06 | |||
| cbe17cde6c | |||
| 876e8bf804 | |||
| b3848c9d05 | |||
| 85743cc75f | |||
| c689b10c0d | |||
| 812b1fff4c | |||
| 9daf22d661 | |||
| 42a2c70b14 | |||
| 64ed6d994b | |||
| 2ff854f79a | |||
| 52c096ea4c | |||
| eda80030b5 | |||
| dfbd176e42 | |||
| 6ddd24ae68 | |||
| a2cdbb621b | |||
| b92d54254b | |||
| d22fa82d7c | |||
| e9dd584792 | |||
| 91906d73be | |||
| acfac42107 | |||
| 492827ea75 | |||
| 4538fca901 | |||
| b02c9517f6 | |||
| 511b5a7bf4 | |||
| 65fbf4db59 | |||
| 3d5b70cc5d | |||
| 83623f6afe | |||
| a020ca680f | |||
| 3a2edf9753 | |||
| 5fe63ce822 | |||
| 6b15a732e4 | |||
| a1e6eccdeb | |||
| b3c6de2072 | |||
| 4e45886647 | |||
| f592656680 | |||
| e9db858dcc | |||
| 2d6faf9d5e | |||
| d4699547e9 | |||
| 716d7aca94 | |||
| b3c00fe6da | |||
| f4a6edf7ec | |||
| f390980d0a | |||
| 1ae5aacc00 | |||
| e9fe2f2d43 | |||
| 27be09ab15 | |||
| 1ef4507d9a | |||
| 2e7978317c | |||
| b7d8332cb0 | |||
| 7fe3eedeea | |||
| b6fde30aa7 | |||
| 5bfa38cbf2 | |||
| a97d2040bb | |||
| a2c6c8201b | |||
| 672abfe95d | |||
| 9ecb587ac0 | |||
| 7965d333ac | |||
| f7300f1473 | |||
| 2b6dcfe9c7 | |||
| dd96da895c | |||
| bca710dbd4 | |||
| 47ade18596 | |||
| 733c9cdf16 | |||
| bbc508d42f | |||
| 0551d22689 | |||
| 53d4edb609 | |||
| f897987ac1 | |||
| 8e558ad3a1 | |||
| 47fe9bde03 | |||
| 5c3a619e2d | |||
| e223edeb45 | |||
| d2c3146334 | |||
| 7d9c8e3065 | |||
| f12ed81e1e | |||
| 6d4d19b6d7 | |||
| 07b90f12a2 | |||
| fd896c6974 | |||
| 1fbfa868fb | |||
| ad05819c2e | |||
| 0c6f71738c | |||
| af451e7006 | |||
| 59f20bcc73 | |||
| 7eca3cdfca | |||
| c40354f838 | |||
| 21a5b4658a | |||
| 073acaa053 | |||
| 38759b229d | |||
| efe32e34ae | |||
| 46db4de11a | |||
| 170a6756f4 | |||
| 7330732f62 | |||
| b08e5ca09a | |||
| dff80a0c0a | |||
| f54ae4b91c | |||
| e5b3cced1f | |||
| 417cca016c | |||
| cb0cc4d06a | |||
| cb0bb44db8 | |||
| 101e04db6d | |||
| b79edda3a7 | |||
| a20d3d11e5 | |||
| 3b4c455813 | |||
| c967a2aa82 | |||
| 79cc6da96f | |||
| fee7d48dc3 | |||
| 8811fb647f | |||
| 37b017459d | |||
| 4889a3881b | |||
| fe4f95b9a3 | |||
| 6609bebeec | |||
| a2817f6524 | |||
| b9560b26ff | |||
| 1ad7071aa0 | |||
| 96b041846d | |||
| 4054ba2a76 | |||
| 192b69b0fb | |||
| 543fbd8ca0 | |||
| c7cb42bd79 | |||
| 804448b6cd | |||
| 5d4e40459f | |||
| b5c43cc113 | |||
| 8ebfcd963a | |||
| 127198675e | |||
| 44fb188994 | |||
| 265385a563 | |||
| 253cc6cbea | |||
| 894709d577 | |||
| f99d3022e8 | |||
| 6823069103 | |||
| 699545a196 | |||
| 5c5614667a | |||
| f0061817ea | |||
| 688202e7d1 | |||
| d46b762d03 | |||
| 0963fd5443 | |||
| 6471770737 | |||
| 314b7d15bb | |||
| c758908745 | |||
| 313d553271 | |||
| bb7db53447 | |||
| 767137aaa0 | |||
| acb2ce6a40 | |||
| 67784708d6 | |||
| 1bd9c334aa | |||
| 17bbc8bf10 | |||
| 4a4c0921a4 | |||
| e425cf079a | |||
| 245e798b79 | |||
| 27fdccce16 | |||
| 484643c0ee | |||
| ec61459619 | |||
| 66ef744447 | |||
| 10d3a9cc92 | |||
| 885320e9ae | |||
| ed02ac4710 | |||
| e4841edbaf | |||
| ef7a06b0db | |||
| 6fe20c1812 | |||
| 9e8c8f79df | |||
| 01d06898fb | |||
| 0a669c7016 | |||
| 27c0d344bf | |||
| c088dc114f | |||
| 37c74b0622 | |||
| b251fc4b89 | |||
| 075c85e2bc | |||
| 62b63ca2ca | |||
| 3680a80248 | |||
| 6713b57d01 | |||
| ea13ef87f2 | |||
| 59bd581e88 | |||
| cba83a62e8 | |||
| f412127fb0 | |||
| 5273bbb23f | |||
| 0ceab3f6a5 | |||
| aedc097188 | |||
| 18b27dd9ef | |||
| 3f50a56623 | |||
| c9f7911efe | |||
| 75fdfe6806 | |||
| eb9f38b102 | |||
| fc40d3c949 | |||
| d176a448e0 | |||
| ada4c30f85 | |||
| 32c9eaff45 | |||
| 9706ee2d53 | |||
| e7c9bc69d3 | |||
| 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 |
@@ -0,0 +1,8 @@
|
|||||||
|
.github
|
||||||
|
.venv
|
||||||
|
.vscode
|
||||||
|
.data
|
||||||
|
.temp
|
||||||
|
web/.next
|
||||||
|
web/node_modules
|
||||||
|
web/.env
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: 漏洞反馈
|
name: 漏洞反馈
|
||||||
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://link.langbot.app/zh/docs/network
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -10,6 +10,15 @@ body:
|
|||||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 部署版本
|
||||||
|
description: 请选择您使用的 LangBot 部署版本。
|
||||||
|
options:
|
||||||
|
- 社区版
|
||||||
|
- 云服务
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 异常情况
|
label: 异常情况
|
||||||
@@ -19,7 +28,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 复现步骤
|
label: 复现步骤
|
||||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果涉及 Dify、n8n、Langflow 等外部平台,请提供应用的导出文件(如 Dify 应用的 DSL),我们将更快回复您。**
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://docs.langbot.app/en/workshop/network-details.html
|
description: Report bugs or vulnerabilities using this template. For container network connection issues, refer to the documentation https://link.langbot.app/en/docs/network
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug?"]
|
labels: ["bug?"]
|
||||||
body:
|
body:
|
||||||
@@ -10,6 +10,15 @@ body:
|
|||||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Deployment version
|
||||||
|
description: Please select the LangBot deployment version you are using.
|
||||||
|
options:
|
||||||
|
- Community Edition
|
||||||
|
- Cloud Service
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Exception
|
label: Exception
|
||||||
@@ -19,7 +28,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Reproduction steps
|
label: Reproduction steps
|
||||||
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
|
description: How to reproduce this problem, the more detailed the better; the more information you provide, the faster we will solve the problem.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -10,6 +21,7 @@
|
|||||||
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
*请在方括号间写`x`以打勾 / Please tick the box with `x`*
|
||||||
|
|
||||||
- [ ] 阅读仓库[贡献指引](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)?
|
- [ ] 阅读仓库[贡献指引](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)?
|
||||||
|
- [ ] 我已签署或将在机器人提示后签署 [CLA](https://github.com/langbot-app/LangBot/blob/master/CLA.md)。 / I have signed, or will sign when prompted by the bot, the [CLA](https://github.com/langbot-app/LangBot/blob/master/CLA.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.
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,10 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
jobs:
|
jobs:
|
||||||
prepare:
|
publish-docker-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Prepare build metadata
|
name: Build image
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.check_version.outputs.version }}
|
|
||||||
is_prerelease: ${{ github.event.release.prerelease }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -39,81 +35,13 @@ jobs:
|
|||||||
echo $GITHUB_REF
|
echo $GITHUB_REF
|
||||||
echo ::set-output name=version::${GITHUB_REF}
|
echo ::set-output name=version::${GITHUB_REF}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-images:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: prepare
|
|
||||||
name: Build ${{ matrix.platform }} image
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [linux/amd64, linux/arm64]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Set platform tag
|
|
||||||
id: platform_tag
|
|
||||||
run: |
|
|
||||||
# Convert platform to tag suffix (e.g., linux/amd64 -> amd64)
|
|
||||||
PLATFORM_TAG=$(echo ${{ matrix.platform }} | sed 's/linux\///g')
|
|
||||||
echo ::set-output name=tag::${PLATFORM_TAG}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Registry
|
- name: Login to Registry
|
||||||
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: Build and cache
|
run: docker buildx create --name mybuilder --use
|
||||||
run: |
|
- name: Build for Release # only relase, exlude pre-release
|
||||||
docker buildx build \
|
if: ${{ github.event.release.prerelease == false }}
|
||||||
--platform ${{ matrix.platform }} \
|
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
|
||||||
--cache-to type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }},mode=max \
|
- name: Build for Pre-release # no update for latest tag
|
||||||
--cache-from type=registry,ref=rockchin/langbot:cache-${{ steps.platform_tag.outputs.tag }} \
|
if: ${{ github.event.release.prerelease == true }}
|
||||||
-t rockchin/langbot:${{ needs.prepare.outputs.version }} \
|
run: docker buildx build --platform linux/arm64,linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push
|
||||||
.
|
|
||||||
|
|
||||||
push-multiarch:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [prepare, build-images]
|
|
||||||
name: Build and push multi-arch images
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push for Release
|
|
||||||
if: ${{ needs.prepare.outputs.is_prerelease == 'false' }}
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--cache-from type=registry,ref=rockchin/langbot:cache-amd64 \
|
|
||||||
--cache-from type=registry,ref=rockchin/langbot:cache-arm64 \
|
|
||||||
-t rockchin/langbot:${{ needs.prepare.outputs.version }} \
|
|
||||||
-t rockchin/langbot:latest \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
|
|
||||||
- name: Build and push for Pre-release
|
|
||||||
if: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
|
|
||||||
run: |
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
--cache-from type=registry,ref=rockchin/langbot:cache-amd64 \
|
|
||||||
--cache-from type=registry,ref=rockchin/langbot:cache-arm64 \
|
|
||||||
-t rockchin/langbot:${{ needs.prepare.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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: "CLA Assistant"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, closed, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: write # re-run the failed CLA check after signing
|
||||||
|
contents: read # signatures are stored in the remote langbot-app/cla repo
|
||||||
|
pull-requests: write # post guidance comments, lock PR after merge
|
||||||
|
statuses: write # set the commit status
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
CLAAssistant:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "CLA Assistant"
|
||||||
|
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||||
|
# Upstream repo was archived in 2026-03; pin to the v2.6.1 commit SHA.
|
||||||
|
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# repo-scope PAT with write access to langbot-app/cla
|
||||||
|
PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_PAT }}
|
||||||
|
with:
|
||||||
|
path-to-document: 'https://github.com/langbot-app/LangBot/blob/master/CLA.md'
|
||||||
|
remote-organization-name: 'langbot-app'
|
||||||
|
remote-repository-name: 'cla'
|
||||||
|
path-to-signatures: 'signatures/version1/cla.json'
|
||||||
|
branch: 'main'
|
||||||
|
allowlist: 'dependabot[bot],github-actions[bot],devin-ai-integration[bot],Copilot,renovate[bot],bot*'
|
||||||
|
custom-notsigned-prcomment: |
|
||||||
|
Thank you for your contribution! :heart: Before we can merge this pull request, we need you to sign the [LangBot Contributor License Agreement (CLA)](https://github.com/langbot-app/LangBot/blob/master/CLA.md). You keep full copyright of your code — the CLA grants us a license to use and distribute your contribution. Signing takes 10 seconds and covers all repositories in this organization, permanently.
|
||||||
|
|
||||||
|
感谢您的贡献!合并前请阅读并签署[贡献者许可协议(CLA)](https://github.com/langbot-app/LangBot/blob/master/CLA.md)。您保留代码的全部版权,签署仅需回复下方指定内容,一次签署对本组织全部仓库永久有效。
|
||||||
|
custom-allsigned-prcomment: 'All contributors have signed the CLA. :white_check_mark: 所有贡献者均已签署 CLA。'
|
||||||
|
lock-pullrequest-aftermerge: true
|
||||||
|
# SECURITY: this workflow runs on pull_request_target (it holds secrets and has
|
||||||
|
# write access to the base repository). NEVER add an actions/checkout step that
|
||||||
|
# checks out the PR's code here.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Frontend Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
paths:
|
||||||
|
- 'web/**'
|
||||||
|
- '.github/workflows/frontend-tests.yml'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'web/**'
|
||||||
|
- '.github/workflows/frontend-tests.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
playwright-smoke:
|
||||||
|
name: Playwright Smoke
|
||||||
|
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: 8.9.2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: web
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
working-directory: web
|
||||||
|
run: pnpm exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run Playwright smoke tests
|
||||||
|
working-directory: web
|
||||||
|
run: pnpm test:e2e
|
||||||
@@ -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/langbot/ tests/ --output-format=concise
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -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 }}
|
||||||
+150
-28
@@ -4,29 +4,29 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, ready_for_review, synchronize]
|
types: [opened, ready_for_review, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- 'pkg/**'
|
- 'src/langbot/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- '.github/workflows/run-tests.yml'
|
- '.github/workflows/run-tests.yml'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
- 'uv.lock'
|
||||||
- 'run_tests.sh'
|
- 'run_tests.sh'
|
||||||
|
- 'scripts/test-*.sh'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
paths:
|
- 'feat/**'
|
||||||
- 'pkg/**'
|
# No path filter on push: every push to the branches above runs the
|
||||||
- 'tests/**'
|
# full unit-test suite. feat/** branches in particular must be tested
|
||||||
- '.github/workflows/run-tests.yml'
|
# on every push (they accumulate large changes before a PR exists).
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'run_tests.sh'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run Unit Tests
|
name: Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.10', '3.11', '3.12']
|
python-version: ['3.11', '3.12', '3.13']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -39,28 +39,13 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
run: |
|
uses: astral-sh/setup-uv@v4
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
||||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: uv sync --dev
|
||||||
uv sync --dev
|
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit + smoke tests
|
||||||
run: |
|
run: uv run pytest tests/unit_tests/ tests/smoke/ -q --tb=short
|
||||||
bash run_tests.sh
|
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
if: matrix.python-version == '3.12'
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
files: ./coverage.xml
|
|
||||||
flags: unit-tests
|
|
||||||
name: unit-tests-coverage
|
|
||||||
fail_ci_if_error: false
|
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
- name: Test Summary
|
- name: Test Summary
|
||||||
if: always()
|
if: always()
|
||||||
@@ -69,3 +54,140 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
|
echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
integration:
|
||||||
|
name: Fast Integration Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Run fast integration tests
|
||||||
|
run: uv run pytest tests/integration/ -m "not slow" -q --tb=short
|
||||||
|
|
||||||
|
- name: Integration Test Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Integration Tests Results" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
e2e:
|
||||||
|
name: E2E Startup 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 E2E startup tests
|
||||||
|
run: uv run pytest tests/e2e -q --tb=short
|
||||||
|
|
||||||
|
- name: E2E Test Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## E2E Startup Test Results" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
box-integration:
|
||||||
|
name: Box 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: Check Docker runtime
|
||||||
|
run: docker info
|
||||||
|
|
||||||
|
- name: Run Box integration tests
|
||||||
|
run: uv run pytest tests/integration_tests -q --tb=short
|
||||||
|
|
||||||
|
- name: Box Integration Test Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Box Integration Test 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
|
||||||
|
|||||||
@@ -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
|
||||||
+11
-1
@@ -42,8 +42,18 @@ botpy.log*
|
|||||||
test.py
|
test.py
|
||||||
/web_ui
|
/web_ui
|
||||||
.venv/
|
.venv/
|
||||||
uv.lock
|
|
||||||
/test
|
/test
|
||||||
plugins.bak
|
plugins.bak
|
||||||
coverage.xml
|
coverage.xml
|
||||||
.coverage
|
.coverage
|
||||||
|
src/langbot/web/
|
||||||
|
testsdk/
|
||||||
|
.qa/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/dist
|
||||||
|
/build
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Next.js build cache (legacy)
|
||||||
|
web/.next/
|
||||||
|
|||||||
@@ -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,79 +1,108 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
This file is for guiding code agents (like Claude Code, GitHub Copilot, OpenAI Codex, etc.) to work in LangBot project.
|
This file guides code agents working in the LangBot main repository. `CLAUDE.md` is a symlink to this file.
|
||||||
|
|
||||||
## Project Overview
|
Read `ARCHITECTURE.md` before non-trivial backend, frontend, runtime, plugin, Box, MCP, persistence, or cross-repo SDK changes. This file is the working checklist; `ARCHITECTURE.md` is the system map.
|
||||||
|
|
||||||
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.
|
## Quick Facts
|
||||||
|
|
||||||
LangBot has a comprehensive frontend, all operations can be performed through the frontend. The project splited into these major parts:
|
- Python backend: `>=3.11,<4.0`, dependencies managed by `uv`.
|
||||||
|
- Frontend: `web/` is Vite + React Router 7 + shadcn/ui + Tailwind, managed by `pnpm`.
|
||||||
|
- Backend framework: Quart served by Hypercorn on `api.port`, default `5300`.
|
||||||
|
- Frontend dev server: `web/` on `3000`, with `VITE_API_BASE_URL` pointing at the backend.
|
||||||
|
- Plugin/Box/runtime contracts live in sibling repo `langbot-plugin-sdk`, pinned as `langbot-plugin` in `pyproject.toml`.
|
||||||
|
|
||||||
- `./pkg`: The core python package of the project backend.
|
## Essential Commands
|
||||||
- `./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
|
```bash
|
||||||
pip install uv
|
|
||||||
uv sync --dev
|
uv sync --dev
|
||||||
```
|
|
||||||
|
|
||||||
Start the backend and run the project in development mode.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run main.py
|
uv run main.py
|
||||||
```
|
uv run pre-commit install
|
||||||
|
|
||||||
Then you can access the project at `http://127.0.0.1:5300`.
|
|
||||||
|
|
||||||
## Frontend Development
|
|
||||||
|
|
||||||
We use `pnpm` to manage dependencies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
cd web
|
||||||
cp .env.example .env
|
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm dev
|
pnpm dev
|
||||||
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can access the project at `http://127.0.0.1:3000`.
|
Useful focused tests:
|
||||||
|
|
||||||
## Plugin System Architecture
|
```bash
|
||||||
|
uv run pytest tests/unit_tests -q
|
||||||
|
uv run pytest tests/integration -q
|
||||||
|
uv run pytest tests/integration/persistence -q
|
||||||
|
uv run pytest tests/manual/mcp_smoke.py
|
||||||
|
|
||||||
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.
|
cd web
|
||||||
|
pnpm lint
|
||||||
|
pnpm test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
Run the narrowest useful test first, then broader checks when confidence is needed.
|
||||||
|
|
||||||
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.
|
## Where to Look
|
||||||
|
|
||||||
> 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.
|
- Architecture map: `ARCHITECTURE.md`.
|
||||||
|
- Dev environment guide: https://docs.langbot.app/zh/develop/dev-config.
|
||||||
|
- Plugin runtime / CLI / SDK debugging: https://docs.langbot.app/zh/develop/plugin-runtime.
|
||||||
|
- API-key auth: `docs/API_KEY_AUTH.md`.
|
||||||
|
- Box deep-dive notes: `docs/review/box-architecture.md` and related files.
|
||||||
|
- In-repo skills: `skills/` is the single source of truth for LangBot agent skills.
|
||||||
|
- SDK repo: `../langbot-plugin-sdk/` when changing shared entities, plugin APIs, action protocol, `lbp rt`, or `lbp box`.
|
||||||
|
|
||||||
## Some Development Tips and Standards
|
## Cross-Repo SDK Work
|
||||||
|
|
||||||
- LangBot is a global project, any comments in code should be in English, and user experience should be considered in all aspects.
|
When changing SDK contracts used by LangBot:
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Some Principles
|
```bash
|
||||||
|
# from langbot-plugin-sdk, with LangBot's .venv active
|
||||||
|
uv pip install .
|
||||||
|
|
||||||
|
# from LangBot, preserve the locally installed SDK
|
||||||
|
uv run --no-sync main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For standalone runtime debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in langbot-plugin-sdk
|
||||||
|
uv run --no-sync lbp rt
|
||||||
|
uv run --no-sync lbp box
|
||||||
|
|
||||||
|
# in LangBot
|
||||||
|
uv run --no-sync main.py --standalone-runtime
|
||||||
|
uv run --no-sync main.py --standalone-box
|
||||||
|
```
|
||||||
|
|
||||||
|
Config keys to verify in `data/config.yaml` / `src/langbot/templates/config.yaml`:
|
||||||
|
|
||||||
|
- Plugin runtime: `plugin.runtime_ws_url`, default Docker host `langbot_plugin_runtime:5400/control/ws`.
|
||||||
|
- Box runtime: `box.enabled`, `box.backend`, `box.runtime.endpoint`, Docker host `langbot_box:5410`.
|
||||||
|
- API/MCP auth: `api.global_api_key`.
|
||||||
|
|
||||||
|
## Change Rules
|
||||||
|
|
||||||
|
- HTTP API changes that should be agent-accessible must update the matching MCP tool in `src/langbot/pkg/api/mcp/server.py` and the relevant skill under `skills/` in the same pass.
|
||||||
|
- New schema changes use Alembic under `src/langbot/pkg/persistence/alembic/versions/`; do not add legacy `dbmXXX` migrations.
|
||||||
|
- New platform behavior belongs in platform adapters only for platform translation; pipeline/business logic belongs in `pkg/pipeline/` or services.
|
||||||
|
- User-facing strings must support i18n (`en_US`, `zh_Hans`; include `ja_JP` where the repo already does).
|
||||||
|
- Code comments and docstrings must be English.
|
||||||
|
- Keep compatibility and security in mind; LangBot is used in both self-hosted/community and toB deployments.
|
||||||
|
- Commit message format: `<type>(<scope>): <subject>`.
|
||||||
|
|
||||||
|
## Runtime Pitfalls
|
||||||
|
|
||||||
|
- Local stdio Plugin Runtime disconnects do not auto-reconnect; restart LangBot if that path breaks.
|
||||||
|
- Orphan runtime processes on `5400`/`5401` commonly break plugin debugging.
|
||||||
|
- Use `uv run --no-sync` after locally installing the SDK, or `uv` may restore the pinned package.
|
||||||
|
- A false Box “no backend” often means Docker is running but the current user lacks Docker socket permission.
|
||||||
|
- Do not confuse external MCP servers LangBot connects to (`pkg/provider/tools/loaders/mcp.py`) with LangBot's own `/mcp` server (`pkg/api/mcp/`).
|
||||||
|
- `CLAUDE.md` is a symlink to this file; edit `AGENTS.md`, not the symlink.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
- Keep it simple, stupid.
|
- Keep it simple, stupid.
|
||||||
- Entities should not be multiplied unnecessarily
|
- Entities should not be multiplied unnecessarily.
|
||||||
- 八荣八耻
|
- 八荣八耻
|
||||||
|
|
||||||
以瞎猜接口为耻,以认真查询为荣。
|
以瞎猜接口为耻,以认真查询为荣。
|
||||||
@@ -83,4 +112,4 @@ Plugin Runtime automatically starts each installed plugin and interacts through
|
|||||||
以跳过验证为耻,以主动测试为荣。
|
以跳过验证为耻,以主动测试为荣。
|
||||||
以破坏架构为耻,以遵循规范为荣。
|
以破坏架构为耻,以遵循规范为荣。
|
||||||
以假装理解为耻,以诚实无知为荣。
|
以假装理解为耻,以诚实无知为荣。
|
||||||
以盲目修改为耻,以谨慎重构为荣。
|
以盲目修改为耻,以谨慎重构为荣。
|
||||||
|
|||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document is a map of LangBot's moving parts. It is intentionally more stable than a feature guide and more concrete than the README: when you need to change behavior, start here, then follow the file references into the code.
|
||||||
|
|
||||||
|
For agent-specific working rules, see `AGENTS.md`. For plugin-runtime and Box-runtime implementation details, also read the sibling SDK repo: [`langbot-plugin-sdk`](https://github.com/langbot-app/langbot-plugin-sdk).
|
||||||
|
|
||||||
|
## What LangBot Is
|
||||||
|
|
||||||
|
LangBot is an open-source platform for building production IM bots backed by LLMs, agents, RAG, plugins, MCP tools, and a web management panel.
|
||||||
|
|
||||||
|
At runtime, one LangBot process owns:
|
||||||
|
|
||||||
|
- a Quart/Hypercorn HTTP service and the built web UI on `:5300`;
|
||||||
|
- messaging-platform adapters such as Discord, Telegram, Slack, WeChat, QQ, WeCom, Lark, DingTalk, KOOK, LINE, Satori, Matrix, and HTTP/WebSocket bots;
|
||||||
|
- a pipeline engine that turns inbound platform messages into LLM/tool/plugin work and replies;
|
||||||
|
- persistence, storage, vector database, telemetry, monitoring, and configuration managers;
|
||||||
|
- bridges to the Plugin Runtime and Box Runtime provided by `langbot-plugin-sdk`;
|
||||||
|
- an MCP server at `/mcp` exposing a curated agent-facing subset of the service layer.
|
||||||
|
|
||||||
|
## Repository Boundary
|
||||||
|
|
||||||
|
LangBot is not a single-repo system.
|
||||||
|
|
||||||
|
- `LangBot/` is the main product: backend, web UI, platform adapters, pipeline engine, HTTP API, MCP server, RAG, persistence, skills integration, and the bridge code that talks to runtimes.
|
||||||
|
- `langbot-plugin-sdk/` is published as `langbot-plugin` and pinned in `LangBot/pyproject.toml`. It contains plugin developer APIs, shared entities, `lbp`, the Plugin Runtime (`lbp rt`), and the Box Runtime (`lbp box`).
|
||||||
|
- Plugins import SDK APIs from `langbot_plugin.*`; the LangBot main process imports the same package for shared entities and runtime protocols.
|
||||||
|
|
||||||
|
This split matters. If a change modifies SDK entities, component APIs, action protocols, `lbp rt`, or `lbp box`, verify the sibling SDK repo and install the local SDK into LangBot's virtualenv when testing cross-repo behavior.
|
||||||
|
|
||||||
|
## Startup Path
|
||||||
|
|
||||||
|
The process entrypoint is small and layered:
|
||||||
|
|
||||||
|
1. `main.py` delegates to `langbot.__main__.main()`.
|
||||||
|
2. `src/langbot/__main__.py` parses `--standalone-runtime`, `--standalone-box`, and `--debug`, checks dependencies, generates missing config/data files, and calls `pkg.core.boot.main()`.
|
||||||
|
3. `pkg/core/boot.py` executes startup stages in order: `LoadConfigStage`, `GenKeysStage`, `SetupLoggerStage`, `BuildAppStage`, `ShowNotesStage`.
|
||||||
|
4. `BuildAppStage` constructs the `Application` object by wiring managers, services, runtime connectors, and controllers.
|
||||||
|
5. `Application.run()` starts the platform manager, query controller, HTTP controller, telemetry/cleanup loops, and plugin initialization.
|
||||||
|
|
||||||
|
The central runtime object is `pkg/core/app.py::Application`. It is a service locator for long-lived managers. That is not elegant, but it is the current architectural center; most subsystems receive `ap: Application` and collaborate through it.
|
||||||
|
|
||||||
|
## Top-Level Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
LangBot/
|
||||||
|
├── main.py # Entrypoint shim
|
||||||
|
├── pyproject.toml # Python package, deps, pinned langbot-plugin
|
||||||
|
├── src/langbot/
|
||||||
|
│ ├── __main__.py # CLI entrypoint and boot handoff
|
||||||
|
│ ├── pkg/
|
||||||
|
│ │ ├── core/ # Application, boot stages, task manager
|
||||||
|
│ │ ├── api/ # HTTP API + MCP server mount
|
||||||
|
│ │ ├── platform/ # IM adapters and runtime bot manager
|
||||||
|
│ │ ├── pipeline/ # Message routing and pipeline stages
|
||||||
|
│ │ ├── provider/ # LLM runners, model manager, tools
|
||||||
|
│ │ ├── plugin/ # LangBot-side Plugin Runtime connector/handler
|
||||||
|
│ │ ├── box/ # LangBot-side Box service/connector
|
||||||
|
│ │ ├── skill/ # Skill metadata/activation integration
|
||||||
|
│ │ ├── rag/ , vector/ # Knowledge-base and vector DB integration
|
||||||
|
│ │ ├── persistence/ # SQLAlchemy/SQLModel, Alembic, legacy migrations
|
||||||
|
│ │ ├── storage/ # Local/S3 file storage abstraction
|
||||||
|
│ │ └── config/, entity/, utils/, telemetry/, survey/
|
||||||
|
│ ├── libs/ # Vendored third-party platform SDKs
|
||||||
|
│ └── templates/ # Default config and component metadata
|
||||||
|
├── web/ # Vite + React Router + shadcn/ui + Tailwind SPA
|
||||||
|
├── docker/ # Deployment manifests
|
||||||
|
├── skills/ # In-repo agent skills, single source of truth
|
||||||
|
└── tests/ # Unit/integration/e2e/manual tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Runtime Graph
|
||||||
|
|
||||||
|
The most useful mental model is this graph:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform adapter
|
||||||
|
→ RuntimeBot
|
||||||
|
→ MessageAggregator
|
||||||
|
→ QueryPool
|
||||||
|
→ Controller
|
||||||
|
→ RuntimePipeline
|
||||||
|
→ PipelineStage chain
|
||||||
|
→ RequestRunner / ToolManager / PluginRuntimeConnector / BoxService
|
||||||
|
→ response via adapter
|
||||||
|
```
|
||||||
|
|
||||||
|
The HTTP and MCP surfaces are parallel entrypoints into the same service layer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP client / Web UI
|
||||||
|
→ Quart route group
|
||||||
|
→ api/http/service/*
|
||||||
|
→ Application managers / persistence / runtime connectors
|
||||||
|
|
||||||
|
MCP client
|
||||||
|
→ /mcp mount
|
||||||
|
→ api/mcp/server.py tools
|
||||||
|
→ the same service layer directly
|
||||||
|
```
|
||||||
|
|
||||||
|
## Message Flow
|
||||||
|
|
||||||
|
Inbound platform messages enter through adapter-specific SDK callbacks. The common path is:
|
||||||
|
|
||||||
|
1. A platform adapter under `pkg/platform/sources/` converts platform-specific events into SDK message/event entities.
|
||||||
|
2. `RuntimeBot` in `pkg/platform/botmgr.py` applies pipeline routing rules and either discards the message, pushes it to webhooks, or sends it to the message aggregator.
|
||||||
|
3. `MessageAggregator` batches/normalizes messages before adding a `Query` to `QueryPool`.
|
||||||
|
4. `Controller` in `pkg/pipeline/controller.py` selects queries subject to global pipeline concurrency and per-session concurrency.
|
||||||
|
5. `RuntimePipeline` in `pkg/pipeline/pipelinemgr.py` runs configured pipeline stages using a responsibility-chain style executor that supports generator stages.
|
||||||
|
6. The chat stage emits plugin events, calls a configured `RequestRunner`, handles streaming/non-streaming responses, records telemetry, and appends conversation history.
|
||||||
|
7. Output stages send text, cards, chunks, files, or error notices back through the original platform adapter.
|
||||||
|
|
||||||
|
Pipeline components are registered by decorators and package import side effects. When adding a new stage, loader, runner, or adapter, check the corresponding preregistration mechanism instead of inventing a second registry.
|
||||||
|
|
||||||
|
## Platform Layer
|
||||||
|
|
||||||
|
Platform code lives under `pkg/platform/`.
|
||||||
|
|
||||||
|
- `botmgr.py` owns runtime bots, routing rules, event logging, webhook pushing, and adapter lifecycle.
|
||||||
|
- `sources/` contains adapter implementations. Each adapter subclasses `langbot_plugin.api.definition.abstract.platform.adapter.AbstractMessagePlatformAdapter` from the SDK.
|
||||||
|
- Platform entities such as `MessageChain`, `Image`, `At`, `Voice`, and events come from `langbot-plugin-sdk`, not from this repo.
|
||||||
|
|
||||||
|
The platform layer should translate between external platform APIs and LangBot's shared message/event model. It should not contain LLM-provider logic or pipeline business logic.
|
||||||
|
|
||||||
|
## Pipeline Layer
|
||||||
|
|
||||||
|
Pipeline code lives under `pkg/pipeline/`.
|
||||||
|
|
||||||
|
Important pieces:
|
||||||
|
|
||||||
|
- `pool.py::QueryPool` stores pending queries and cached in-flight queries for plugin backward-compatible calls.
|
||||||
|
- `controller.py::Controller` schedules query processing and enforces concurrency.
|
||||||
|
- `pipelinemgr.py::RuntimePipeline` materializes database pipeline config into a runtime stage chain.
|
||||||
|
- `process/handlers/chat.py::ChatMessageHandler` is the main LLM conversation handler.
|
||||||
|
- Stage families include response rules, banned sessions, content filters, preprocessors, rate limits, message truncation, long text handling, response-back, command handling, and wrappers.
|
||||||
|
|
||||||
|
Pipelines are configuration-driven. Prefer adding a stage or extending an existing stage family over hard-coding behavior in platform adapters.
|
||||||
|
|
||||||
|
## Provider, RAG, and Tools
|
||||||
|
|
||||||
|
Provider code lives under `pkg/provider/`.
|
||||||
|
|
||||||
|
- `modelmgr/` manages configured model providers and requesters.
|
||||||
|
- `runners/` implements request runners such as the local agent runner and external workflow integrations.
|
||||||
|
- `tools/toolmgr.py` aggregates tools from native tools, plugin tools, external MCP servers, and skill-authoring tools.
|
||||||
|
- `tools/loaders/mcp.py` is the MCP client side: external MCP servers that LangBot connects to for agent tools.
|
||||||
|
- RAG lives across `pkg/rag/`, `pkg/vector/`, model services, and plugin KnowledgeEngine actions.
|
||||||
|
|
||||||
|
Do not confuse LangBot's MCP client side with LangBot's own MCP server at `/mcp`; they are different surfaces.
|
||||||
|
|
||||||
|
## Plugin System
|
||||||
|
|
||||||
|
The plugin system crosses the repo boundary.
|
||||||
|
|
||||||
|
In this repo:
|
||||||
|
|
||||||
|
- `pkg/plugin/connector.py` connects LangBot to the Plugin Runtime over stdio or WebSocket.
|
||||||
|
- `pkg/plugin/handler.py` exposes LangBot actions to the runtime and calls runtime actions for plugin operations.
|
||||||
|
- `pkg/provider/tools/loaders/plugin.py` exposes plugin Tool components to LLM runners.
|
||||||
|
- Pipeline handlers emit SDK events such as normal-message events and prompt-processing events.
|
||||||
|
|
||||||
|
In `langbot-plugin-sdk`:
|
||||||
|
|
||||||
|
- `src/langbot_plugin/api/` defines `BasePlugin`, component base classes, message/event entities, contexts, proxies, and manifests.
|
||||||
|
- `src/langbot_plugin/runtime/` implements `lbp rt`, plugin discovery, dependency installation, process launching, and control/debug connections.
|
||||||
|
- `src/langbot_plugin/entities/io/` defines the action protocol shared by LangBot, runtime, and plugin processes.
|
||||||
|
|
||||||
|
The Plugin Runtime supports stdio and WebSocket control transports. Direct local LangBot runs usually spawn the runtime over stdio. Containerized/standalone deployments connect over WebSocket using `plugin.runtime_ws_url` and `--standalone-runtime`.
|
||||||
|
|
||||||
|
## Box Runtime and Skills
|
||||||
|
|
||||||
|
Box is the sandbox subsystem used by native agent tools, stdio MCP servers, skill authoring, and managed processes.
|
||||||
|
|
||||||
|
In this repo:
|
||||||
|
|
||||||
|
- `pkg/box/service.py` is the application-facing facade for exec, sessions, managed processes, skill CRUD, status, reconnects, quotas, mounts, and sandbox profiles.
|
||||||
|
- `pkg/box/connector.py` connects to the Box Runtime over stdio, Windows subprocess+WebSocket, or remote WebSocket.
|
||||||
|
- `pkg/provider/tools/loaders/native.py`, `mcp_stdio.py`, and skill loaders depend on Box availability.
|
||||||
|
- `pkg/skill/manager.py` loads skills from the Box runtime, falling back to local `data/skills` when needed.
|
||||||
|
|
||||||
|
In `langbot-plugin-sdk`:
|
||||||
|
|
||||||
|
- `src/langbot_plugin/box/server.py` implements `lbp box` and the WebSocket endpoints on `:5410`.
|
||||||
|
- `src/langbot_plugin/box/runtime.py` owns sandbox sessions and managed processes.
|
||||||
|
- `backend.py`, `nsjail_backend.py`, and `e2b_backend.py` implement sandbox backends.
|
||||||
|
- `skill_store.py` manages skill packages from the Box side.
|
||||||
|
|
||||||
|
Important config keys live under `box:` in `src/langbot/templates/config.yaml`: `box.enabled`, `box.backend`, `box.runtime.endpoint`, and `box.local.*`. Start LangBot with `--standalone-box` when connecting to an externally launched Box runtime.
|
||||||
|
|
||||||
|
## HTTP API, Web UI, and MCP Server
|
||||||
|
|
||||||
|
`pkg/api/http/controller/main.py` builds a Quart app, registers route groups, serves the built SPA, and wraps the ASGI app with the MCP dispatcher.
|
||||||
|
|
||||||
|
- HTTP route groups live under `pkg/api/http/controller/groups/`.
|
||||||
|
- Service-layer logic lives under `pkg/api/http/service/`.
|
||||||
|
- The built web UI is served from the frontend build path with SPA fallback.
|
||||||
|
- The MCP server lives under `pkg/api/mcp/` and is mounted at `/mcp`.
|
||||||
|
|
||||||
|
The MCP server intentionally exposes a curated subset of the API. Tools call service classes directly rather than making HTTP requests back into LangBot.
|
||||||
|
|
||||||
|
Maintenance rule: when adding, removing, or changing an HTTP endpoint that should be agent-accessible, update the matching MCP tool and the relevant in-repo skill under `skills/` in the same pass.
|
||||||
|
|
||||||
|
## Persistence and Configuration
|
||||||
|
|
||||||
|
Persistence is centered on `pkg/persistence/mgr.py`.
|
||||||
|
|
||||||
|
- SQLite is the default database; PostgreSQL is supported.
|
||||||
|
- Models live under `pkg/entity/persistence/`.
|
||||||
|
- Fresh schemas are created from metadata, then legacy migrations run up to the frozen 3.x baseline, then Alembic migrations run to head.
|
||||||
|
- New schema changes should use Alembic under `pkg/persistence/alembic/versions/`; do not extend the frozen legacy migration chain.
|
||||||
|
|
||||||
|
Configuration starts from `src/langbot/templates/config.yaml` and is generated into `data/config.yaml` on first run. Most long-lived managers read from `ap.instance_config.data`.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
The frontend lives in `web/` and is a Vite SPA using React Router 7, shadcn/ui, Tailwind CSS, and pnpm. It is not Next.js, despite some historical filenames.
|
||||||
|
|
||||||
|
In development, `pnpm dev` serves the UI on `:3000` and reads `VITE_API_BASE_URL` to call the backend on `:5300`. In production, the built frontend is packaged into the Python distribution and served by the backend.
|
||||||
|
|
||||||
|
Keep frontend API behavior aligned with `pkg/api/http/service/` and route groups. User-facing strings must go through the existing i18n setup.
|
||||||
|
|
||||||
|
## Agent-Facing Surfaces
|
||||||
|
|
||||||
|
LangBot is deliberately agent-friendly. The agent-facing surfaces are part of the architecture, not extra docs.
|
||||||
|
|
||||||
|
- `skills/` is the single source of truth for in-repo skills.
|
||||||
|
- `pkg/api/mcp/server.py` exposes the LangBot MCP server at `/mcp`.
|
||||||
|
- `api.global_api_key` authenticates API/MCP access without a browser login.
|
||||||
|
- `AGENTS.md` and `ARCHITECTURE.md` tell coding agents how the repo works.
|
||||||
|
|
||||||
|
When one of these changes, update the others if the behavior or contract changed. API, MCP tools, and skills are one system; drift is a bug.
|
||||||
|
|
||||||
|
## Where to Change Things
|
||||||
|
|
||||||
|
- New HTTP API: add/adjust a service in `pkg/api/http/service/`, a route group in `pkg/api/http/controller/groups/`, tests, and MCP/skills if agent-accessible.
|
||||||
|
- New platform adapter: add a `pkg/platform/sources/*` adapter, component metadata/templates as needed, i18n, docs, and tests/smoke coverage.
|
||||||
|
- New pipeline behavior: add or extend a pipeline stage family under `pkg/pipeline/`; avoid putting pipeline rules in adapters.
|
||||||
|
- New LLM provider/requester: work under `pkg/provider/modelmgr/` and related service/UI surfaces.
|
||||||
|
- New LLM tool source: extend `pkg/provider/tools/loaders/` and `ToolManager` intentionally.
|
||||||
|
- New plugin component/API/protocol: change `langbot-plugin-sdk` first or in lockstep, then update LangBot bridge code.
|
||||||
|
- New Box capability: change both `pkg/box/` and `langbot-plugin-sdk/src/langbot_plugin/box/`, plus config and tests.
|
||||||
|
- New database schema: add an Alembic migration, not a legacy `dbmXXX` migration.
|
||||||
|
|
||||||
|
## Design Biases
|
||||||
|
|
||||||
|
- Keep platform translation, pipeline orchestration, provider execution, and runtime protocols separate.
|
||||||
|
- Reuse existing registries and service layers instead of adding parallel paths.
|
||||||
|
- Prefer small, explicit agent surfaces over exposing every internal API.
|
||||||
|
- Treat cross-repo contracts with the SDK as public interfaces.
|
||||||
|
- Test behavior at the narrowest useful layer first, then add integration/e2e coverage for runtime or platform changes.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# LangBot Individual Contributor License Agreement (v1.0)
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to LangBot (the "Project"), stewarded by Beijing Langbo Intelligent Technology Co., Ltd. (北京浪波智能科技有限公司) ("We" or "Us").
|
||||||
|
|
||||||
|
This Individual Contributor License Agreement ("Agreement") documents the rights granted by contributors to Us. By signing this Agreement (see Section 9), You accept and agree to the following terms and conditions for Your present and future Contributions submitted to the Project. Except for the licenses granted herein to Us and recipients of software distributed by Us, You reserve all right, title, and interest in and to Your Contributions.
|
||||||
|
|
||||||
|
## 1. Definitions
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Us.
|
||||||
|
|
||||||
|
"Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Us for inclusion in, or documentation of, any of the products or repositories owned or managed by Us (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||||
|
|
||||||
|
## 2. Grant of Copyright License
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. For clarity, this includes the right for Us to distribute Your Contributions, alone or as part of the Work, under the terms of any license, including without limitation open source licenses and commercial or proprietary licenses.
|
||||||
|
|
||||||
|
## 3. Grant of Patent License
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby grant to Us and to recipients of software distributed by Us a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
## 4. Authority; Employer
|
||||||
|
|
||||||
|
You represent that You are legally entitled to grant the above licenses. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent that You have received permission to make Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions to Us, or that Your employer has executed a separate Corporate Contributor License Agreement with Us.
|
||||||
|
|
||||||
|
## 5. Original Creation; Disclosure
|
||||||
|
|
||||||
|
You represent that each of Your Contributions is Your original creation (see Section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are personally aware and which are associated with any part of Your Contributions.
|
||||||
|
|
||||||
|
## 6. No Obligation of Support; Disclaimer
|
||||||
|
|
||||||
|
You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
## 7. Third-Party Works
|
||||||
|
|
||||||
|
Should You wish to submit work that is not Your original creation, You may submit it to Us separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which You are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
|
||||||
|
|
||||||
|
## 8. Notification
|
||||||
|
|
||||||
|
You agree to notify Us of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect.
|
||||||
|
|
||||||
|
## 9. Electronic Signature
|
||||||
|
|
||||||
|
This Agreement is accepted and signed electronically: posting a comment containing the exact phrase designated by Us (currently "I have read the CLA Document and I hereby sign the CLA") from Your GitHub account on a pull request in the Project's repositories constitutes Your binding electronic signature to this Agreement. You represent that the GitHub account used to sign belongs to You and that You are of legal age to form a binding contract. Your signature covers Your present and future Contributions to all repositories owned or managed by Us, until and unless You notify Us in writing that You withdraw from this Agreement for future Contributions (licenses already granted are irrevocable).
|
||||||
|
|
||||||
|
## 10. Our Commitment
|
||||||
|
|
||||||
|
We commit that the Project's main repository will continue to make an open source version of the Work publicly available.
|
||||||
|
|
||||||
|
## 11. Miscellaneous
|
||||||
|
|
||||||
|
This Agreement is the entire agreement between You and Us regarding Your Contributions and supersedes any prior agreements on this subject. If any provision is held unenforceable, the remaining provisions remain in effect. This Agreement is executed in English; the Chinese translation below is provided for reference only, and the English version shall prevail in case of any discrepancy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# LangBot 个人贡献者许可协议(v1.0)中文参考译文
|
||||||
|
|
||||||
|
> 本译文仅供参考,如与英文版有任何歧义,以英文版为准。
|
||||||
|
|
||||||
|
感谢您有意为 LangBot(下称"本项目")作出贡献。本项目由北京浪波智能科技有限公司(下称"我方")运营管理。
|
||||||
|
|
||||||
|
本《个人贡献者许可协议》(下称"本协议")旨在记录贡献者授予我方的各项权利。您一经签署本协议(见第 9 条),即接受并同意以下条款与条件,适用于您向本项目提交的现在及未来的全部贡献。除本协议授予我方及我方分发软件之接收者的许可外,您保留对您的贡献的全部权利、所有权和利益。
|
||||||
|
|
||||||
|
## 1. 定义
|
||||||
|
|
||||||
|
"您"指与我方订立本协议的版权所有人,或经版权所有人授权的法律实体。
|
||||||
|
|
||||||
|
"贡献"指您有意提交给我方、用于纳入我方拥有或管理的任何产品或代码仓库(下称"作品")或其文档的任何原创作品,包括对既有作品的修改或增补。就本定义而言,"提交"指以任何电子、口头或书面形式向我方或我方代表发送的通信,包括但不限于在由我方或代表我方管理的电子邮件列表、源代码管理系统和问题跟踪系统中,为讨论和改进作品而进行的通信;但您以显著方式标注或以书面形式声明为"非贡献"(Not a Contribution)的通信除外。
|
||||||
|
|
||||||
|
## 2. 版权许可的授予
|
||||||
|
|
||||||
|
在遵守本协议条款与条件的前提下,您特此授予我方及我方分发软件之接收者一项永久的、全球范围的、非独占的、免费的、免版税的、不可撤销的版权许可,以复制您的贡献、基于其创作衍生作品、公开展示、公开表演、再许可以及分发您的贡献及上述衍生作品。为明确起见,上述许可包括我方有权以任何许可条款(包括但不限于开源许可证以及商业或专有许可证)单独或作为作品的一部分分发您的贡献。
|
||||||
|
|
||||||
|
## 3. 专利许可的授予
|
||||||
|
|
||||||
|
在遵守本协议条款与条件的前提下,您特此授予我方及我方分发软件之接收者一项永久的、全球范围的、非独占的、免费的、免版税的、不可撤销的(本条所述情形除外)专利许可,以制造、委托制造、使用、许诺销售、销售、进口及以其他方式转让作品;该许可仅适用于您可许可的、且因您的贡献本身或您的贡献与其所提交之作品的结合而必然受到侵犯的专利权利要求。如任何实体对您或任何其他实体提起专利诉讼(包括诉讼中的交叉请求或反诉),主张您的贡献或您所贡献的作品构成直接或帮助性专利侵权,则依据本协议就该贡献或作品授予该实体的任何专利许可,自该诉讼提起之日起终止。
|
||||||
|
|
||||||
|
## 4. 权利能力与雇主
|
||||||
|
|
||||||
|
您声明您在法律上有权授予上述许可。如您的雇主对您创作的、包含您的贡献在内的知识产权享有权利,您声明:您已获得该雇主代表其作出贡献的许可,或该雇主已就您向我方的贡献放弃上述权利,或该雇主已与我方另行签署《企业贡献者许可协议》。
|
||||||
|
|
||||||
|
## 5. 原创性声明与披露义务
|
||||||
|
|
||||||
|
您声明您的每项贡献均为您的原创作品(代表第三方提交的情形见第 7 条)。您声明您提交的贡献中已完整披露您本人知悉的、与您的贡献任何部分相关的任何第三方许可或其他限制(包括但不限于相关专利和商标)的全部细节。
|
||||||
|
|
||||||
|
## 6. 无支持义务;免责声明
|
||||||
|
|
||||||
|
您无义务为您的贡献提供支持,除非您自愿提供。您可以免费提供支持、收费提供支持或不提供支持。除非适用法律要求或另有书面约定,您的贡献按"现状"(AS IS)提供,不附带任何明示或默示的保证或条件,包括但不限于关于权属、不侵权、适销性或特定用途适用性的任何保证或条件。
|
||||||
|
|
||||||
|
## 7. 第三方作品
|
||||||
|
|
||||||
|
如您希望提交非您原创的作品,您可以将其与任何贡献分开单独提交给我方,并完整说明其来源以及您本人知悉的任何许可或其他限制(包括但不限于相关专利、商标和许可协议)的全部细节,同时以显著方式将该作品标注为"代表第三方提交:[此处注明第三方名称]"。
|
||||||
|
|
||||||
|
## 8. 通知义务
|
||||||
|
|
||||||
|
如您知悉任何事实或情况将导致上述声明在任何方面不准确,您同意通知我方。
|
||||||
|
|
||||||
|
## 9. 电子签署
|
||||||
|
|
||||||
|
本协议以电子方式接受并签署:您通过您的 GitHub 账号,在本项目代码仓库的拉取请求(pull request)中发表包含我方指定语句(现为 "I have read the CLA Document and I hereby sign the CLA")的评论,即构成您对本协议具有约束力的电子签名。您声明用于签署的 GitHub 账号归您本人所有,且您已达到订立有约束力合同的法定年龄。您的签署覆盖您对我方拥有或管理的全部代码仓库的现在及未来的贡献,直至您以书面形式通知我方就未来贡献退出本协议为止(已授予的许可不可撤销)。
|
||||||
|
|
||||||
|
## 10. 我方承诺
|
||||||
|
|
||||||
|
我方承诺本项目主仓库将持续公开提供作品的开源版本。
|
||||||
|
|
||||||
|
## 11. 其他
|
||||||
|
|
||||||
|
本协议构成您与我方之间就您的贡献达成的完整协议,并取代双方先前就此主题达成的任何协议。如本协议任何条款被认定为不可执行,其余条款仍然有效。本协议以英文签署,中文译文仅供参考,如有歧义以英文版为准。
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
- 在 PR 和 Commit Message 中请使用全英文
|
- 在 PR 和 Commit Message 中请使用全英文
|
||||||
- 对于中文用户,issue 中可以使用中文
|
- 对于中文用户,issue 中可以使用中文
|
||||||
|
|
||||||
|
### 贡献者许可协议(CLA)
|
||||||
|
|
||||||
|
为了保护项目和每一位贡献者,我们要求所有代码贡献者签署[贡献者许可协议(CLA)](./CLA.md)。这是 Apache、Google、Grafana 等主流开源项目的标准做法:您保留自己代码的全部版权,仅授予项目使用、分发您贡献的许可。
|
||||||
|
|
||||||
|
签署只需 10 秒:首次提交 PR 时,机器人会自动评论提示,按提示回复一句话即完成签署,此后对本组织所有仓库永久有效。历史贡献不受影响。
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
@@ -29,3 +35,9 @@
|
|||||||
|
|
||||||
- Use English in PRs and Commit Messages
|
- Use English in PRs and Commit Messages
|
||||||
- For English users, you can use English in issues
|
- For English users, you can use English in issues
|
||||||
|
|
||||||
|
### Contributor License Agreement (CLA)
|
||||||
|
|
||||||
|
To protect the project and every contributor, we require all code contributors to sign our [Contributor License Agreement](./CLA.md). This is standard practice in major open source projects such as Apache, Google, and Grafana: you keep full copyright of your code — the CLA only grants us a license to use and distribute your contribution.
|
||||||
|
|
||||||
|
Signing takes 10 seconds: when you open your first PR, a bot will guide you to reply with a single comment. One signature covers all repositories in this organization, permanently. Past contributions are not affected.
|
||||||
|
|||||||
+52
-5
@@ -4,7 +4,26 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY web ./web
|
COPY web ./web
|
||||||
|
|
||||||
RUN cd web && npm install && npm run build
|
RUN cd web && npm install && npx vite build
|
||||||
|
|
||||||
|
# Build nsjail from source so the image ships a self-contained sandbox backend
|
||||||
|
# that needs no host Docker socket. Pinned to a release tag for reproducibility.
|
||||||
|
# Multi-stage keeps the compile toolchain (bison/flex/protobuf-dev/libnl-dev)
|
||||||
|
# out of the final image; only the nsjail binary and its small runtime libs
|
||||||
|
# (libprotobuf, libnl-route-3) are carried over.
|
||||||
|
FROM python:3.12.7-slim AS nsjail-build
|
||||||
|
|
||||||
|
ARG NSJAIL_VERSION=3.6
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates git build-essential \
|
||||||
|
autoconf bison flex libtool pkg-config \
|
||||||
|
protobuf-compiler libprotobuf-dev libnl-route-3-dev \
|
||||||
|
&& git clone --depth 1 --branch "${NSJAIL_VERSION}" https://github.com/google/nsjail.git /nsjail \
|
||||||
|
&& make -C /nsjail \
|
||||||
|
&& install -m 0755 /nsjail/nsjail /usr/local/bin/nsjail \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
FROM python:3.12.7-slim
|
FROM python:3.12.7-slim
|
||||||
|
|
||||||
@@ -12,12 +31,40 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
COPY --from=node /app/web/out ./web/out
|
COPY --from=node /app/web/dist ./web/dist
|
||||||
|
|
||||||
RUN apt update \
|
# nsjail binary built in the dedicated stage above. Self-contained sandbox
|
||||||
&& apt install gcc -y \
|
# backend; lets the Box runtime isolate code without a host Docker socket.
|
||||||
|
COPY --from=nsjail-build /usr/local/bin/nsjail /usr/local/bin/nsjail
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends gcc ca-certificates curl gnupg \
|
||||||
|
# nsjail runtime libraries (the build toolchain stays in the nsjail-build
|
||||||
|
# stage; only these shared libs are needed to execute the binary).
|
||||||
|
&& apt-get install -y --no-install-recommends libprotobuf32 libnl-route-3-200 \
|
||||||
|
# Install the Docker CLI (client only) so the optional langbot_box
|
||||||
|
# service can drive the mounted host Docker socket and create sandbox
|
||||||
|
# containers. The same image powers langbot / plugin_runtime / box; only
|
||||||
|
# box uses the client. Arch-aware via dpkg so multi-arch builds work.
|
||||||
|
&& install -m 0755 -d /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \
|
||||||
|
&& chmod a+r /etc/apt/keyrings/docker.asc \
|
||||||
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" > /etc/apt/sources.list.d/docker.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends docker-ce-cli \
|
||||||
|
# Install Node.js LTS so the sandbox (nsjail/Docker box) can run npx-based
|
||||||
|
# stdio MCP servers. node/npx land in /usr/bin, which is on the nsjail
|
||||||
|
# read-only mount whitelist (_READONLY_SYSTEM_MOUNTS), so they are bound
|
||||||
|
# into the sandbox chroot automatically. Without node, any npx-launched
|
||||||
|
# MCP server exits with return_code=127 (command not found).
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh \
|
||||||
|
&& bash /tmp/nodesource_setup.sh \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -f /tmp/nodesource_setup.sh \
|
||||||
&& python -m pip install --no-cache-dir uv \
|
&& python -m pip install --no-cache-dir uv \
|
||||||
&& uv sync \
|
&& uv sync \
|
||||||
|
&& apt-get purge -y --auto-remove curl gnupg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& touch /.dockerenv
|
&& touch /.dockerenv
|
||||||
|
|
||||||
CMD [ "uv", "run", "main.py" ]
|
CMD [ "uv", "run", "--no-sync", "main.py" ]
|
||||||
@@ -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/
|
||||||
@@ -1,155 +1,201 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/products/langbot/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></a>
|
||||||
|
|
||||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
<h3>Production-grade platform for building agentic IM bots.</h3>
|
||||||
|
<h4>Quickly build, debug, and ship AI bots to Slack, Discord, Telegram, WeChat, and more.</h4>
|
||||||
|
|
||||||
|
English / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
[](https://gitcode.com/RockChinQ/LangBot)
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
<a href="https://langbot.app">项目主页</a> |
|
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文档</a> |
|
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">插件介绍</a> |
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
|
|
||||||
|
|
||||||
|
<a href="https://langbot.app">Website</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/features">Features</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/guide">Docs</a> |
|
||||||
|
<a href="https://link.langbot.app/en/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app/cloud">Cloud</a> |
|
||||||
|
<a href="https://space.langbot.app">Plugin Market</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">Roadmap</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot 是一个开源的大语言模型原生即时通信机器人开发平台,旨在提供开箱即用的 IM 机器人开发体验,具有 Agent、RAG、MCP 等多种 LLM 应用功能,适配全球主流即时通信平台,并提供丰富的 API 接口,支持自定义开发。
|
---
|
||||||
|
|
||||||
## 📦 开始使用
|
## What is LangBot?
|
||||||
|
|
||||||
#### Docker Compose 部署
|
LangBot is an **open-source, production-grade platform** for building AI-powered instant messaging bots. It connects Large Language Models (LLMs) to any chat platform, enabling you to create intelligent agents that can converse, execute tasks, and integrate with your existing workflows.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="LangBot web management dashboard — real-time monitoring of message volume, model calls, success rate and active sessions" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Key Capabilities
|
||||||
|
|
||||||
|
- **AI Conversations & Agents** — Multi-turn dialogues, tool calling, multi-modal support, streaming output. Built-in RAG (knowledge base) with deep integration to [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org), [Deerflow](https://deerflow.tech), [Weknora](https://weknora.weixin.qq.com).
|
||||||
|
- **Universal IM Platform Support** — One codebase for Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Production-Ready** — Access control, rate limiting, sensitive word filtering, comprehensive monitoring, and exception handling. Trusted by enterprises.
|
||||||
|
- **Plugin Ecosystem** — Hundreds of plugins, event-driven architecture, component extensions, and [MCP protocol](https://modelcontextprotocol.io/) support.
|
||||||
|
- **Web Management Panel** — Configure, manage, and monitor your bots through an intuitive browser interface. No YAML editing required.
|
||||||
|
- **Multi-Pipeline Architecture** — Different bots for different scenarios, with comprehensive monitoring and exception handling.
|
||||||
|
|
||||||
|
[→ Learn more about all features](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 Practical guides: [deploy a multi-platform AI bot in 5 minutes](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connect DeepSeek to WeChat, Discord, and Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [run a Dify Agent in Discord, Telegram, and Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/), and [build an n8n-powered chatbot](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 Stay Updated
|
||||||
|
|
||||||
|
Click the Star and Watch buttons in the top-right corner of the repository to get the latest updates.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### ☁️ LangBot Cloud (Recommended)
|
||||||
|
|
||||||
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — Zero deployment, ready to use.
|
||||||
|
|
||||||
|
### One-Line Launch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx langbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). Visit http://localhost:5300 — done.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/langbot-app/LangBot
|
git clone https://github.com/langbot-app/LangBot
|
||||||
cd LangBot/docker
|
cd LangBot/docker
|
||||||
docker compose up -d
|
docker compose --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
---
|
||||||
|
|
||||||
## 😎 保持更新
|
## Supported Platforms
|
||||||
|
|
||||||
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
|
| Platform | Status | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| Discord | ✅ | Official |
|
||||||
|
| Telegram | ✅ | Official |
|
||||||
|
| Slack | ✅ | Official |
|
||||||
|
| LINE | ✅ | Official |
|
||||||
|
| QQ | ✅ | Personal & Official API (Channel, DM, Group) |
|
||||||
|
| WeCom | ✅ | Enterprise WeChat, External CS, AI Bot |
|
||||||
|
| WeChat | ✅ | Personal & Official Account |
|
||||||
|
| Lark | ✅ | Official |
|
||||||
|
| DingTalk | ✅ | Official |
|
||||||
|
| KOOK | ✅ | Official |
|
||||||
|
| Satori | ✅ | |
|
||||||
|
| Email | ✅ | Matrix, Satori |
|
||||||
|
| Matrix | ✅ | Supports multiple bridged platforms such as Signal, WhatsApp, Messenger, iMessage, Mattermost, Google Chat, IRC, XMPP, Zulip, and more |
|
||||||
|
|
||||||

|
---
|
||||||
|
|
||||||
## ✨ 特性
|
## Supported LLMs & Integrations
|
||||||
|
|
||||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)。
|
| Provider | Type | Status |
|
||||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
| ----------------------------------------------------------------------------------------------------------------- | ------------ | ------ |
|
||||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | Local LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | Local LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | Protocol | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | Gateway | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | Gateway | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | Gateway | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | Gateway | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | Gateway | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU Platform | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU Platform | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU Platform | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | Gateway | ✅ |
|
||||||
|
| [302.AI](https://share.302ai.cn/SuTG99) | Gateway | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | Gateway | ✅ |
|
||||||
|
|
||||||
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
|
[→ View all integrations](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
或访问 demo 环境:https://demo.langbot.dev/
|
---
|
||||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
|
||||||
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。
|
|
||||||
|
|
||||||
### 消息平台
|
## Why LangBot?
|
||||||
|
|
||||||
| 平台 | 状态 | 备注 |
|
| Use Case | How LangBot Helps |
|
||||||
| --- | --- | --- |
|
| --------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
| **Customer Support** | Deploy AI agents to Slack/Discord/Telegram that answer questions using your knowledge base |
|
||||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
| **Internal Tools** | Connect n8n/Dify workflows to WeCom/DingTalk for automated business processes |
|
||||||
| 企业微信 | ✅ | |
|
| **Community Management** | Moderate QQ/Discord groups with AI-powered content filtering and interaction |
|
||||||
| 企微对外客服 | ✅ | |
|
| **Multi-Platform Presence** | One bot, all platforms. Manage from a single dashboard |
|
||||||
| 企微智能机器人 | ✅ | |
|
|
||||||
| 个人微信 | ✅ | |
|
|
||||||
| 微信公众号 | ✅ | |
|
|
||||||
| 飞书 | ✅ | |
|
|
||||||
| 钉钉 | ✅ | |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | ✅ | |
|
|
||||||
|
|
||||||
### 大模型能力
|
---
|
||||||
|
|
||||||
| 模型 | 状态 | 备注 |
|
## Built for AI Agents 🤖
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 接口格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智谱AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [胜算云](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 全球大模型都可调用(友情推荐) |
|
|
||||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 资源平台 |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型运行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型接口聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [小马算力](https://www.tokenpony.cn/453z1) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里云百炼](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支持通过 MCP 协议获取工具 |
|
|
||||||
| [百宝箱Tbox](https://www.tbox.cn/open) | ✅ | 蚂蚁百宝箱智能体平台,每月免费10亿大模型Token |
|
|
||||||
|
|
||||||
### TTS
|
LangBot is **agent-friendly by design** — your coding agents (Claude Code, Codex, Copilot, Cursor, …) can operate, extend, and deploy LangBot with first-class support:
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
- **MCP Server** — LangBot exposes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) endpoint at `/mcp`, mirroring the HTTP API so an agent can manage bots, pipelines, plugins, and models programmatically. Authenticate with the same API key (set a global key in `config.yaml` or use a per-user key) — no login flow required. Configure it in the Web panel's **API & MCP** tab.
|
||||||
| --- | --- |
|
- **In-repo Skills** — The [`skills/`](skills/) directory is the **single source of truth** for working with LangBot: plugin development, core development, end-to-end testing, deployment, and operating the LangBot / LangBot Space MCP servers. Point your agent at this directory and it knows how to build.
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
- **AGENTS.md** — Every repo ships an [`AGENTS.md`](AGENTS.md) (symlinked to `CLAUDE.md`) describing architecture, conventions, and the rule that API changes must keep the MCP server and skills in sync.
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
- **`llms.txt`** — Machine-readable project context for LLMs is published on the website.
|
||||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
|
||||||
|
|
||||||
### 文生图
|
> **Cloud / Marketplace:** [LangBot Space](https://space.langbot.app) also exposes an MCP server so agents can search and inspect the plugin / MCP / skill marketplace, authenticated with a Personal Access Token.
|
||||||
|
|
||||||
| 平台/模型 | 备注 |
|
---
|
||||||
| --- | --- |
|
|
||||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
|
||||||
|
|
||||||
## 😘 社区贡献
|
## Live Demo
|
||||||
|
|
||||||
感谢以下[代码贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)和社区里其他成员对 LangBot 的贡献:
|
**Try it now:** https://demo.langbot.dev/
|
||||||
|
|
||||||
|
- Email: `demo@langbot.app`
|
||||||
|
- Password: `langbot123456`
|
||||||
|
|
||||||
|
_Note: Public demo environment. Do not enter sensitive information._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord Community](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Thanks to all [contributors](https://github.com/langbot-app/LangBot/graphs/contributors) who have helped make LangBot better:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</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.
|
|
||||||
-->
|
|
||||||
|
|||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
<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/IrlV8QFacU)
|
||||||
|
[](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。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="LangBot Web 管理面板仪表盘 — 实时监控消息量、模型调用、成功率与活跃会话" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 核心能力
|
||||||
|
|
||||||
|
- **AI 对话与 Agent** — 多轮对话、工具调用、多模态、流式输出。自带 RAG(知识库),深度集成 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org)、[Deerflow](https://deerflow.tech)、[Weknora](https://weknora.weixin.qq.com)等 LLMOps 平台。
|
||||||
|
- **全平台支持** — 一套代码,覆盖 QQ、微信、企业微信、飞书、钉钉、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生产就绪** — 访问控制、限速、敏感词过滤、全面监控与异常处理,已被多家企业采用。
|
||||||
|
- **插件生态** — 数百个插件,跨进程的事件驱动架构,组件扩展,适配 [MCP 协议](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 通过浏览器直观地配置、管理和监控机器人,无需手动编辑配置文件。
|
||||||
|
- **多流水线架构** — 不同机器人用于不同场景,具备全面的监控和异常处理能力。
|
||||||
|
|
||||||
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
|
📍 实践指南:[5 分钟部署多平台 AI 机器人](https://blog.langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[将 DeepSeek 接入微信、企业微信与 Discord](https://blog.langbot.app/zh/blog/connect-deepseek-to-wechat/)、[让 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://blog.langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 构建多平台 AI 聊天机器人](https://blog.langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 保持更新
|
||||||
|
|
||||||
|
点击[仓库首页](https://github.com/langbot-app/LangBot)右上角 Star 和 Watch 按钮,获取最新动态。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### ☁️ 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 --profile all 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](https://docs.langbot.app/zh/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支持的平台
|
||||||
|
|
||||||
|
| 平台 | 状态 | 备注 |
|
||||||
|
|------|------|------|
|
||||||
|
| 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.302ai.cn/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`
|
||||||
|
|
||||||
|
*注意:公开演示环境,请不要在其中填入任何敏感信息。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为 AI Agent 而生 🤖
|
||||||
|
|
||||||
|
LangBot **从设计上就对 Agent 友好** —— 你的编码 Agent(Claude Code、Codex、Copilot、Cursor 等)可以一等公民般地操作、扩展和部署 LangBot:
|
||||||
|
|
||||||
|
- **MCP Server** —— LangBot 内置 [Model Context Protocol](https://modelcontextprotocol.io/) 端点 `/mcp`,与 HTTP API 对齐,Agent 可编程式管理机器人、流水线、插件和模型。使用同一套 API Key 鉴权(可在 `config.yaml` 配置全局 Key,或使用用户 Key),无需登录流程。在 Web 面板的 **API 与 MCP** 标签页中配置。
|
||||||
|
- **仓库内 Skills** —— [`skills/`](skills/) 目录是使用 LangBot 的**唯一事实来源**:插件开发、核心开发、端到端测试、部署,以及操作 LangBot / LangBot Space MCP Server。把 Agent 指向这个目录,它就知道如何动手。
|
||||||
|
- **AGENTS.md** —— 每个仓库都提供 [`AGENTS.md`](AGENTS.md)(软链到 `CLAUDE.md`),描述架构、规范,以及「API 变更必须同步更新 MCP Server 和 skills」的约定。
|
||||||
|
- **`llms.txt`** —— 面向 LLM 的机器可读项目上下文已发布在官网。
|
||||||
|
|
||||||
|
> **云端 / 市场:** [LangBot Space](https://space.langbot.app) 同样开放 MCP Server,Agent 可搜索和查看插件 / MCP / Skill 市场,使用 Personal Access Token 鉴权。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社区
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
- [Discord 社区](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社区群](https://qm.qq.com/q/DxZZcNxM1W)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趋势
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
感谢所有[贡献者](https://github.com/langbot-app/LangBot/graphs/contributors)对 LangBot 的帮助:
|
||||||
|
|
||||||
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## For Code Agents
|
||||||
|
|
||||||
|
If you're a code agent based on LLM, you should read [AGENTS.md](AGENTS.md) to understand the project structure and development process before you start any development.
|
||||||
|
-->
|
||||||
-126
@@ -1,126 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<a href="https://langbot.app">
|
|
||||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
English / [简体中文](README.md) / [繁體中文](README_TW.md) / [日本語](README_JP.md) / (PR for your language)
|
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
|
||||||
|
|
||||||
<a href="https://langbot.app">Home</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">Deployment</a> |
|
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Plugin</a> |
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">Submit Plugin</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
LangBot is an open-source LLM native instant messaging robot development platform, aiming to provide out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, adapting to global instant messaging platforms, and providing rich API interfaces, supporting custom development.
|
|
||||||
|
|
||||||
## 📦 Getting Started
|
|
||||||
|
|
||||||
#### Docker Compose Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/langbot-app/LangBot
|
|
||||||
cd LangBot/docker
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:5300 to start using it.
|
|
||||||
|
|
||||||
Detailed documentation [Docker Deployment](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
|
||||||
|
|
||||||
#### One-click Deployment on BTPanel
|
|
||||||
|
|
||||||
LangBot has been listed on the BTPanel, if you have installed the BTPanel, you can use the [document](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) to use it.
|
|
||||||
|
|
||||||
#### Zeabur Cloud Deployment
|
|
||||||
|
|
||||||
Community contributed Zeabur template.
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railway Cloud Deployment
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
|
||||||
|
|
||||||
#### Other Deployment Methods
|
|
||||||
|
|
||||||
Directly use the released version to run, see the [Manual Deployment](https://docs.langbot.app/en/deploy/langbot/manual.html) documentation.
|
|
||||||
|
|
||||||
## 😎 Stay Ahead
|
|
||||||
|
|
||||||
Click the Star and Watch button in the upper right corner of the repository to get the latest updates.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 💬 Chat with LLM / Agent: Supports multiple LLMs, adapt to group chats and private chats; Supports multi-round conversations, tool calls, multi-modal, and streaming output capabilities. Built-in RAG (knowledge base) implementation, and deeply integrates with [Dify](https://dify.ai).
|
|
||||||
- 🤖 Multi-platform Support: Currently supports QQ, QQ Channel, WeCom, personal WeChat, Lark, DingTalk, Discord, Telegram, etc.
|
|
||||||
- 🛠️ High Stability, Feature-rich: Native access control, rate limiting, sensitive word filtering, etc. mechanisms; Easy to use, supports multiple deployment methods. Supports multiple pipeline configurations, different bots can be used for different scenarios.
|
|
||||||
- 🧩 Plugin Extension, Active Community: Support event-driven, component extension, etc. plugin mechanisms; Integrate Anthropic [MCP protocol](https://modelcontextprotocol.io/); Currently has hundreds of plugins.
|
|
||||||
- 😻 Web UI: Support management LangBot instance through the browser. No need to manually write configuration files.
|
|
||||||
|
|
||||||
For more detailed specifications, please refer to the [documentation](https://docs.langbot.app/en/insight/features.html).
|
|
||||||
|
|
||||||
Or visit the demo environment: https://demo.langbot.dev/
|
|
||||||
- Login information: Email: `demo@langbot.app` Password: `langbot123456`
|
|
||||||
- Note: For WebUI demo only, please do not fill in any sensitive information in the public environment.
|
|
||||||
|
|
||||||
### Message Platform
|
|
||||||
|
|
||||||
| Platform | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Discord | ✅ | |
|
|
||||||
| Telegram | ✅ | |
|
|
||||||
| Slack | ✅ | |
|
|
||||||
| LINE | ✅ | |
|
|
||||||
| Personal QQ | ✅ | |
|
|
||||||
| QQ Official API | ✅ | |
|
|
||||||
| WeCom | ✅ | |
|
|
||||||
| WeComCS | ✅ | |
|
|
||||||
| WeCom AI Bot | ✅ | |
|
|
||||||
| Personal WeChat | ✅ | |
|
|
||||||
| Lark | ✅ | |
|
|
||||||
| DingTalk | ✅ | |
|
|
||||||
|
|
||||||
### LLMs
|
|
||||||
|
|
||||||
| LLM | Status | Remarks |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | Available for any OpenAI interface format model |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps platform |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLM and GPU resource platform |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | Local LLM running platform |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Local LLM running platform |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLM interface gateway(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLM gateway(MaaS), LLMOps platform |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLM gateway(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Support tool access through MCP protocol |
|
|
||||||
|
|
||||||
## 🤝 Community Contribution
|
|
||||||
|
|
||||||
Thank you for the following [code contributors](https://github.com/langbot-app/LangBot/graphs/contributors) and other members in the community for their contributions to LangBot:
|
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
|
||||||
</a>
|
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
<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/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></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.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="Panel de gestión web de LangBot — monitoreo en tiempo real de volumen de mensajes, llamadas a modelos, tasa de éxito y sesiones activas" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 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), [Deerflow](https://deerflow.tech)、[Weknora](https://weknora.weixin.qq.com).
|
||||||
|
- **Soporte Universal de Plataformas de MI** — Un solo código base para Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Listo para Producción** — Control de acceso, limitación de velocidad, filtrado de palabras sensibles, monitoreo completo y manejo de excepciones. De confianza para empresas.
|
||||||
|
- **Ecosistema de Plugins** — Cientos de plugins, arquitectura basada en eventos, extensiones de componentes y soporte del [protocolo MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panel de Gestión Web** — Configure, gestione y monitoree sus bots a través de una interfaz de navegador intuitiva. Sin necesidad de editar YAML.
|
||||||
|
- **Arquitectura Multi-Pipeline** — Diferentes bots para diferentes escenarios, con monitoreo completo y manejo de excepciones.
|
||||||
|
|
||||||
|
[→ Conocer más sobre todas las funcionalidades](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 Guías prácticas: [desplegar un bot de IA multiplataforma en 5 minutos](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [conectar DeepSeek a WeChat, Discord y Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [ejecutar un Dify Agent en Discord, Telegram y Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) y [crear un chatbot con n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 Manténgase Actualizado
|
||||||
|
|
||||||
|
Haga clic en los botones Star y Watch en la esquina superior derecha del repositorio para obtener las últimas actualizaciones.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.302ai.cn/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.*
|
||||||
|
|
||||||
|
## Diseñado para Agentes de IA 🤖
|
||||||
|
|
||||||
|
LangBot es **agent-friendly por diseño** —— tus agentes de codificación (Claude Code, Codex, Copilot, Cursor, …) pueden operar, extender y desplegar LangBot con soporte de primera clase:
|
||||||
|
|
||||||
|
- **Servidor MCP** —— LangBot expone un endpoint integrado de [Model Context Protocol](https://modelcontextprotocol.io/) en `/mcp`, replicando la API HTTP para que un agente gestione bots, pipelines, plugins y modelos de forma programática. Autentícate con la misma API key (configura una clave global en `config.yaml` o usa una clave por usuario) —— sin flujo de login. Configúralo en la pestaña **API & MCP** del panel web.
|
||||||
|
- **Skills en el repositorio** —— El directorio [`skills/`](skills/) es la **única fuente de verdad** para trabajar con LangBot: desarrollo de plugins, desarrollo del core, pruebas end-to-end, despliegue y operación de los servidores MCP de LangBot / LangBot Space. Apunta tu agente a este directorio y sabrá cómo construir.
|
||||||
|
- **AGENTS.md** —— Cada repo incluye un [`AGENTS.md`](AGENTS.md) (enlazado simbólicamente a `CLAUDE.md`) que describe la arquitectura, las convenciones y la regla de que los cambios en la API deben mantener sincronizados el servidor MCP y los skills.
|
||||||
|
- **`llms.txt`** —— El contexto del proyecto legible por máquina para LLMs está publicado en el sitio web.
|
||||||
|
|
||||||
|
> **Nube / Marketplace:** [LangBot Space](https://space.langbot.app) también expone un servidor MCP para que los agentes busquen e inspeccionen el marketplace de plugins / MCP / skills, autenticados con un Personal Access Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
<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/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></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.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="Tableau de bord de gestion web LangBot — surveillance en temps réel du volume de messages, des appels de modèles, du taux de réussite et des sessions actives" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 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), [Deerflow](https://deerflow.tech), [Weknora](https://weknora.weixin.qq.com).
|
||||||
|
- **Support Universel des Plateformes de MI** — Un seul code pour Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Prêt pour la Production** — Contrôle d'accès, limitation de débit, filtrage de mots sensibles, surveillance complète et gestion des exceptions. Approuvé par les entreprises.
|
||||||
|
- **Écosystème de Plugins** — Des centaines de plugins, architecture événementielle, extensions de composants, et support du [protocole MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Panneau de Gestion Web** — Configurez, gérez et surveillez vos bots via une interface navigateur intuitive. Aucune édition de YAML requise.
|
||||||
|
- **Architecture Multi-Pipeline** — Différents bots pour différents scénarios, avec surveillance complète et gestion des exceptions.
|
||||||
|
|
||||||
|
[→ En savoir plus sur toutes les fonctionnalités](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 Guides pratiques : [déployer un bot IA multiplateforme en 5 minutes](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [connecter DeepSeek à WeChat, Discord et Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [exécuter un Dify Agent dans Discord, Telegram et Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) et [créer un chatbot avec n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 Restez à Jour
|
||||||
|
|
||||||
|
Cliquez sur les boutons Star et Watch dans le coin supérieur droit du dépôt pour obtenir les dernières mises à jour.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.302ai.cn/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.*
|
||||||
|
|
||||||
|
## Conçu pour les agents IA 🤖
|
||||||
|
|
||||||
|
LangBot est **agent-friendly par conception** —— vos agents de codage (Claude Code, Codex, Copilot, Cursor, …) peuvent exploiter, étendre et déployer LangBot avec un support de premier ordre :
|
||||||
|
|
||||||
|
- **Serveur MCP** —— LangBot expose un endpoint [Model Context Protocol](https://modelcontextprotocol.io/) intégré sur `/mcp`, reflétant l'API HTTP pour qu'un agent gère bots, pipelines, plugins et modèles de façon programmatique. Authentifiez-vous avec la même clé API (définissez une clé globale dans `config.yaml` ou utilisez une clé par utilisateur) —— sans flux de connexion. Configurez-le dans l'onglet **API & MCP** du panneau web.
|
||||||
|
- **Skills dans le dépôt** —— Le répertoire [`skills/`](skills/) est la **source unique de vérité** pour travailler avec LangBot : développement de plugins, développement du cœur, tests de bout en bout, déploiement et exploitation des serveurs MCP de LangBot / LangBot Space. Pointez votre agent vers ce répertoire et il saura construire.
|
||||||
|
- **AGENTS.md** —— Chaque dépôt fournit un [`AGENTS.md`](AGENTS.md) (lien symbolique vers `CLAUDE.md`) décrivant l'architecture, les conventions et la règle selon laquelle les changements d'API doivent garder le serveur MCP et les skills synchronisés.
|
||||||
|
- **`llms.txt`** —— Le contexte projet lisible par machine pour les LLM est publié sur le site web.
|
||||||
|
|
||||||
|
> **Cloud / Marketplace :** [LangBot Space](https://space.langbot.app) expose également un serveur MCP pour que les agents recherchent et inspectent le marketplace de plugins / MCP / skills, authentifiés avec un Personal Access Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>
|
||||||
+151
-80
@@ -1,125 +1,196 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / (PR for your language)
|
<a href="https://www.producthunt.com/products/langbot/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></a>
|
||||||
|
|
||||||
|
<h3>AIエージェント搭載IMボットを構築するための本番グレードプラットフォーム。</h3>
|
||||||
|
<h4>Slack、Discord、Telegram、WeChat などに AI ボットを素早く構築、デバッグ、デプロイ。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
|
|
||||||
<a href="https://langbot.app">ホーム</a> |
|
<a href="https://langbot.app">ホーム</a> |
|
||||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
<a href="https://link.langbot.app/ja/docs/features">機能</a> |
|
||||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
<a href="https://link.langbot.app/ja/docs/guide">ドキュメント</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">プラグインの提出</a>
|
<a href="https://link.langbot.app/ja/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">プラグインマーケット</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">ロードマップ</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。
|
---
|
||||||
|
|
||||||
## 📦 始め方
|
## LangBot とは?
|
||||||
|
|
||||||
#### Docker Compose デプロイ
|
LangBot は、AI搭載のインスタントメッセージングボットを構築するための**オープンソースの本番グレードプラットフォーム**です。大規模言語モデル(LLM)をあらゆるチャットプラットフォームに接続し、会話、タスク実行、既存のワークフローとの統合が可能なインテリジェントエージェントを作成できます。
|
||||||
|
|
||||||
```bash
|
<p align="center">
|
||||||
git clone https://github.com/langbot-app/LangBot
|
<img src="res/dashboard-overview.png" alt="LangBot Web 管理パネルのダッシュボード — メッセージ量、モデル呼び出し、成功率、アクティブセッションをリアルタイム監視" width="720"/>
|
||||||
cd LangBot/docker
|
</p>
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
http://localhost:5300 にアクセスして使用を開始します。
|
### 主な機能
|
||||||
|
|
||||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
- **AI対話とエージェント** — マルチターン対話、ツール呼び出し、マルチモーダル対応、ストリーミング出力。RAG(ナレッジベース)を内蔵し、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org)、[Deerflow](https://deerflow.tech)、[Weknora](https://weknora.weixin.qq.com) と深く統合。
|
||||||
|
- **ユニバーサルIMプラットフォーム対応** — 単一のコードベースで Discord、Telegram、Slack、LINE、QQ、WeChat、WeCom、Lark、DingTalk、KOOK に対応。
|
||||||
|
- **本番環境対応** — アクセス制御、レート制限、センシティブワードフィルタリング、包括的な監視、例外処理を搭載。エンタープライズの信頼に応える品質。
|
||||||
|
- **プラグインエコシステム** — 数百のプラグイン、イベント駆動アーキテクチャ、コンポーネント拡張、[MCPプロトコル](https://modelcontextprotocol.io/)対応。
|
||||||
|
- **Web管理パネル** — 直感的なブラウザインターフェースからボットの設定、管理、監視が可能。YAML編集は不要。
|
||||||
|
- **マルチパイプラインアーキテクチャ** — 異なるシナリオに異なるボットを配置し、包括的な監視と例外処理を実現。
|
||||||
|
|
||||||
#### Panelでのワンクリックデプロイ
|
[→ すべての機能について詳しく見る](https://link.langbot.app/ja/docs/features)
|
||||||
|
|
||||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
📍 実践ガイド: [5分でマルチプラットフォームAIボットをデプロイ](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/)、[DeepSeekをWeChat・Discord・Telegramに接続](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/)、[Dify AgentをDiscord・Telegram・Slackで動かす](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/)、[n8n連携チャットボットを構築](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/)。
|
||||||
|
|
||||||
#### Zeaburクラウドデプロイ
|
---
|
||||||
|
|
||||||
コミュニティが提供するZeaburテンプレート。
|
|
||||||
|
|
||||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railwayクラウドデプロイ
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
|
||||||
|
|
||||||
#### その他のデプロイ方法
|
|
||||||
|
|
||||||
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
|
|
||||||
|
|
||||||
## 😎 最新情報を入手
|
## 😎 最新情報を入手
|
||||||
|
|
||||||
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ 機能
|
## クイックスタート
|
||||||
|
|
||||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai) と深く統合。
|
### ☁️ LangBot Cloud(推奨)
|
||||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
|
||||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
|
||||||
- 🧩 プラグイン拡張、活発なコミュニティ: イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
|
||||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
|
||||||
|
|
||||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — デプロイ不要、すぐに使えます。
|
||||||
|
|
||||||
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
|
### ワンライン起動
|
||||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
|
||||||
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
|
|
||||||
|
|
||||||
### メッセージプラットフォーム
|
```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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 対応プラットフォーム
|
||||||
|
|
||||||
| プラットフォーム | ステータス | 備考 |
|
| プラットフォーム | ステータス | 備考 |
|
||||||
| --- | --- | --- |
|
|----------|--------|-------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 公式 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 公式 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 公式 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 公式 |
|
||||||
| 個人QQ | ✅ | |
|
| QQ | ✅ | 個人・公式API(チャンネル・DM・グループ) |
|
||||||
| QQ公式API | ✅ | |
|
| WeCom | ✅ | 企業WeChat、外部CS、AIボット |
|
||||||
| WeCom | ✅ | |
|
| WeChat | ✅ | 個人・公式アカウント |
|
||||||
| WeComCS | ✅ | |
|
| Lark | ✅ | 公式 |
|
||||||
| WeCom AI Bot | ✅ | |
|
| DingTalk | ✅ | 公式 |
|
||||||
| 個人WeChat | ✅ | |
|
| KOOK | ✅ | 公式 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | Matrix、Satori |
|
||||||
|
| Matrix | ✅ | Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip など複数のブリッジ先プラットフォームに対応 |
|
||||||
|
|
||||||
### LLMs
|
---
|
||||||
|
|
||||||
| LLM | ステータス | 備考 |
|
## 対応LLMと統合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 任意のOpenAIインターフェース形式モデルに対応 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [Zhipu AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型とGPUリソースプラットフォーム |
|
|
||||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | LLMとGPUリソースプラットフォーム |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOpsプラットフォーム |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | ローカルLLM実行プラットフォーム |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | LLMインターフェースゲートウェイ(MaaS) |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | LLMゲートウェイ(MaaS), LLMOpsプラットフォーム |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | LLMゲートウェイ(MaaS) |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | MCPプロトコルをサポート |
|
|
||||||
|
|
||||||
## 🤝 コミュニティ貢献
|
| プロバイダー | タイプ | ステータス |
|
||||||
|
|----------|------|--------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [Zhipu AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | ローカルLLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | ローカルLLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | プロトコル | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | ゲートウェイ | ✅ |
|
||||||
|
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ゲートウェイ | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ゲートウェイ | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | ゲートウェイ | ✅ |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPUプラットフォーム | ✅ |
|
||||||
|
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPUプラットフォーム | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | ゲートウェイ | ✅ |
|
||||||
|
| [302.AI](https://share.302ai.cn/SuTG99) | ゲートウェイ | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | ゲートウェイ | ✅ |
|
||||||
|
|
||||||
LangBot への貢献に対して、以下の [コード貢献者](https://github.com/langbot-app/LangBot/graphs/contributors) とコミュニティの他のメンバーに感謝します。
|
[→ すべての統合を表示](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## なぜ LangBot?
|
||||||
|
|
||||||
|
| ユースケース | LangBot の活用方法 |
|
||||||
|
|----------|-------------------|
|
||||||
|
| **カスタマーサポート** | ナレッジベースを活用して質問に回答するAIエージェントをSlack/Discord/Telegramにデプロイ |
|
||||||
|
| **社内ツール** | n8n/Difyのワークフローを WeCom/DingTalk に接続し、業務プロセスを自動化 |
|
||||||
|
| **コミュニティ管理** | AI搭載のコンテンツフィルタリングとインタラクションでQQ/Discordグループをモデレーション |
|
||||||
|
| **マルチプラットフォーム展開** | 1つのボットで全プラットフォームに対応。単一のダッシュボードから管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ライブデモ
|
||||||
|
|
||||||
|
**今すぐ試す:** https://demo.langbot.dev/
|
||||||
|
- メール: `demo@langbot.app`
|
||||||
|
- パスワード: `langbot123456`
|
||||||
|
|
||||||
|
*注意: 公開デモ環境です。機密情報を入力しないでください。*
|
||||||
|
|
||||||
|
## AI エージェントのために 🤖
|
||||||
|
|
||||||
|
LangBot は **設計段階からエージェントフレンドリー** です。お使いのコーディングエージェント(Claude Code、Codex、Copilot、Cursor など)が、ファーストクラスのサポートで LangBot を操作・拡張・デプロイできます:
|
||||||
|
|
||||||
|
- **MCP サーバー** —— LangBot は組み込みの [Model Context Protocol](https://modelcontextprotocol.io/) エンドポイント `/mcp` を公開し、HTTP API とミラーリングされているため、エージェントがボット・パイプライン・プラグイン・モデルをプログラム的に管理できます。同じ API キーで認証(`config.yaml` でグローバルキーを設定、またはユーザーキーを使用)—— ログインフロー不要。Web パネルの **API & MCP** タブで設定します。
|
||||||
|
- **リポジトリ内 Skills** —— [`skills/`](skills/) ディレクトリは LangBot を扱うための**唯一の信頼できる情報源**です:プラグイン開発、コア開発、E2E テスト、デプロイ、LangBot / LangBot Space MCP サーバーの操作。エージェントをこのディレクトリに向ければ、構築方法を理解します。
|
||||||
|
- **AGENTS.md** —— すべてのリポジトリに [`AGENTS.md`](AGENTS.md)(`CLAUDE.md` へのシンボリックリンク)があり、アーキテクチャ・規約、そして「API 変更時は MCP サーバーと skills を同期する」というルールを記述しています。
|
||||||
|
- **`llms.txt`** —— LLM 向けの機械可読なプロジェクトコンテキストを公式サイトで公開しています。
|
||||||
|
|
||||||
|
> **クラウド / マーケット:** [LangBot Space](https://space.langbot.app) も MCP サーバーを公開しており、エージェントが Personal Access Token で認証してプラグイン / MCP / Skill マーケットを検索・確認できます。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コミュニティ
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
- [Discord コミュニティ](https://discord.gg/wdNEHETs87)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 推移
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## コントリビューター
|
||||||
|
|
||||||
|
LangBot をより良くするために貢献してくださったすべての[コントリビューター](https://github.com/langbot-app/LangBot/graphs/contributors)に感謝します:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
<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/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></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)을 모든 채팅 플랫폼에 연결하여 대화, 작업 실행, 기존 워크플로우와의 통합이 가능한 지능형 에이전트를 만들 수 있습니다.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="LangBot 웹 관리 패널 대시보드 — 메시지 양, 모델 호출, 성공률, 활성 세션 실시간 모니터링" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 핵심 기능
|
||||||
|
|
||||||
|
- **AI 대화 및 에이전트** — 멀티턴 대화, 도구 호출, 멀티모달 지원, 스트리밍 출력. 내장 RAG(지식 베이스)와 [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org), [Deerflow](https://deerflow.tech), [Weknora](https://weknora.weixin.qq.com) 심층 통합.
|
||||||
|
- **유니버설 IM 플랫폼 지원** — 단일 코드베이스로 Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK 지원.
|
||||||
|
- **프로덕션 레디** — 접근 제어, 속도 제한, 민감어 필터링, 종합 모니터링 및 예외 처리. 기업 환경에서 검증됨.
|
||||||
|
- **플러그인 생태계** — 수백 개의 플러그인, 이벤트 기반 아키텍처, 컴포넌트 확장, [MCP 프로토콜](https://modelcontextprotocol.io/) 지원.
|
||||||
|
- **웹 관리 패널** — 직관적인 브라우저 인터페이스로 봇을 구성, 관리 및 모니터링. YAML 편집 불필요.
|
||||||
|
- **멀티 파이프라인 아키텍처** — 다양한 시나리오에 맞는 다양한 봇 구성, 종합 모니터링 및 예외 처리.
|
||||||
|
|
||||||
|
[→ 모든 기능 자세히 보기](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 실전 가이드: [5분 만에 멀티 플랫폼 AI 봇 배포하기](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [DeepSeek를 WeChat, Discord, Telegram에 연결하기](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [Dify Agent를 Discord, Telegram, Slack에서 실행하기](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/), [n8n 기반 챗봇 만들기](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 최신 정보 받기
|
||||||
|
|
||||||
|
리포지토리 오른쪽 상단의 Star 및 Watch 버튼을 클릭하여 최신 업데이트를 받으세요.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 빠른 시작
|
||||||
|
|
||||||
|
### ☁️ 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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 지원 플랫폼
|
||||||
|
|
||||||
|
| 플랫폼 | 상태 | 비고 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 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.302ai.cn/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`
|
||||||
|
|
||||||
|
*참고: 공개 데모 환경입니다. 민감한 정보를 입력하지 마세요.*
|
||||||
|
|
||||||
|
## AI 에이전트를 위한 설계 🤖
|
||||||
|
|
||||||
|
LangBot은 **설계 단계부터 에이전트 친화적**입니다 —— 코딩 에이전트(Claude Code, Codex, Copilot, Cursor 등)가 일급 지원으로 LangBot을 운영·확장·배포할 수 있습니다:
|
||||||
|
|
||||||
|
- **MCP 서버** —— LangBot은 내장 [Model Context Protocol](https://modelcontextprotocol.io/) 엔드포인트 `/mcp`를 제공하며, HTTP API와 동일하게 미러링되어 에이전트가 봇·파이프라인·플러그인·모델을 프로그래밍 방식으로 관리할 수 있습니다. 동일한 API 키로 인증하며(`config.yaml`에 전역 키 설정 또는 사용자 키 사용) 로그인 절차가 필요 없습니다. 웹 패널의 **API & MCP** 탭에서 설정합니다.
|
||||||
|
- **저장소 내 Skills** —— [`skills/`](skills/) 디렉터리는 LangBot 작업의 **단일 진실 공급원**입니다: 플러그인 개발, 코어 개발, E2E 테스트, 배포, LangBot / LangBot Space MCP 서버 운영. 에이전트를 이 디렉터리로 안내하면 빌드 방법을 알게 됩니다.
|
||||||
|
- **AGENTS.md** —— 모든 저장소에는 [`AGENTS.md`](AGENTS.md)(`CLAUDE.md`로 심볼릭 링크)가 있으며 아키텍처, 규약, 그리고 API 변경 시 MCP 서버와 skills를 동기화해야 한다는 규칙을 설명합니다.
|
||||||
|
- **`llms.txt`** —— LLM을 위한 기계 판독 가능한 프로젝트 컨텍스트가 웹사이트에 게시되어 있습니다.
|
||||||
|
|
||||||
|
> **클라우드 / 마켓플레이스:** [LangBot Space](https://space.langbot.app)도 MCP 서버를 제공하여 에이전트가 Personal Access Token으로 인증해 플러그인 / MCP / Skill 마켓플레이스를 검색하고 조회할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 커뮤니티
|
||||||
|
|
||||||
|
[](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>
|
||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
<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/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></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) с любой чат-платформой, позволяя создавать интеллектуальных агентов, которые могут вести диалоги, выполнять задачи и интегрироваться с вашими существующими рабочими процессами.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="Панель веб-управления LangBot — мониторинг объёма сообщений, вызовов моделей, успешности и активных сессий в реальном времени" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Ключевые возможности
|
||||||
|
|
||||||
|
- **ИИ-диалоги и агенты** — Многораундовые диалоги, вызов инструментов, мультимодальная поддержка, потоковый вывод. Встроенная реализация RAG (база знаний) с глубокой интеграцией в [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io), [Langflow](https://langflow.org), [Deerflow](https://deerflow.tech), [Weknora](https://weknora.weixin.qq.com).
|
||||||
|
- **Универсальная поддержка IM-платформ** — Единая кодовая база для Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Готовность к продакшену** — Контроль доступа, ограничение скорости, фильтрация чувствительных слов, комплексный мониторинг и обработка исключений. Проверено в корпоративной среде.
|
||||||
|
- **Экосистема плагинов** — Сотни плагинов, событийно-ориентированная архитектура, расширения компонентов и поддержка [протокола MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Веб-панель управления** — Настраивайте, управляйте и мониторьте ваших ботов через интуитивный браузерный интерфейс. Ручное редактирование YAML не требуется.
|
||||||
|
- **Мультиконвейерная архитектура** — Разные боты для разных сценариев с комплексным мониторингом и обработкой исключений.
|
||||||
|
|
||||||
|
[→ Подробнее обо всех возможностях](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 Практические руководства: [развернуть мультиплатформенного ИИ-бота за 5 минут](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [подключить DeepSeek к WeChat, Discord и Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [запустить Dify Agent в Discord, Telegram и Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) и [создать чат-бота на n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 Оставайтесь в курсе
|
||||||
|
|
||||||
|
Нажмите кнопки Star и Watch в правом верхнем углу репозитория, чтобы получать последние обновления.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Быстрый старт
|
||||||
|
|
||||||
|
### ☁️ 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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поддерживаемые платформы
|
||||||
|
|
||||||
|
| Платформа | Статус | Примечания |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| 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.302ai.cn/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`
|
||||||
|
|
||||||
|
*Примечание: Публичная демо-среда. Не вводите конфиденциальную информацию.*
|
||||||
|
|
||||||
|
## Создано для ИИ-агентов 🤖
|
||||||
|
|
||||||
|
LangBot **дружелюбен к агентам по своей архитектуре** —— ваши кодинг-агенты (Claude Code, Codex, Copilot, Cursor и др.) могут управлять, расширять и развёртывать LangBot с первоклассной поддержкой:
|
||||||
|
|
||||||
|
- **MCP-сервер** —— LangBot предоставляет встроенную конечную точку [Model Context Protocol](https://modelcontextprotocol.io/) по адресу `/mcp`, зеркалирующую HTTP API, чтобы агент мог программно управлять ботами, пайплайнами, плагинами и моделями. Аутентификация той же API-ключом (задайте глобальный ключ в `config.yaml` или используйте пользовательский ключ) —— без процедуры входа. Настраивается на вкладке **API & MCP** веб-панели.
|
||||||
|
- **Skills в репозитории** —— Каталог [`skills/`](skills/) является **единственным источником истины** для работы с LangBot: разработка плагинов, разработка ядра, сквозное тестирование, развёртывание и работа с MCP-серверами LangBot / LangBot Space. Направьте агента в этот каталог, и он будет знать, как собирать.
|
||||||
|
- **AGENTS.md** —— Каждый репозиторий содержит [`AGENTS.md`](AGENTS.md) (символическая ссылка на `CLAUDE.md`), описывающий архитектуру, соглашения и правило: изменения API должны синхронизировать MCP-сервер и skills.
|
||||||
|
- **`llms.txt`** —— Машиночитаемый контекст проекта для LLM опубликован на сайте.
|
||||||
|
|
||||||
|
> **Облако / Маркетплейс:** [LangBot Space](https://space.langbot.app) также предоставляет MCP-сервер, чтобы агенты могли искать и просматривать маркетплейс плагинов / MCP / skills, аутентифицируясь с помощью Personal Access Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сообщество
|
||||||
|
|
||||||
|
[](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>
|
||||||
+160
-87
@@ -1,128 +1,149 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://langbot.app">
|
<a href="https://langbot.app">
|
||||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
<img width="130" src="res/logo-blue.png" alt="LangBot"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div align="center"><a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<div align="center">
|
||||||
|
|
||||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / (PR for your language)
|
<a href="https://hellogithub.com/repository/langbot-app/LangBot" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=5ce8ae2aa4f74316bf393b57b952433c&claim_uid=gtmc6YWjMZkT21R" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
<h3>生產級 AI 即時通訊機器人開發平台。</h3>
|
||||||
|
<h4>快速建構、除錯和部署 AI 機器人到微信、QQ、飛書、Slack、Discord、Telegram 等平台。</h4>
|
||||||
|
|
||||||
|
[English](README.md) / [简体中文](README_CN.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||||
|
|
||||||
[](https://discord.gg/wdNEHETs87)
|
[](https://discord.gg/wdNEHETs87)
|
||||||
[](https://qm.qq.com/q/JLi38whHum)
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
[](https://deepwiki.com/langbot-app/LangBot)
|
[](https://deepwiki.com/langbot-app/LangBot)
|
||||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||||
|
[](https://github.com/langbot-app/LangBot/stargazers)
|
||||||
[](https://gitcode.com/RockChinQ/LangBot)
|
[](https://gitcode.com/RockChinQ/LangBot)
|
||||||
|
|
||||||
<a href="https://langbot.app">主頁</a> |
|
<a href="https://langbot.app">官網</a> |
|
||||||
<a href="https://docs.langbot.app/zh/insight/guide.html">部署文件</a> |
|
<a href="https://link.langbot.app/zh/docs/features">特性</a> |
|
||||||
<a href="https://docs.langbot.app/zh/plugin/plugin-intro.html">外掛介紹</a> |
|
<a href="https://link.langbot.app/zh/docs/guide">文件</a> |
|
||||||
<a href="https://github.com/langbot-app/LangBot/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交外掛</a>
|
<a href="https://link.langbot.app/zh/docs/api">API</a> |
|
||||||
|
<a href="https://space.langbot.app">外掛市場</a> |
|
||||||
|
<a href="https://langbot.featurebase.app/roadmap">路線圖</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
|
---
|
||||||
|
|
||||||
## 📦 開始使用
|
## 什麼是 LangBot?
|
||||||
|
|
||||||
#### Docker Compose 部署
|
LangBot 是一個**開源的生產級平台**,用於建構 AI 驅動的即時通訊機器人。它將大語言模型(LLM)連接到各種聊天平台,幫助你創建能夠對話、執行任務、並整合到現有工作流程中的智能 Agent。
|
||||||
|
|
||||||
```bash
|
<p align="center">
|
||||||
git clone https://github.com/langbot-app/LangBot
|
<img src="res/dashboard-overview.png" alt="LangBot Web 管理面板儀表板 — 即時監控訊息量、模型調用、成功率與活躍工作階段" width="720"/>
|
||||||
cd LangBot/docker
|
</p>
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
訪問 http://localhost:5300 即可開始使用。
|
### 核心能力
|
||||||
|
|
||||||
詳細文件[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
- **AI 對話與 Agent** — 多輪對話、工具調用、多模態、流式輸出。自帶 RAG(知識庫),深度整合 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)、[Langflow](https://langflow.org)、 [Deerflow](https://deerflow.tech)、[Weknora](https://weknora.weixin.qq.com)等 LLMOps 平台。
|
||||||
|
- **全平台支援** — 一套程式碼,覆蓋 QQ、微信、企業微信、飛書、釘釘、Discord、Telegram、Slack、LINE、KOOK 等平台。
|
||||||
|
- **生產就緒** — 存取控制、限速、敏感詞過濾、全面監控與異常處理,已被多家企業採用。
|
||||||
|
- **外掛生態** — 數百個外掛,事件驅動架構,組件擴展,適配 [MCP 協議](https://modelcontextprotocol.io/)。
|
||||||
|
- **Web 管理面板** — 透過瀏覽器直觀地配置、管理和監控機器人,無需手動編輯設定檔。
|
||||||
|
- **多流水線架構** — 不同機器人用於不同場景,具備全面的監控和異常處理能力。
|
||||||
|
|
||||||
#### 寶塔面板部署
|
[→ 了解更多功能特性](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
已上架寶塔面板,若您已安裝寶塔面板,可以根據[文件](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
📍 實踐指南:[5 分鐘部署多平台 AI 機器人](https://blog.langbot.app/zh/blog/deploy-ai-bot-in-5-minutes/)、[將 DeepSeek 接入微信、企業微信與 Discord](https://blog.langbot.app/zh/blog/connect-deepseek-to-wechat/)、[讓 Dify Agent 跑在 Discord、Telegram 和 Slack 上](https://blog.langbot.app/zh/blog/dify-agent-discord-telegram-slack/),以及[用 n8n 建構多平台 AI 聊天機器人](https://blog.langbot.app/zh/blog/n8n-multi-platform-ai-chatbot/)。
|
||||||
|
|
||||||
#### Zeabur 雲端部署
|
---
|
||||||
|
|
||||||
社群貢獻的 Zeabur 模板。
|
|
||||||
|
|
||||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
|
||||||
|
|
||||||
#### Railway 雲端部署
|
|
||||||
|
|
||||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
|
||||||
|
|
||||||
#### 手動部署
|
|
||||||
|
|
||||||
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
|
||||||
|
|
||||||
## 😎 保持更新
|
## 😎 保持更新
|
||||||
|
|
||||||
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## ✨ 特性
|
## 快速開始
|
||||||
|
|
||||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)。
|
### ☁️ LangBot Cloud(推薦)
|
||||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
|
||||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
|
||||||
- 🧩 外掛擴展、活躍社群:支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
|
||||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
|
||||||
|
|
||||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
**[LangBot Cloud](https://space.langbot.app/cloud)** — 免部署,開箱即用。
|
||||||
|
|
||||||
或訪問 demo 環境:https://demo.langbot.dev/
|
### 一鍵啟動
|
||||||
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
|
|
||||||
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
|
|
||||||
|
|
||||||
### 訊息平台
|
```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 --profile all 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](https://docs.langbot.app/zh/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支援的平台
|
||||||
|
|
||||||
| 平台 | 狀態 | 備註 |
|
| 平台 | 狀態 | 備註 |
|
||||||
| --- | --- | --- |
|
|------|------|------|
|
||||||
| Discord | ✅ | |
|
| Discord | ✅ | 官方 |
|
||||||
| Telegram | ✅ | |
|
| Telegram | ✅ | 官方 |
|
||||||
| Slack | ✅ | |
|
| Slack | ✅ | 官方 |
|
||||||
| LINE | ✅ | |
|
| LINE | ✅ | 官方 |
|
||||||
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
|
| QQ | ✅ | 個人號、官方機器人(頻道、私聊、群聊) |
|
||||||
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
|
| 企業微信 | ✅ | 應用訊息、對外客服、智能機器人 |
|
||||||
| 微信 | ✅ | |
|
| 微信 | ✅ | 個人微信、微信公眾號 |
|
||||||
| 企微對外客服 | ✅ | |
|
| 飛書 | ✅ | 官方 |
|
||||||
| 企微智能機器人 | ✅ | |
|
| 釘釘 | ✅ | 官方 |
|
||||||
| 微信公眾號 | ✅ | |
|
| KOOK | ✅ | 官方 |
|
||||||
| Lark | ✅ | |
|
| Satori | ✅ | |
|
||||||
| DingTalk | ✅ | |
|
| Email | ✅ | 只 Matrix、Satori |
|
||||||
|
| Matrix | ✅ | 支援多種橋接平台,如 Signal、WhatsApp、Messenger、iMessage、Mattermost、Google Chat、IRC、XMPP、Zulip 等 |
|
||||||
|
|
||||||
### 大模型能力
|
---
|
||||||
|
|
||||||
| 模型 | 狀態 | 備註 |
|
## 支援的大模型與整合
|
||||||
| --- | --- | --- |
|
|
||||||
| [OpenAI](https://platform.openai.com/) | ✅ | 可接入任何 OpenAI 介面格式模型 |
|
|
||||||
| [DeepSeek](https://www.deepseek.com/) | ✅ | |
|
|
||||||
| [Moonshot](https://www.moonshot.cn/) | ✅ | |
|
|
||||||
| [Anthropic](https://www.anthropic.com/) | ✅ | |
|
|
||||||
| [xAI](https://x.ai/) | ✅ | |
|
|
||||||
| [智譜AI](https://open.bigmodel.cn/) | ✅ | |
|
|
||||||
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | 大模型和 GPU 資源平台 |
|
|
||||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | 大模型聚合平台 |
|
|
||||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
|
||||||
| [Dify](https://dify.ai) | ✅ | LLMOps 平台 |
|
|
||||||
| [Ollama](https://ollama.com/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [LMStudio](https://lmstudio.ai/) | ✅ | 本地大模型運行平台 |
|
|
||||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | 大模型介面聚合平台 |
|
|
||||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | 大模型聚合平台 |
|
|
||||||
| [阿里雲百煉](https://bailian.console.aliyun.com/) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | 大模型聚合平台, LLMOps 平台 |
|
|
||||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | 大模型聚合平台 |
|
|
||||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | 支援通過 MCP 協議獲取工具 |
|
|
||||||
|
|
||||||
### TTS
|
| 提供商 | 類型 | 狀態 |
|
||||||
|
|--------|------|------|
|
||||||
|
| [OpenAI](https://platform.openai.com/) | LLM | ✅ |
|
||||||
|
| [Anthropic](https://www.anthropic.com/) | LLM | ✅ |
|
||||||
|
| [DeepSeek](https://www.deepseek.com/) | LLM | ✅ |
|
||||||
|
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | LLM | ✅ |
|
||||||
|
| [xAI](https://x.ai/) | LLM | ✅ |
|
||||||
|
| [Moonshot](https://www.moonshot.cn/) | LLM | ✅ |
|
||||||
|
| [智譜AI](https://open.bigmodel.cn/) | LLM | ✅ |
|
||||||
|
| [Ollama](https://ollama.com/) | 本地 LLM | ✅ |
|
||||||
|
| [LM Studio](https://lmstudio.ai/) | 本地 LLM | ✅ |
|
||||||
|
| [Dify](https://dify.ai) | LLMOps | ✅ |
|
||||||
|
| [MCP](https://modelcontextprotocol.io/) | 協議 | ✅ |
|
||||||
|
| [SiliconFlow](https://siliconflow.cn/) | 聚合平台 | ✅ |
|
||||||
|
| [阿里雲百煉](https://bailian.console.aliyun.com/) | 聚合平台 | ✅ |
|
||||||
|
| [火山方舟](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | 聚合平台 | ✅ |
|
||||||
|
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | 聚合平台 | ✅ |
|
||||||
|
| [GiteeAI](https://ai.gitee.com/) | 聚合平台 | ✅ |
|
||||||
|
| [勝算雲](https://www.shengsuanyun.com/?from=CH_KYIPP758) | GPU 平台 | ✅ |
|
||||||
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | GPU 平台 | ✅ |
|
||||||
|
| [接口 AI](https://jiekou.ai/) | 聚合平台 | ✅ |
|
||||||
|
| [302.AI](https://share.302ai.cn/SuTG99) | 聚合平台 | ✅ |
|
||||||
|
| [Qiniu](https://www.qiniu.com/ai/agent) | 聚合平台 | ✅ |
|
||||||
|
|
||||||
|
### TTS(語音合成)
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||||
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||||
@@ -130,13 +151,65 @@ docker compose up -d
|
|||||||
### 文生圖
|
### 文生圖
|
||||||
|
|
||||||
| 平台/模型 | 備註 |
|
| 平台/模型 | 備註 |
|
||||||
| --- | --- |
|
|-----------|------|
|
||||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin) |
|
||||||
|
|
||||||
## 😘 社群貢獻
|
[→ 查看完整整合列表](https://link.langbot.app/zh/docs/features)
|
||||||
|
|
||||||
感謝以下[程式碼貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)和社群裡其他成員對 LangBot 的貢獻:
|
---
|
||||||
|
|
||||||
|
## 為什麼選擇 LangBot?
|
||||||
|
|
||||||
|
| 使用場景 | LangBot 如何幫助 |
|
||||||
|
|----------|------------------|
|
||||||
|
| **客戶服務** | 將 AI Agent 部署到微信/企微/釘釘/飛書,基於知識庫自動回答使用者問題 |
|
||||||
|
| **內部工具** | 將 n8n/Dify 工作流接入企微/釘釘,實現業務流程自動化 |
|
||||||
|
| **社群運營** | 在 QQ/Discord 群中使用 AI 驅動的內容審核與智能互動 |
|
||||||
|
| **多平台觸達** | 一個機器人,覆蓋所有平台。透過統一面板集中管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 線上演示
|
||||||
|
|
||||||
|
**立即體驗:** https://demo.langbot.dev/
|
||||||
|
- 信箱:`demo@langbot.app`
|
||||||
|
- 密碼:`langbot123456`
|
||||||
|
|
||||||
|
*注意:公開演示環境,請不要在其中填入任何敏感資訊。*
|
||||||
|
|
||||||
|
## 為 AI Agent 而生 🤖
|
||||||
|
|
||||||
|
LangBot **從設計上就對 Agent 友善** —— 你的編碼 Agent(Claude Code、Codex、Copilot、Cursor 等)可以一等公民般地操作、擴充和部署 LangBot:
|
||||||
|
|
||||||
|
- **MCP Server** —— LangBot 內建 [Model Context Protocol](https://modelcontextprotocol.io/) 端點 `/mcp`,與 HTTP API 對齊,Agent 可程式化管理機器人、流水線、外掛和模型。使用同一套 API Key 鑑權(可在 `config.yaml` 設定全域 Key,或使用使用者 Key),無需登入流程。在 Web 面板的 **API 與 MCP** 分頁中設定。
|
||||||
|
- **倉庫內 Skills** —— [`skills/`](skills/) 目錄是使用 LangBot 的**唯一事實來源**:外掛開發、核心開發、端到端測試、部署,以及操作 LangBot / LangBot Space MCP Server。把 Agent 指向這個目錄,它就知道如何動手。
|
||||||
|
- **AGENTS.md** —— 每個倉庫都提供 [`AGENTS.md`](AGENTS.md)(軟連結到 `CLAUDE.md`),描述架構、規範,以及「API 變更必須同步更新 MCP Server 和 skills」的約定。
|
||||||
|
- **`llms.txt`** —— 面向 LLM 的機器可讀專案上下文已發布在官網。
|
||||||
|
|
||||||
|
> **雲端 / 市集:** [LangBot Space](https://space.langbot.app) 同樣開放 MCP Server,Agent 可搜尋和檢視外掛 / MCP / Skill 市集,使用 Personal Access Token 鑑權。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 社群
|
||||||
|
|
||||||
|
[](https://discord.gg/wdNEHETs87)
|
||||||
|
[](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
- [Discord 社群](https://discord.gg/wdNEHETs87)
|
||||||
|
- [QQ 社群群](https://qm.qq.com/q/JLi38whHum)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Star 趨勢
|
||||||
|
|
||||||
|
[](https://star-history.com/#langbot-app/LangBot&Date)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 貢獻者
|
||||||
|
|
||||||
|
感謝所有[貢獻者](https://github.com/langbot-app/LangBot/graphs/contributors)對 LangBot 的幫助:
|
||||||
|
|
||||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
<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/launches/langbot?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-langbot" target="_blank" rel="noopener noreferrer"><img alt="LangBot - Easy-to-use global IM bot platform designed for the LLM era | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=979554&theme=light&t=1782822143403"></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.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/dashboard-overview.png" alt="Bảng điều khiển quản lý web LangBot — giám sát thời gian thực khối lượng tin nhắn, lệnh gọi mô hình, tỷ lệ thành công và phiên hoạt động" width="720"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 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), [Deerflow](https://deerflow.tech), [Weknora](https://weknora.weixin.qq.com).
|
||||||
|
- **Hỗ trợ đa nền tảng IM** — Một mã nguồn cho Discord, Telegram, Slack, LINE, QQ, WeChat, WeCom, Lark, DingTalk, KOOK.
|
||||||
|
- **Sẵn sàng cho sản xuất** — Kiểm soát truy cập, giới hạn tốc độ, lọc từ nhạy cảm, giám sát toàn diện và xử lý ngoại lệ. Được doanh nghiệp tin dùng.
|
||||||
|
- **Hệ sinh thái Plugin** — Hàng trăm plugin, kiến trúc hướng sự kiện, mở rộng thành phần, và hỗ trợ [giao thức MCP](https://modelcontextprotocol.io/).
|
||||||
|
- **Bảng quản lý Web** — Cấu hình, quản lý và giám sát bot thông qua giao diện trình duyệt trực quan. Không cần chỉnh sửa YAML.
|
||||||
|
- **Kiến trúc đa Pipeline** — Các bot khác nhau cho các kịch bản khác nhau, với giám sát toàn diện và xử lý ngoại lệ.
|
||||||
|
|
||||||
|
[→ Tìm hiểu thêm về tất cả tính năng](https://link.langbot.app/en/docs/features)
|
||||||
|
|
||||||
|
📍 Hướng dẫn thực hành: [triển khai bot AI đa nền tảng trong 5 phút](https://blog.langbot.app/en/blog/deploy-ai-bot-in-5-minutes/), [kết nối DeepSeek với WeChat, Discord và Telegram](https://blog.langbot.app/en/blog/connect-deepseek-to-wechat/), [chạy Dify Agent trên Discord, Telegram và Slack](https://blog.langbot.app/en/blog/dify-agent-discord-telegram-slack/) và [xây dựng chatbot với n8n](https://blog.langbot.app/en/blog/n8n-multi-platform-ai-chatbot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 😎 Cập nhật Mới nhất
|
||||||
|
|
||||||
|
Nhấp vào các nút Star và Watch ở góc trên bên phải của kho lưu trữ để nhận các bản cập nhật mới nhất.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 --profile all 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](https://docs.langbot.app/en/deploy/langbot/kubernetes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.302ai.cn/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 xây dựng cho AI Agent 🤖
|
||||||
|
|
||||||
|
LangBot **thân thiện với agent ngay từ thiết kế** —— các coding agent của bạn (Claude Code, Codex, Copilot, Cursor, …) có thể vận hành, mở rộng và triển khai LangBot với sự hỗ trợ hạng nhất:
|
||||||
|
|
||||||
|
- **MCP Server** —— LangBot cung cấp endpoint [Model Context Protocol](https://modelcontextprotocol.io/) tích hợp tại `/mcp`, phản chiếu HTTP API để agent quản lý bot, pipeline, plugin và model theo cách lập trình. Xác thực bằng cùng một API key (đặt key toàn cục trong `config.yaml` hoặc dùng key theo người dùng) —— không cần luồng đăng nhập. Cấu hình tại tab **API & MCP** trong bảng điều khiển Web.
|
||||||
|
- **Skills trong repo** —— Thư mục [`skills/`](skills/) là **nguồn sự thật duy nhất** để làm việc với LangBot: phát triển plugin, phát triển core, kiểm thử end-to-end, triển khai và vận hành MCP Server của LangBot / LangBot Space. Trỏ agent của bạn vào thư mục này và nó sẽ biết cách xây dựng.
|
||||||
|
- **AGENTS.md** —— Mỗi repo đều có [`AGENTS.md`](AGENTS.md) (liên kết tượng trưng tới `CLAUDE.md`) mô tả kiến trúc, quy ước và quy tắc rằng thay đổi API phải giữ MCP Server và skills đồng bộ.
|
||||||
|
- **`llms.txt`** —— Ngữ cảnh dự án có thể đọc bằng máy dành cho LLM được công bố trên website.
|
||||||
|
|
||||||
|
> **Cloud / Marketplace:** [LangBot Space](https://space.langbot.app) cũng cung cấp MCP Server để agent tìm kiếm và kiểm tra marketplace plugin / MCP / skill, xác thực bằng Personal Access Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare YAML node definitions with frontend node-configs."""
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
# 1. Parse YAML files
|
||||||
|
yaml_dir = 'src/langbot/templates/metadata/nodes'
|
||||||
|
yaml_nodes = {}
|
||||||
|
|
||||||
|
for filename in sorted(os.listdir(yaml_dir)):
|
||||||
|
if filename.endswith('.yaml'):
|
||||||
|
filepath = os.path.join(yaml_dir, filename)
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
node_name = data.get('name', filename.replace('.yaml', ''))
|
||||||
|
yaml_nodes[node_name] = {
|
||||||
|
'category': data.get('category', ''),
|
||||||
|
'inputs': [i['name'] for i in data.get('inputs', [])],
|
||||||
|
'outputs': [o['name'] for o in data.get('outputs', [])],
|
||||||
|
'config': [c['name'] for c in data.get('config', [])]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Parse frontend node-configs TypeScript files
|
||||||
|
node_configs_dir = 'web/src/app/home/workflows/components/workflow-editor/node-configs'
|
||||||
|
|
||||||
|
frontend_nodes = {}
|
||||||
|
|
||||||
|
def parse_ts_file(filepath):
|
||||||
|
"""Parse a TypeScript file to extract node configurations."""
|
||||||
|
with open(filepath, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find all node type definitions
|
||||||
|
# Pattern: nodeType: 'xxx'
|
||||||
|
node_type_pattern = r"nodeType:\s*'([^']+)'"
|
||||||
|
node_types = re.findall(node_type_pattern, content)
|
||||||
|
|
||||||
|
# For each node type, extract inputs, outputs, and config
|
||||||
|
for node_type in node_types:
|
||||||
|
# Find the config object for this node type
|
||||||
|
# Look for the section between this nodeType and the next one or end of object
|
||||||
|
pattern = rf"nodeType:\s*'({re.escape(node_type)})'.*?(?=nodeType:|export\s+(const|function)|$)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
section = match.group(0)
|
||||||
|
|
||||||
|
# Extract inputs
|
||||||
|
inputs = re.findall(r"createInput\('([^']+)'", section)
|
||||||
|
|
||||||
|
# Extract outputs
|
||||||
|
outputs = re.findall(r"createOutput\('([^']+)'", section)
|
||||||
|
|
||||||
|
# Extract config names
|
||||||
|
config_names = re.findall(r"name:\s*'([^']+)'", section)
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_config = []
|
||||||
|
for c in config_names:
|
||||||
|
if c not in seen:
|
||||||
|
seen.add(c)
|
||||||
|
unique_config.append(c)
|
||||||
|
|
||||||
|
frontend_nodes[node_type] = {
|
||||||
|
'inputs': inputs,
|
||||||
|
'outputs': outputs,
|
||||||
|
'config': unique_config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse all config files
|
||||||
|
for filename in os.listdir(node_configs_dir):
|
||||||
|
if filename.endswith('.ts') and filename != 'types.ts' and filename != 'index.ts':
|
||||||
|
filepath = os.path.join(node_configs_dir, filename)
|
||||||
|
parse_ts_file(filepath)
|
||||||
|
|
||||||
|
# 3. Compare and report differences
|
||||||
|
print("=" * 80)
|
||||||
|
print("WORKFLOW NODE COMPARISON REPORT: YAML vs Frontend")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
all_node_types = sorted(set(list(yaml_nodes.keys()) + list(frontend_nodes.keys())))
|
||||||
|
|
||||||
|
discrepancies = []
|
||||||
|
|
||||||
|
for node_type in all_node_types:
|
||||||
|
yaml_def = yaml_nodes.get(node_type)
|
||||||
|
frontend_def = frontend_nodes.get(node_type)
|
||||||
|
|
||||||
|
node_discrepancies = []
|
||||||
|
|
||||||
|
if not yaml_def:
|
||||||
|
print(f"\n⚠️ {node_type}: ONLY in frontend (not in YAML)")
|
||||||
|
continue
|
||||||
|
if not frontend_def:
|
||||||
|
print(f"\n⚠️ {node_type}: ONLY in YAML (not in frontend)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Compare inputs
|
||||||
|
yaml_inputs = set(yaml_def['inputs'])
|
||||||
|
frontend_inputs = set(frontend_def['inputs'])
|
||||||
|
if yaml_inputs != frontend_inputs:
|
||||||
|
only_yaml = yaml_inputs - frontend_inputs
|
||||||
|
only_frontend = frontend_inputs - yaml_inputs
|
||||||
|
node_discrepancies.append({
|
||||||
|
'type': 'inputs',
|
||||||
|
'only_yaml': list(only_yaml),
|
||||||
|
'only_frontend': list(only_frontend)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Compare outputs
|
||||||
|
yaml_outputs = set(yaml_def['outputs'])
|
||||||
|
frontend_outputs = set(frontend_def['outputs'])
|
||||||
|
if yaml_outputs != frontend_outputs:
|
||||||
|
only_yaml = yaml_outputs - frontend_outputs
|
||||||
|
only_frontend = frontend_outputs - yaml_outputs
|
||||||
|
node_discrepancies.append({
|
||||||
|
'type': 'outputs',
|
||||||
|
'only_yaml': list(only_yaml),
|
||||||
|
'only_frontend': list(only_frontend)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Compare config
|
||||||
|
yaml_config = set(yaml_def['config'])
|
||||||
|
frontend_config = set(frontend_def['config'])
|
||||||
|
if yaml_config != frontend_config:
|
||||||
|
only_yaml = yaml_config - frontend_config
|
||||||
|
only_frontend = frontend_config - yaml_config
|
||||||
|
node_discrepancies.append({
|
||||||
|
'type': 'config',
|
||||||
|
'only_yaml': list(only_yaml),
|
||||||
|
'only_frontend': list(only_frontend)
|
||||||
|
})
|
||||||
|
|
||||||
|
if node_discrepancies:
|
||||||
|
print(f"\n❌ {node_type} ({yaml_def['category']}): HAS DISCREPANCIES")
|
||||||
|
for d in node_discrepancies:
|
||||||
|
print(f" {d['type']}:")
|
||||||
|
if d['only_yaml']:
|
||||||
|
print(f" Only in YAML: {d['only_yaml']}")
|
||||||
|
if d['only_frontend']:
|
||||||
|
print(f" Only in Frontend: {d['only_frontend']}")
|
||||||
|
discrepancies.append((node_type, node_discrepancies))
|
||||||
|
else:
|
||||||
|
print(f"\n✅ {node_type} ({yaml_def['category']}): OK")
|
||||||
|
|
||||||
|
print(f"\n{'=' * 80}")
|
||||||
|
print(f"SUMMARY: {len(discrepancies)} nodes with discrepancies out of {len(all_node_types)} total")
|
||||||
|
print(f"{'=' * 80}")
|
||||||
|
|
||||||
|
# Output as JSON for further processing
|
||||||
|
output = {
|
||||||
|
'yaml_nodes': {k: v for k, v in yaml_nodes.items()},
|
||||||
|
'frontend_nodes': {k: v for k, v in frontend_nodes.items()},
|
||||||
|
'discrepancies': {k: v for k, v in discrepancies}
|
||||||
|
}
|
||||||
|
|
||||||
|
with open('node_comparison.json', 'w') as f:
|
||||||
|
json.dump(output, f, indent=2)
|
||||||
|
|
||||||
|
print(f"\nDetailed comparison saved to node_comparison.json")
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick test script for LangBot Kubernetes deployment
|
||||||
|
# This script helps you test the Kubernetes deployment locally
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 LangBot Kubernetes Deployment Test Script"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for kubectl
|
||||||
|
if ! command -v kubectl &> /dev/null; then
|
||||||
|
echo "❌ kubectl is not installed. Please install kubectl first."
|
||||||
|
echo "Visit: https://kubernetes.io/docs/tasks/tools/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ kubectl is installed"
|
||||||
|
|
||||||
|
# Check if kubectl can connect to a cluster
|
||||||
|
if ! kubectl cluster-info &> /dev/null; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ No Kubernetes cluster found."
|
||||||
|
echo ""
|
||||||
|
echo "To test locally, you can use:"
|
||||||
|
echo " - kind: https://kind.sigs.k8s.io/"
|
||||||
|
echo " - minikube: https://minikube.sigs.k8s.io/"
|
||||||
|
echo " - k3s: https://k3s.io/"
|
||||||
|
echo ""
|
||||||
|
echo "Example with kind:"
|
||||||
|
echo " kind create cluster --name langbot-test"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Connected to Kubernetes cluster"
|
||||||
|
kubectl cluster-info
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Ask user to confirm
|
||||||
|
read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Deployment cancelled."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📦 Deploying LangBot..."
|
||||||
|
kubectl apply -f kubernetes.yaml
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ Waiting for pods to be ready..."
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s
|
||||||
|
kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Deployment status:"
|
||||||
|
kubectl get all -n langbot
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🌐 To access LangBot Web UI, run:"
|
||||||
|
echo " kubectl port-forward -n langbot svc/langbot 5300:5300"
|
||||||
|
echo ""
|
||||||
|
echo "Then visit: http://localhost:5300"
|
||||||
|
echo ""
|
||||||
|
echo "📝 To view logs:"
|
||||||
|
echo " kubectl logs -n langbot -l app=langbot -f"
|
||||||
|
echo ""
|
||||||
|
echo "🗑️ To uninstall:"
|
||||||
|
echo " kubectl delete namespace langbot"
|
||||||
|
echo ""
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Docker Compose configuration for LangBot
|
||||||
|
# For Kubernetes deployment, see kubernetes.yaml and the deployment guide at https://docs.langbot.app
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -12,7 +14,41 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||||
|
networks:
|
||||||
|
- langbot_network
|
||||||
|
|
||||||
|
# The Box sandbox runtime is optional. It is only started when you run
|
||||||
|
# ``docker compose --profile box up`` (or ``docker compose --profile all
|
||||||
|
# up``). With Box off, LangBot keeps the dashboard / skills list visible
|
||||||
|
# (read-only) but disables sandbox tools, skill add/edit and stdio MCP —
|
||||||
|
# set ``box.enabled: false`` in ``data/config.yaml`` (or
|
||||||
|
# ``BOX__ENABLED=false`` in the langbot service env below) to match.
|
||||||
|
langbot_box:
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
container_name: langbot_box
|
||||||
|
profiles: ["box", "all"]
|
||||||
|
volumes:
|
||||||
|
# Keep the source and target path identical because langbot_box uses the
|
||||||
|
# host Docker socket to create sandbox containers. Override
|
||||||
|
# LANGBOT_BOX_ROOT with an absolute path if you do not want the default.
|
||||||
|
- ${LANGBOT_BOX_ROOT:-${PWD}/data/box}:${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
|
# Mount container runtime socket for Box sandbox backend.
|
||||||
|
# Uncomment the one that matches your container runtime:
|
||||||
|
# - /var/run/podman/podman.sock:/var/run/podman/podman.sock # Podman
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock # Docker
|
||||||
|
restart: on-failure
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
# The Box runtime does NOT read box.local.* from config.yaml or env; it
|
||||||
|
# receives its configuration from LangBot via the INIT RPC action.
|
||||||
|
# Do not add LANGBOT_BOX_* / BOX__* here — they would be silently ignored.
|
||||||
|
# Launched through the same CLI entry point as the plugin runtime
|
||||||
|
# (`langbot_plugin.cli.__init__ <subcommand>`). WebSocket is the default
|
||||||
|
# control transport — mirrors `rt`, which also runs with no flag. Pass
|
||||||
|
# `-s` / `--stdio-control` only for the stdio mode LangBot uses outside
|
||||||
|
# containers.
|
||||||
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "box"]
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
@@ -21,13 +57,20 @@ services:
|
|||||||
container_name: langbot
|
container_name: langbot
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./plugins:/app/plugins
|
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
# Unified env-override convention: SECTION__SUBSECTION__KEY overrides the
|
||||||
|
# matching config.yaml field (see LoadConfigStage). These map onto
|
||||||
|
# box.* and are forwarded to the Box runtime via INIT RPC.
|
||||||
|
- BOX__LOCAL__HOST_ROOT=${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
|
- BOX__LOCAL__DEFAULT_WORKSPACE=default
|
||||||
|
- BOX__LOCAL__SKILLS_ROOT=skills
|
||||||
|
- BOX__LOCAL__ALLOWED_MOUNT_ROOTS=${LANGBOT_BOX_ROOT:-${PWD}/data/box}
|
||||||
|
- BOX__DOCKER__CPU_LIMIT_ENABLED=${LANGBOT_BOX_DOCKER_CPU_LIMIT_ENABLED:-true}
|
||||||
ports:
|
ports:
|
||||||
- 5300:5300 # For web ui
|
- 5300:5300 # For web ui and webhook callback
|
||||||
- 2280-2290:2280-2290 # For platform webhook
|
- 2280-2285:2280-2285 # For platform reverse connection
|
||||||
networks:
|
networks:
|
||||||
- langbot_network
|
- langbot_network
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,571 @@
|
|||||||
|
# Kubernetes Deployment for LangBot
|
||||||
|
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
|
||||||
|
#
|
||||||
|
# Full deployment guide (zh/en/ja): https://docs.langbot.app -> Installation -> Kubernetes
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# - For the Box sandbox runtime: a node with a reachable Docker daemon
|
||||||
|
# (the box mounts the node's /var/run/docker.sock). See the deployment guide.
|
||||||
|
#
|
||||||
|
# Components:
|
||||||
|
# - Namespace: langbot
|
||||||
|
# - PersistentVolumeClaims for data persistence
|
||||||
|
# - Deployments for langbot, langbot-plugin-runtime, and langbot-box (sandbox)
|
||||||
|
# - Services for network access
|
||||||
|
# - ConfigMap for timezone + runtime endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
# 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"
|
||||||
|
# Box sandbox runtime endpoint. LangBot connects to the Box runtime over
|
||||||
|
# WebSocket. The hostname MUST match the langbot-box Service name. Note the
|
||||||
|
# in-container default ("langbot_box") uses an underscore, which is an
|
||||||
|
# invalid Kubernetes DNS name — so the endpoint is always set explicitly here.
|
||||||
|
BOX__RUNTIME__ENDPOINT: "ws://langbot-box:5410"
|
||||||
|
|
||||||
|
---
|
||||||
|
# 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 Box (sandbox) runtime
|
||||||
|
#
|
||||||
|
# The Box runtime backs LangBot's sandbox tools (exec / read / write / edit /
|
||||||
|
# glob / grep), the `activate` skill tool, skill add/edit, and stdio-mode MCP
|
||||||
|
# servers. It is OPTIONAL: if you do not deploy it, set `BOX__ENABLED=false` on
|
||||||
|
# the langbot Deployment (or `box.enabled: false` in config.yaml) so the
|
||||||
|
# dashboard renders cleanly with sandbox features disabled.
|
||||||
|
#
|
||||||
|
# IMPORTANT — how the sandbox actually runs:
|
||||||
|
# The bundled image ships only the Docker CLI (no dockerd, no nsjail). The Box
|
||||||
|
# runtime therefore creates sandbox containers by talking to a Docker daemon
|
||||||
|
# over the mounted socket (`/var/run/docker.sock`). Because that daemon
|
||||||
|
# resolves bind-mount paths on the NODE filesystem, the Box workspace root
|
||||||
|
# must be the SAME absolute path inside the box container, inside every
|
||||||
|
# sandbox container it spawns, AND on the node. That is why this manifest uses
|
||||||
|
# a hostPath at a fixed absolute path (/app/data/box) and pins langbot + box
|
||||||
|
# to the same node via podAffinity. A normal PVC will NOT work for the box
|
||||||
|
# workspace, because the node's dockerd cannot see paths that exist only
|
||||||
|
# inside the pod's mount namespace.
|
||||||
|
#
|
||||||
|
# Security note: mounting the host Docker socket grants the Box runtime (and any
|
||||||
|
# code executed in the sandbox) effective root on the node. Only deploy Box on
|
||||||
|
# nodes you trust for this workload, ideally a dedicated node pool. For a
|
||||||
|
# stronger isolation boundary, switch box.backend to 'e2b' (set E2B_API_KEY) and
|
||||||
|
# drop the docker.sock mount + hostPath entirely.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langbot-box
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-box
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot-box
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langbot-box
|
||||||
|
spec:
|
||||||
|
# Pin to the same node as langbot so they share the hostPath box root.
|
||||||
|
affinity:
|
||||||
|
podAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: langbot
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
containers:
|
||||||
|
- name: langbot-box
|
||||||
|
image: rockchin/langbot:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
# Launched through the same CLI entry point as the plugin runtime.
|
||||||
|
# No flag => WebSocket control transport (default), listening on 5410.
|
||||||
|
command: ["uv", "run", "--no-sync", "-m", "langbot_plugin.cli.__init__", "box"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 5410
|
||||||
|
name: box-rpc
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: TZ
|
||||||
|
# The Box runtime does NOT read box.local.* / BOX__* from its own env;
|
||||||
|
# it receives its configuration from LangBot via the INIT RPC action.
|
||||||
|
# Do not add BOX__* here — they would be silently ignored.
|
||||||
|
volumeMounts:
|
||||||
|
# Box workspace root — identical path on node, box, and sandbox
|
||||||
|
# containers (see the IMPORTANT note above).
|
||||||
|
- name: box-root
|
||||||
|
mountPath: /app/data/box
|
||||||
|
# Host Docker socket — the sandbox backend uses it to create containers.
|
||||||
|
- name: docker-sock
|
||||||
|
mountPath: /var/run/docker.sock
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5410
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5410
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
volumes:
|
||||||
|
- name: box-root
|
||||||
|
hostPath:
|
||||||
|
path: /app/data/box
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: docker-sock
|
||||||
|
hostPath:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
type: Socket
|
||||||
|
restartPolicy: Always
|
||||||
|
|
||||||
|
---
|
||||||
|
# Service for LangBot Box runtime
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langbot-box
|
||||||
|
namespace: langbot
|
||||||
|
labels:
|
||||||
|
app: langbot-box
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langbot-box
|
||||||
|
ports:
|
||||||
|
- port: 5410
|
||||||
|
targetPort: 5410
|
||||||
|
protocol: TCP
|
||||||
|
name: box-rpc
|
||||||
|
|
||||||
|
---
|
||||||
|
# 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
|
||||||
|
# Box (sandbox) runtime endpoint. Connects LangBot to the langbot-box
|
||||||
|
# Service over WebSocket. Remove this (and the langbot-box Deployment)
|
||||||
|
# and set BOX__ENABLED=false if you do not want the sandbox.
|
||||||
|
- name: BOX__RUNTIME__ENDPOINT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: langbot-config
|
||||||
|
key: BOX__RUNTIME__ENDPOINT
|
||||||
|
# box.local.* config — forwarded to the Box runtime via INIT RPC. The
|
||||||
|
# host_root MUST match the box-root hostPath mountPath below AND the box
|
||||||
|
# Deployment's box-root mountPath, so that skill package paths resolve
|
||||||
|
# identically on both sides and on the node's Docker daemon.
|
||||||
|
- name: BOX__LOCAL__HOST_ROOT
|
||||||
|
value: "/app/data/box"
|
||||||
|
- name: BOX__LOCAL__DEFAULT_WORKSPACE
|
||||||
|
value: "default"
|
||||||
|
- name: BOX__LOCAL__SKILLS_ROOT
|
||||||
|
value: "skills"
|
||||||
|
- name: BOX__LOCAL__ALLOWED_MOUNT_ROOTS
|
||||||
|
value: "/app/data/box"
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /app/data
|
||||||
|
- name: plugins
|
||||||
|
mountPath: /app/plugins
|
||||||
|
# Same node-level box root as the langbot-box Deployment. Mounted over
|
||||||
|
# the data PVC's /app/data/box subpath so both LangBot and the Box
|
||||||
|
# runtime (and the node's dockerd) agree on one absolute path.
|
||||||
|
- name: box-root
|
||||||
|
mountPath: /app/data/box
|
||||||
|
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
|
||||||
|
# Node-level box workspace root, shared with the langbot-box Deployment.
|
||||||
|
# hostPath (not PVC) because the node's Docker daemon must see the same
|
||||||
|
# absolute path when bind-mounting workspaces into sandbox containers.
|
||||||
|
- name: box-root
|
||||||
|
hostPath:
|
||||||
|
path: /app/data/box
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
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
|
||||||
@@ -10,6 +10,38 @@ API keys can be managed through the web interface:
|
|||||||
2. Click the "API Keys" button at the bottom of the sidebar
|
2. Click the "API Keys" button at the bottom of the sidebar
|
||||||
3. Create, view, copy, or delete API keys as needed
|
3. Create, view, copy, or delete API keys as needed
|
||||||
|
|
||||||
|
## Global API Key (config.yaml)
|
||||||
|
|
||||||
|
In addition to web-UI-created keys (stored in the database, prefixed `lbk_`),
|
||||||
|
LangBot supports a **global API key** defined directly in `data/config.yaml`.
|
||||||
|
This is useful for automated deployments, infrastructure-as-code, and AI agents
|
||||||
|
that need API/MCP access **without a login session and without creating a
|
||||||
|
database record first**.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
port: 5300
|
||||||
|
# ...
|
||||||
|
global_api_key: 'your-strong-secret-here' # leave empty to disable
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- When `api.global_api_key` is a non-empty string, that exact value is accepted
|
||||||
|
anywhere a normal API key is accepted — the `X-API-Key` header or
|
||||||
|
`Authorization: Bearer <key>` — across the HTTP service API **and the MCP
|
||||||
|
server**.
|
||||||
|
- The global key does **not** require the `lbk_` prefix; use any sufficiently
|
||||||
|
strong secret.
|
||||||
|
- Leave it empty (`''`, the default) to disable it entirely; only database-backed
|
||||||
|
`lbk_` keys will then be accepted.
|
||||||
|
- Existing installs are unaffected until you add the key — config completion only
|
||||||
|
backfills top-level keys, and the lookup is defensive when the field is absent.
|
||||||
|
|
||||||
|
> **Security:** the global key is stored in plaintext in `config.yaml`. Only
|
||||||
|
> enable it on trusted/internal deployments, keep the file permissions tight,
|
||||||
|
> always serve over HTTPS, and rotate the value if it may have leaked.
|
||||||
|
|
||||||
## Using API Keys
|
## Using API Keys
|
||||||
|
|
||||||
### Authentication Headers
|
### Authentication Headers
|
||||||
|
|||||||
@@ -0,0 +1,575 @@
|
|||||||
|
# HTTP Bot Adapter — Design Document
|
||||||
|
|
||||||
|
> Status: **Implemented** · Branch: `feat/http-bot-adapter` · Author: LangBot core
|
||||||
|
>
|
||||||
|
> A first-class, **standalone** message-platform adapter (`http_bot`) that lets
|
||||||
|
> any external system (e.g. LangBot Space ticketing, an internal back-office, a
|
||||||
|
> CRM, a custom web app) talk to a LangBot pipeline over plain HTTP — **inbound**
|
||||||
|
> by POSTing messages in, **outbound** by receiving replies on a callback URL —
|
||||||
|
> with full support for the pipeline's native N→1 aggregation and 1→M
|
||||||
|
> multi-reply semantics, and **without** holding a long-lived WebSocket
|
||||||
|
> connection.
|
||||||
|
>
|
||||||
|
> **Shipped in this branch:**
|
||||||
|
> - `src/langbot/pkg/platform/sources/http_bot.yaml` — adapter manifest (auto-discovered)
|
||||||
|
> - `src/langbot/pkg/platform/sources/http_bot.py` — `HttpBotAdapter`
|
||||||
|
> - `src/langbot/pkg/platform/sources/http_bot_signing.py` — HMAC helpers
|
||||||
|
> - `src/langbot/pkg/platform/sources/http_bot.svg` — icon
|
||||||
|
> - `docs/platforms/http-bot.md` — integration guide
|
||||||
|
> - `docs/http-bot-openapi.json` — machine-readable contract
|
||||||
|
> - `examples/http-bot/` — Python + TypeScript reference clients
|
||||||
|
>
|
||||||
|
> **Final decisions (resolving the original open questions):**
|
||||||
|
> 1. Callback URL is **config-only** — never accepted per-message (SSRF closed).
|
||||||
|
> 2. **Session reset is provided** — `POST /bots/<uuid>/reset` keyed by `session_id`.
|
||||||
|
> 3. Reference **clients are provided** — `examples/http-bot/client.py` + `client.ts`.
|
||||||
|
> 4. **Sync convenience mode is included** — `POST /bots/<uuid>/sync` (opt-in, lossy).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Background & Motivation
|
||||||
|
|
||||||
|
### 1.1 The concrete need
|
||||||
|
|
||||||
|
LangBot Space wants to use a LangBot pipeline as the brain for **ticket
|
||||||
|
handling**. The integration is **server-to-server**: Space's backend pushes a
|
||||||
|
user's ticket messages into LangBot and renders LangBot's replies back into the
|
||||||
|
ticket thread.
|
||||||
|
|
||||||
|
This interaction is **not** request/response shaped:
|
||||||
|
|
||||||
|
- **N → 1**: a user may fire several messages in a row ("the app crashed" …
|
||||||
|
"when I click export" … "here's a screenshot"). The pipeline's
|
||||||
|
**message aggregation** feature should debounce and merge these into one turn.
|
||||||
|
- **1 → N**: a single turn may yield **multiple** outbound messages — a tool/
|
||||||
|
function call narrating progress, a plugin emitting several cards, a streamed
|
||||||
|
answer split into chunks.
|
||||||
|
|
||||||
|
### 1.2 Why the existing options don't fit
|
||||||
|
|
||||||
|
LangBot today exposes exactly one externally-reachable way to drive a pipeline
|
||||||
|
that is **not** tied to a specific IM vendor: the **WebSocket** path
|
||||||
|
(`/api/v1/pipelines/<uuid>/ws/connect` for dashboard debug, and
|
||||||
|
`/api/v1/embed/<bot_uuid>/ws/connect` for the embeddable web widget).
|
||||||
|
|
||||||
|
For a server-to-server integration the WebSocket path has real friction:
|
||||||
|
|
||||||
|
| Problem | Detail |
|
||||||
|
|---|---|
|
||||||
|
| Long-lived connection | Caller must maintain a socket, heartbeats, and reconnect logic for what is fundamentally a fire-and-collect workload. |
|
||||||
|
| Session identity | Inbound messages are keyed by the transient `connection_id` (`websocket_{connection_id}`); the caller **cannot supply a stable, business-meaningful session id** (e.g. a ticket number). Multi-ticket isolation is not expressible. |
|
||||||
|
| Auth mismatch | The debug socket is gated by the **dashboard JWT** (must not be handed to an external service); the embed socket is gated by **Cloudflare Turnstile** (a *browser* human-check that a backend cannot satisfy). Neither is a server-to-server credential. |
|
||||||
|
| In-memory, single-process state | Session history lives in process memory and is lost on restart. |
|
||||||
|
|
||||||
|
> **Key realisation.** The N→1 / 1→M behaviour the caller wants is **not**
|
||||||
|
> provided by WebSocket — it is provided by the **pipeline** (aggregation +
|
||||||
|
> the adapter being free to call `reply_message` any number of times). It is
|
||||||
|
> therefore **transport-independent**. We can deliver the exact same semantics
|
||||||
|
> over a far lighter HTTP transport.
|
||||||
|
|
||||||
|
### 1.3 Why a *new, standalone* adapter (not a refactor of an existing one)
|
||||||
|
|
||||||
|
The brief is explicit: **do not reuse / fork an existing vendor adapter.** The
|
||||||
|
vendor adapters (`lark`, `wecom`, `qqofficial`, `slack`, …) carry vendor-specific
|
||||||
|
signature schemes, payload shapes, and message-segment mappings. Bending one of
|
||||||
|
them into a "generic" mode would couple a public integration surface to one
|
||||||
|
vendor's quirks and make the developer experience worse for everyone.
|
||||||
|
|
||||||
|
Instead we ship `http_bot` as a clean, independent adapter whose **entire
|
||||||
|
contract is LangBot's own** — documented, versioned, and designed front-to-back
|
||||||
|
around *integrator* developer experience.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Goals & Non-Goals
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- **G1** A standalone `http_bot` adapter, selectable like any other platform
|
||||||
|
adapter in the dashboard, with its own config schema and docs.
|
||||||
|
- **G2** **Inbound**: external systems POST messages to a stable LangBot URL,
|
||||||
|
carrying a **caller-defined `session_id`** that maps 1:1 to a LangBot session.
|
||||||
|
- **G3** **Outbound**: LangBot delivers each reply by POSTing to a
|
||||||
|
caller-configured **callback URL**; one turn may produce **many** callbacks.
|
||||||
|
- **G4** Preserve pipeline-native **N→1 aggregation** and **1→M multi-reply**.
|
||||||
|
- **G5** Server-to-server **auth**: shared-secret HMAC request signing both
|
||||||
|
directions (no JWT, no Turnstile, no long-lived socket).
|
||||||
|
- **G6** **Great DX**: copy-pasteable curl, a tiny reference client, an OpenAPI
|
||||||
|
fragment, idempotency, clear error envelope, and a local echo-server recipe.
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- Not replacing or deprecating the WebSocket / embed widget path (that remains
|
||||||
|
the right tool for *browser*, real-time, streaming chat UIs).
|
||||||
|
- Not a synchronous "one request → one response" RPC (explicitly rejected: it
|
||||||
|
cannot express 1→M; see §9 for the optional sync convenience mode).
|
||||||
|
- No built-in message **persistence/replay** in v1 (callbacks are at-least-once
|
||||||
|
best-effort; durability is the caller's responsibility — see §8).
|
||||||
|
- No multi-tenant API-key management UI in v1 (one secret per bot; see §11).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How LangBot routes a message (the parts we plug into)
|
||||||
|
|
||||||
|
Understanding the existing flow is what makes this adapter cheap. A message
|
||||||
|
flows through these stages (verified against current `master`):
|
||||||
|
|
||||||
|
```
|
||||||
|
INBOUND OUTBOUND
|
||||||
|
external POST ─┐ ┌─ reply_message()
|
||||||
|
▼ │ reply_message_chunk()
|
||||||
|
POST /bots/<bot_uuid> (unified webhook router, AuthType.NONE)
|
||||||
|
│ webhooks.py → adapter.handle_unified_webhook(bot_uuid, path, request)
|
||||||
|
▼ │
|
||||||
|
HttpBotAdapter.handle_unified_webhook │ (called 0..N times
|
||||||
|
• verify HMAC signature │ per turn by the
|
||||||
|
• parse {session_id, message[]} │ pipeline / plugins)
|
||||||
|
• build FriendMessage / GroupMessage │
|
||||||
|
• fire registered listener ───────────────┐ │
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ │
|
||||||
|
botmgr.on_friend_message / on_group_message │
|
||||||
|
• (optional) webhook_pusher fan-out │
|
||||||
|
• msg_aggregator.add_message(...) ── N→1 debounce ──►│
|
||||||
|
│ │
|
||||||
|
▼ │
|
||||||
|
query_pool → pipeline.run() ─── invokes adapter ─────┘
|
||||||
|
reply methods 1..M times
|
||||||
|
```
|
||||||
|
|
||||||
|
Two framework facts we rely on:
|
||||||
|
|
||||||
|
1. **N→1 aggregation is free.** `botmgr` hands every inbound event to
|
||||||
|
`self.ap.msg_aggregator.add_message(...)`, which debounces per
|
||||||
|
`session_id` and merges consecutive messages into one pipeline turn
|
||||||
|
(`pkg/pipeline/aggregator.py`). The adapter does nothing special.
|
||||||
|
|
||||||
|
2. **1→M is free.** The pipeline (and any plugin in the chain) calls
|
||||||
|
`adapter.reply_message()` / `reply_message_chunk()` **as many times as it
|
||||||
|
wants** per turn. The adapter's only job is to deliver each call outward.
|
||||||
|
For `http_bot` that means: **one outbound callback POST per call.**
|
||||||
|
|
||||||
|
3. **A unified inbound route already exists.** `WebhookRouterGroup`
|
||||||
|
(`pkg/api/http/controller/groups/webhooks.py`) maps
|
||||||
|
`POST /bots/<bot_uuid>[/<path>]` (auth `NONE`) to
|
||||||
|
`adapter.handle_unified_webhook(bot_uuid, path, request)`. `http_bot`
|
||||||
|
implements that method and is reachable **without registering any new
|
||||||
|
route** — it does its own signature verification, exactly like the vendor
|
||||||
|
webhook adapters do.
|
||||||
|
|
||||||
|
> Net new code is essentially: one `http_bot.py` adapter, one `http_bot.yaml`
|
||||||
|
> schema, signing helpers, and docs. No router, aggregator, or pipeline changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────┐ (1) inbound: POST signed message
|
||||||
|
│ External system │ ──────────────────────────────────────────────► ┌──────────────────────┐
|
||||||
|
│ (LangBot Space, │ POST /bots/<bot_uuid> │ LangBot │
|
||||||
|
│ CRM, web app …) │ X-LB-Signature, X-LB-Timestamp │ │
|
||||||
|
│ │ { session_id, message:[...] } │ HttpBotAdapter │
|
||||||
|
│ - callback server │ ◄────────────────────────────────────────────── │ (platform/sources) │
|
||||||
|
│ (receives │ (4) outbound: POST signed reply(s) │ │
|
||||||
|
│ replies) │ POST <callback_url> │ pipeline + aggregator│
|
||||||
|
└────────────────────┘ X-LB-Signature, X-LB-Timestamp └──────────────────────┘
|
||||||
|
{ session_id, sequence, is_final,
|
||||||
|
message:[...] } (sent 1..M times)
|
||||||
|
```
|
||||||
|
|
||||||
|
- The adapter is **stateless across requests** at the HTTP layer; session
|
||||||
|
continuity is carried by `session_id` and resolved by LangBot's normal
|
||||||
|
session manager.
|
||||||
|
- **Inbound** and **outbound** are **independent HTTP exchanges**. LangBot does
|
||||||
|
not answer the inbound POST with the pipeline result; it `202 Accepts` it and
|
||||||
|
later POSTs the reply(s) to the callback URL. This is what makes 1→M natural.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Configuration Schema (`http_bot.yaml`)
|
||||||
|
|
||||||
|
Follows the existing `MessagePlatformAdapter` manifest convention (cf.
|
||||||
|
`slack.yaml`). Fields:
|
||||||
|
|
||||||
|
| field | type | required | purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `inbound_secret` | string (secret) | yes | HMAC key the **caller** uses to sign inbound POSTs; LangBot verifies. |
|
||||||
|
| `callback_url` | string (url) | no* | Where LangBot POSTs replies. *Optional if the caller supplies `callback_url` per-message (see §6.1); a static default lives here. |
|
||||||
|
| `outbound_secret` | string (secret) | no | HMAC key LangBot uses to sign outbound callbacks; caller verifies. Defaults to `inbound_secret` if empty. |
|
||||||
|
| `default_session_type` | enum `person`/`group` | no | Default when a message omits `session_type`. Default `person`. |
|
||||||
|
| `signature_required` | bool | no | If `false`, skip inbound signature check (dev only; logs a warning). Default `true`. |
|
||||||
|
| `callback_timeout` | int (seconds) | no | Per-callback HTTP timeout. Default `15`. |
|
||||||
|
| `callback_max_retries` | int | no | Retries on 5xx/timeout with backoff. Default `3`. |
|
||||||
|
| `webhook_url` | webhook-url (display) | — | Read-only field rendering the inbound URL `…/bots/<bot_uuid>` for copy-paste, like other webhook adapters. |
|
||||||
|
|
||||||
|
Manifest sketch (i18n labels elided for brevity):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: MessagePlatformAdapter
|
||||||
|
metadata:
|
||||||
|
name: http_bot
|
||||||
|
label: { en_US: "HTTP Bot", zh_Hans: "HTTP 通用接入" }
|
||||||
|
description:
|
||||||
|
en_US: "Integrate any backend over plain HTTP. Push messages in, receive replies on a callback URL. Server-to-server, no long-lived connection."
|
||||||
|
zh_Hans: "通过 HTTP 接入任意后端系统。推入消息、在回调地址接收回复。面向服务间集成,无需长连接。"
|
||||||
|
icon: http_bot.svg
|
||||||
|
spec:
|
||||||
|
categories: [popular, global]
|
||||||
|
help_links:
|
||||||
|
zh: https://docs.langbot.app/zh/platforms/http-bot
|
||||||
|
en: https://docs.langbot.app/en/platforms/http-bot
|
||||||
|
config:
|
||||||
|
- { name: inbound_secret, type: string, required: true, default: "" }
|
||||||
|
- { name: callback_url, type: string, required: false, default: "" }
|
||||||
|
- { name: outbound_secret, type: string, required: false, default: "" }
|
||||||
|
- { name: default_session_type, type: select, required: false, default: "person",
|
||||||
|
options: [person, group] }
|
||||||
|
- { name: signature_required, type: boolean, required: false, default: true }
|
||||||
|
- { name: callback_timeout, type: integer, required: false, default: 15 }
|
||||||
|
- { name: callback_max_retries, type: integer, required: false, default: 3 }
|
||||||
|
- { name: webhook_url, type: webhook-url, required: false, default: "" }
|
||||||
|
execution:
|
||||||
|
python:
|
||||||
|
path: ./http_bot.py
|
||||||
|
attr: HttpBotAdapter
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The HTTP Contract (this is the DX surface)
|
||||||
|
|
||||||
|
### 6.1 Inbound — push a message into LangBot
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /bots/{bot_uuid}
|
||||||
|
Content-Type: application/json
|
||||||
|
X-LB-Timestamp: 1718000000
|
||||||
|
X-LB-Signature: sha256=<hex hmac>
|
||||||
|
X-LB-Idempotency-Key: <uuid> # optional, dedup window
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"session_id": "ticket-10293", // REQUIRED. Caller-defined. Maps 1:1 to a LangBot session.
|
||||||
|
"session_type": "person", // optional, "person" | "group"; default from config
|
||||||
|
"sender": { // optional metadata, surfaced to pipeline/plugins
|
||||||
|
"id": "user-5567",
|
||||||
|
"name": "Alice"
|
||||||
|
},
|
||||||
|
"message": [ // REQUIRED. A LangBot MessageChain (list of segments).
|
||||||
|
{ "type": "Plain", "text": "Export keeps failing on the dashboard." },
|
||||||
|
{ "type": "Image", "url": "https://.../screenshot.png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response (LangBot does **not** block on the pipeline):
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
// 202 Accepted
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "accepted",
|
||||||
|
"data": {
|
||||||
|
"session_id": "ticket-10293",
|
||||||
|
"accepted_message_id": "in_01H....", // server-assigned id for this inbound message
|
||||||
|
"aggregating": true // true if buffered by the aggregator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**N→1 in practice.** Fire three POSTs with the same `session_id` inside the
|
||||||
|
aggregation window → the pipeline runs **once** with the three messages merged.
|
||||||
|
No special flag needed; this is the aggregator's default behaviour when enabled
|
||||||
|
on the pipeline.
|
||||||
|
|
||||||
|
### 6.2 Outbound — LangBot delivers replies to your callback
|
||||||
|
|
||||||
|
For each `reply_message` / `reply_message_chunk` the pipeline emits, LangBot
|
||||||
|
POSTs to `callback_url`:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST {callback_url}
|
||||||
|
Content-Type: application/json
|
||||||
|
X-LB-Timestamp: 1718000001
|
||||||
|
X-LB-Signature: sha256=<hex hmac over body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"session_id": "ticket-10293", // echoes the inbound session
|
||||||
|
"reply_to": "in_01H....", // the inbound message id this answers
|
||||||
|
"sequence": 1, // 1-based ordinal within this turn (for 1→M ordering)
|
||||||
|
"is_final": false, // false for intermediate/streamed parts
|
||||||
|
"stream": false, // true when this is a streamed chunk
|
||||||
|
"message": [
|
||||||
|
{ "type": "Plain", "text": "Looking into it — checking your export logs…" }
|
||||||
|
],
|
||||||
|
"timestamp": "2026-06-22T09:00:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**1→M in practice.** A turn that fires a function call then a final answer
|
||||||
|
produces e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST callback → { sequence: 1, is_final: false, message: ["Checking logs…"] }
|
||||||
|
POST callback → { sequence: 2, is_final: false, message: ["Found 2 failed exports."] }
|
||||||
|
POST callback → { sequence: 3, is_final: true, message: ["Fixed. Try again now."] }
|
||||||
|
```
|
||||||
|
|
||||||
|
The caller stitches by `session_id` + `sequence`, and knows the turn is complete
|
||||||
|
when `is_final: true` arrives.
|
||||||
|
|
||||||
|
Your callback endpoint should return `200` quickly. A non-2xx triggers retry
|
||||||
|
with backoff (`callback_max_retries`).
|
||||||
|
|
||||||
|
### 6.3 Error envelope (inbound)
|
||||||
|
|
||||||
|
Consistent, machine-readable; never leak internals:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "code": 40101, "msg": "invalid signature", "data": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
| HTTP | code | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| 202 | 0 | accepted |
|
||||||
|
| 400 | 40001 | malformed body / missing `session_id` or `message` |
|
||||||
|
| 401 | 40101 | bad/expired signature |
|
||||||
|
| 403 | 40301 | bot disabled |
|
||||||
|
| 404 | 40401 | bot_uuid not found / not an `http_bot` adapter |
|
||||||
|
| 409 | 40901 | duplicate idempotency key (already accepted) |
|
||||||
|
| 413 | 41301 | message too large |
|
||||||
|
| 500 | 50001 | internal error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Signing scheme (both directions)
|
||||||
|
|
||||||
|
Symmetric, dependency-free HMAC-SHA256 — trivial to implement in any language.
|
||||||
|
|
||||||
|
```
|
||||||
|
signing_string = "{timestamp}.{raw_request_body}"
|
||||||
|
signature = "sha256=" + hex(HMAC_SHA256(secret, signing_string))
|
||||||
|
```
|
||||||
|
|
||||||
|
Verification rules:
|
||||||
|
|
||||||
|
- Reject if `|now - timestamp| > 300s` (replay window).
|
||||||
|
- Constant-time compare (`hmac.compare_digest`).
|
||||||
|
- Inbound verified with `inbound_secret`; outbound signed with
|
||||||
|
`outbound_secret` (falls back to `inbound_secret`).
|
||||||
|
- `signature_required: false` bypasses verification **and logs a warning** —
|
||||||
|
intended only for local development behind a trusted network.
|
||||||
|
|
||||||
|
Reference (Python, ~6 lines):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import hmac, hashlib, time
|
||||||
|
|
||||||
|
def sign(secret: str, body: bytes, ts: int | None = None) -> tuple[str, str]:
|
||||||
|
ts = ts or int(time.time())
|
||||||
|
mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256)
|
||||||
|
return str(ts), "sha256=" + mac.hexdigest()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Delivery semantics & reliability
|
||||||
|
|
||||||
|
- **Inbound**: `202 Accepted` means *queued*, not *processed*. Use
|
||||||
|
`X-LB-Idempotency-Key` to make client retries safe (dedup window, e.g. 10 min).
|
||||||
|
- **Outbound**: **at-least-once**, best-effort. Retries on timeout/5xx with
|
||||||
|
exponential backoff up to `callback_max_retries`. Callbacks for one
|
||||||
|
`session_id` are delivered **in `sequence` order** (serialised per session);
|
||||||
|
across sessions they may interleave.
|
||||||
|
- **No persistence in v1**: if LangBot restarts mid-turn, in-flight callbacks
|
||||||
|
may be lost. Durable replay is deferred (see §13). Callers needing exactly-once
|
||||||
|
should dedup on `(session_id, reply_to, sequence)`.
|
||||||
|
- **Backpressure**: the adapter must not block the pipeline on slow callbacks —
|
||||||
|
outbound POSTs run on a per-session ordered queue with the configured timeout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Optional: synchronous convenience mode (v1.1, behind a flag)
|
||||||
|
|
||||||
|
Some simple callers genuinely want "POST a message, get the reply in the HTTP
|
||||||
|
response" and don't care about streaming/multi-part. We can offer an **opt-in**
|
||||||
|
sync endpoint that internally waits for `is_final` and **collapses** all 1→M
|
||||||
|
parts into one array:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /bots/{bot_uuid}/sync → 200 { session_id, message: [ ...all parts concatenated... ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemented by attaching a per-request future that resolves on the final reply,
|
||||||
|
with a hard timeout. This is a **convenience wrapper** over the same machinery,
|
||||||
|
explicitly documented as lossy for streaming/ordering. Not in v1 core.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Adapter implementation sketch (`platform/sources/http_bot.py`)
|
||||||
|
|
||||||
|
Implements `AbstractMessagePlatformAdapter`. Key methods:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class HttpBotAdapter(AbstractMessagePlatformAdapter):
|
||||||
|
listeners: dict = pydantic.Field(default_factory=dict, exclude=True)
|
||||||
|
|
||||||
|
# --- inbound -------------------------------------------------------
|
||||||
|
async def handle_unified_webhook(self, bot_uuid, path, request):
|
||||||
|
body = await request.get_body()
|
||||||
|
if self.config.get("signature_required", True):
|
||||||
|
if not self._verify(request, body):
|
||||||
|
return jsonify({"code": 40101, "msg": "invalid signature"}), 401
|
||||||
|
data = json.loads(body)
|
||||||
|
session_id = data["session_id"] # caller-defined identity
|
||||||
|
session_type = data.get("session_type", self.config.get("default_session_type", "person"))
|
||||||
|
chain = MessageChain.model_validate(data["message"])
|
||||||
|
event = self._build_event(session_type, session_id, data.get("sender"), chain)
|
||||||
|
# remember where to send replies for this session
|
||||||
|
self._callback_for[session_id] = data.get("callback_url") or self.config.get("callback_url")
|
||||||
|
# fire the registered listener → botmgr → msg_aggregator (N→1) → pipeline
|
||||||
|
if type(event) in self.listeners:
|
||||||
|
asyncio.create_task(self.listeners[type(event)](event, self))
|
||||||
|
return jsonify({"code": 0, "msg": "accepted",
|
||||||
|
"data": {"session_id": session_id, "accepted_message_id": event.message_id}}), 202
|
||||||
|
|
||||||
|
# --- outbound (called 1..M times per turn by the pipeline) ---------
|
||||||
|
async def reply_message(self, message_source, message, quote_origin=False):
|
||||||
|
return await self._post_callback(message_source, message, is_final=True, stream=False)
|
||||||
|
|
||||||
|
async def reply_message_chunk(self, message_source, bot_message, message,
|
||||||
|
quote_origin=False, is_final=False):
|
||||||
|
return await self._post_callback(message_source, message, is_final=is_final, stream=True)
|
||||||
|
|
||||||
|
async def is_stream_output_supported(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def register_listener(self, event_type, func): self.listeners[event_type] = func
|
||||||
|
def unregister_listener(self, event_type, func): self.listeners.pop(event_type, None)
|
||||||
|
async def run_async(self): pass # nothing to poll; purely webhook-driven
|
||||||
|
async def kill(self): pass
|
||||||
|
```
|
||||||
|
|
||||||
|
`_post_callback` resolves the session's callback URL, assigns the next
|
||||||
|
`sequence`, signs the body, and enqueues an ordered, retrying POST.
|
||||||
|
|
||||||
|
Session→callback mapping is kept in a small in-memory dict keyed by
|
||||||
|
`session_id` (acceptable for v1; a turn's callback URL is captured at inbound
|
||||||
|
time so replies always have a destination even if config later changes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Security considerations
|
||||||
|
|
||||||
|
- **Inbound route is `AuthType.NONE`** at the framework level (same as all
|
||||||
|
webhook adapters) — the adapter **must** enforce HMAC itself. Default
|
||||||
|
`signature_required: true`.
|
||||||
|
- **Timestamp window** (±300s) + idempotency key blunt replay.
|
||||||
|
- **SSRF on callback_url**: validate scheme (`https` in prod), and consider an
|
||||||
|
allow-list / block of private CIDRs since LangBot initiates the POST. Document
|
||||||
|
this; enforce in code where feasible.
|
||||||
|
- **Secret storage**: secrets live in the bot's `adapter_config` like every
|
||||||
|
other adapter credential; surfaced as `type: string`/secret in the dashboard.
|
||||||
|
- **One secret per bot** in v1. Per-caller key rotation / multiple keys is a
|
||||||
|
future enhancement (§13).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Developer Experience (explicit deliverables)
|
||||||
|
|
||||||
|
The whole point of a standalone adapter is that **integrating is pleasant**. v1
|
||||||
|
ships:
|
||||||
|
|
||||||
|
1. **`docs/platforms/http-bot.md`** — task-oriented integration guide:
|
||||||
|
create the bot → copy inbound URL → set secret → stand up a callback
|
||||||
|
endpoint → send first message → handle 1→M.
|
||||||
|
2. **Copy-paste curl** for the first message (with a working signing one-liner).
|
||||||
|
3. **Reference clients** (≤50 LOC each) in `examples/http-bot/`:
|
||||||
|
`client.py` (push + a Flask/Quart callback receiver) and `client.ts`.
|
||||||
|
4. **OpenAPI fragment** `docs/http-bot-openapi.json` describing inbound +
|
||||||
|
callback shapes, so integrators can codegen.
|
||||||
|
5. **Local echo recipe**: a one-command callback server that prints every
|
||||||
|
reply, so a developer sees N→1 and 1→M working in under five minutes.
|
||||||
|
6. **Postman/Hoppscotch collection** (nice-to-have).
|
||||||
|
|
||||||
|
DX acceptance check: *a developer who has never seen LangBot can, from the docs
|
||||||
|
alone, push a message and observe a multi-part reply on their callback within
|
||||||
|
10 minutes.*
|
||||||
|
|
||||||
|
### Quickstart (curl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOT=https://your-langbot/bots/2f1c....
|
||||||
|
SECRET=supersecret
|
||||||
|
BODY='{"session_id":"ticket-10293","message":[{"type":"Plain","text":"hello"}]}'
|
||||||
|
TS=$(date +%s)
|
||||||
|
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"
|
||||||
|
curl -sS -X POST "$BOT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-LB-Timestamp: $TS" \
|
||||||
|
-H "X-LB-Signature: $SIG" \
|
||||||
|
-d "$BODY"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Future work
|
||||||
|
|
||||||
|
- **Durable outbound queue** (persist + replay across restarts; exactly-once).
|
||||||
|
- **Per-caller API keys** with rotation and scopes (multi-tenant Space usage).
|
||||||
|
- **Sync convenience endpoint** (§9) once core is stable.
|
||||||
|
- **Server-Sent Events outbound option** for callers that *do* want a stream but
|
||||||
|
not a full duplex socket — single GET, server pushes chunks.
|
||||||
|
- **Dashboard "test console"** for `http_bot` (send a message, watch callbacks)
|
||||||
|
mirroring the existing WebSocket debug panel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Rollout / task breakdown
|
||||||
|
|
||||||
|
| # | Task | Touches |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `http_bot.yaml` manifest + icon | `platform/sources/` |
|
||||||
|
| 2 | `HttpBotAdapter` (inbound verify, event build, outbound queue) | `platform/sources/http_bot.py` |
|
||||||
|
| 3 | Signing helper module (shared) | `platform/sources/` or `utils/` |
|
||||||
|
| 4 | i18n strings (en/zh/ja) | adapter yaml + web locale |
|
||||||
|
| 5 | Integration docs `docs/platforms/http-bot.md` | `docs/` |
|
||||||
|
| 6 | OpenAPI fragment + reference clients | `docs/`, `examples/http-bot/` |
|
||||||
|
| 7 | Tests: signature verify, N→1 aggregation, 1→M ordering, retry | `tests/` |
|
||||||
|
| 8 | (opt) SSRF guard for callback_url | adapter |
|
||||||
|
|
||||||
|
No changes required to: the unified webhook router, the aggregator, the query
|
||||||
|
pool, or the pipeline. That is the design's main payoff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Resolved decisions
|
||||||
|
|
||||||
|
1. **Callback URL trust** — **config-only.** The inbound message may not carry a
|
||||||
|
`callback_url`; replies always go to the bot-config URL. Closes the SSRF
|
||||||
|
vector where a leaked inbound secret could redirect replies.
|
||||||
|
2. **Session lifecycle** — **`POST /bots/<uuid>/reset`** (body `{session_id,
|
||||||
|
session_type?}`) drops the matching session from the session manager; the
|
||||||
|
next message starts a fresh conversation. Implemented via sub-path routing in
|
||||||
|
`handle_unified_webhook`.
|
||||||
|
3. **Group semantics** — for `session_type: group`, `session_id` is the group/
|
||||||
|
launcher id; `sender.id` (and optional `sender.group_name`) identify the
|
||||||
|
member. A Space ticket maps to one `session_id`.
|
||||||
|
4. **Backpressure** — bounded per-session outbound queue (maxlen 1000); on
|
||||||
|
overflow the oldest reply is dropped and a warning logged, so a persistently
|
||||||
|
down callback can never exhaust memory.
|
||||||
|
|
||||||
|
### Still open / deferred (see §13)
|
||||||
|
|
||||||
|
- Durable outbound queue (persist + replay across restarts).
|
||||||
|
- Per-caller API keys with rotation/scopes for multi-tenant Space usage.
|
||||||
|
- SSE outbound option and a dashboard test console.
|
||||||
@@ -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,无任何旧代码遗留!
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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. 实现消息撤回功能
|
||||||
@@ -0,0 +1,713 @@
|
|||||||
|
# Workflow 系统开发者文档
|
||||||
|
|
||||||
|
本文档面向 LangBot 开发者,详细介绍 Workflow 系统的技术架构、核心组件和扩展方法。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [系统架构概述](#系统架构概述)
|
||||||
|
- [目录结构](#目录结构)
|
||||||
|
- [核心组件](#核心组件)
|
||||||
|
- [后端模块](#后端模块)
|
||||||
|
- [前端组件](#前端组件)
|
||||||
|
- [数据库表结构](#数据库表结构)
|
||||||
|
- [API 接口文档](#api-接口文档)
|
||||||
|
- [如何添加新节点类型](#如何添加新节点类型)
|
||||||
|
- [调试功能实现](#调试功能实现)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统架构概述
|
||||||
|
|
||||||
|
Workflow 系统采用前后端分离架构,主要包含以下层次:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 前端层 (React) │
|
||||||
|
│ ┌─────────────┬──────────────┬──────────────┬───────────┐ │
|
||||||
|
│ │ 可视化编辑器 │ 节点面板 │ 属性面板 │ 调试器 │ │
|
||||||
|
│ │ ReactFlow │ NodePalette │ PropertyPanel│ Debugger │ │
|
||||||
|
│ └─────────────┴──────────────┴──────────────┴───────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ API 层 (Quart) │
|
||||||
|
│ ┌─────────────┬──────────────┬──────────────────────────┐ │
|
||||||
|
│ │ Workflow API│ Debug API │ Node Types API │ │
|
||||||
|
│ └─────────────┴──────────────┴──────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 核心引擎层 (Python) │
|
||||||
|
│ ┌─────────────┬──────────────┬──────────────┬───────────┐ │
|
||||||
|
│ │ Executor │ Registry │ Node │ Entities │ │
|
||||||
|
│ │ 执行引擎 │ 节点注册表 │ 节点基类 │ 数据结构 │ │
|
||||||
|
│ └─────────────┴──────────────┴──────────────┴───────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ 存储层 (SQLAlchemy) │
|
||||||
|
│ ┌─────────────┬──────────────┬──────────────────────────┐ │
|
||||||
|
│ │ Workflow │ Executions │ Triggers │ │
|
||||||
|
│ └─────────────┴──────────────┴──────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
### 后端代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot/src/langbot/pkg/
|
||||||
|
├── workflow/ # Workflow 核心模块
|
||||||
|
│ ├── __init__.py # 模块初始化,导出公共接口
|
||||||
|
│ ├── entities.py # 数据实体定义
|
||||||
|
│ ├── executor.py # 执行引擎
|
||||||
|
│ ├── node.py # 节点基类和装饰器
|
||||||
|
│ ├── registry.py # 节点类型注册表
|
||||||
|
│ └── nodes/ # 内置节点实现
|
||||||
|
│ ├── __init__.py # 注册所有内置节点
|
||||||
|
│ ├── trigger.py # 触发节点
|
||||||
|
│ ├── process.py # 处理节点
|
||||||
|
│ ├── control.py # 控制节点
|
||||||
|
│ └── action.py # 动作节点
|
||||||
|
├── entity/persistence/
|
||||||
|
│ └── workflow.py # 数据库模型
|
||||||
|
├── api/http/
|
||||||
|
│ ├── controller/groups/workflows/
|
||||||
|
│ │ └── workflows.py # API 路由控制器
|
||||||
|
│ └── service/
|
||||||
|
│ └── workflow.py # 业务逻辑服务
|
||||||
|
└── persistence/migrations/
|
||||||
|
└── dbm026_workflow_tables.py # 数据库迁移
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot/web/src/app/home/workflows/
|
||||||
|
├── page.tsx # Workflow 列表页
|
||||||
|
├── WorkflowDetailContent.tsx # 详情页内容
|
||||||
|
├── store/
|
||||||
|
│ └── useWorkflowStore.ts # Zustand 状态管理
|
||||||
|
└── components/
|
||||||
|
├── workflow-editor/ # 可视化编辑器
|
||||||
|
│ ├── index.ts # 导出
|
||||||
|
│ ├── WorkflowEditorComponent.tsx # 主编辑器组件
|
||||||
|
│ ├── WorkflowNodeComponent.tsx # 自定义节点组件
|
||||||
|
│ ├── NodePalette.tsx # 节点面板
|
||||||
|
│ ├── PropertyPanel.tsx # 属性面板
|
||||||
|
│ └── node-configs/ # 节点配置元数据
|
||||||
|
│ ├── types.ts # 配置类型定义
|
||||||
|
│ ├── trigger-configs.ts
|
||||||
|
│ ├── ai-configs.ts
|
||||||
|
│ ├── process-configs.ts
|
||||||
|
│ ├── control-configs.ts
|
||||||
|
│ ├── action-configs.ts
|
||||||
|
│ ├── integration-configs.ts
|
||||||
|
│ └── index.ts # 配置汇总
|
||||||
|
├── workflow-debugger/ # 调试器组件
|
||||||
|
│ ├── index.ts
|
||||||
|
│ └── WorkflowDebugger.tsx
|
||||||
|
├── workflow-form/ # 表单组件
|
||||||
|
│ └── WorkflowFormComponent.tsx
|
||||||
|
└── workflow-executions/ # 执行历史组件
|
||||||
|
└── WorkflowExecutionsTab.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心组件
|
||||||
|
|
||||||
|
### 后端模块
|
||||||
|
|
||||||
|
#### 1. 执行引擎 (WorkflowExecutor)
|
||||||
|
|
||||||
|
位置:[`executor.py`](../../src/langbot/pkg/workflow/executor.py)
|
||||||
|
|
||||||
|
执行引擎负责工作流的实际执行,包括:
|
||||||
|
|
||||||
|
- **拓扑排序**:确定节点执行顺序
|
||||||
|
- **节点执行**:调用各节点的 execute 方法
|
||||||
|
- **控制流处理**:处理条件分支、循环、并行执行
|
||||||
|
- **错误处理**:支持重试机制
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WorkflowExecutor:
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
workflow: WorkflowDefinition,
|
||||||
|
context: ExecutionContext,
|
||||||
|
start_node_id: Optional[str] = None
|
||||||
|
) -> ExecutionContext:
|
||||||
|
"""执行工作流"""
|
||||||
|
# 1. 构建执行图
|
||||||
|
# 2. 初始化节点状态
|
||||||
|
# 3. 找到起始节点
|
||||||
|
# 4. 按拓扑顺序执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**调试执行器 (DebugWorkflowExecutor)**
|
||||||
|
|
||||||
|
继承自 WorkflowExecutor,增加了调试支持:
|
||||||
|
|
||||||
|
- 断点支持
|
||||||
|
- 单步执行
|
||||||
|
- 暂停/继续
|
||||||
|
- 实时日志
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DebugWorkflowExecutor(WorkflowExecutor):
|
||||||
|
async def execute_debug(
|
||||||
|
self,
|
||||||
|
workflow: WorkflowDefinition,
|
||||||
|
context: ExecutionContext,
|
||||||
|
debug_state: DebugExecutionState,
|
||||||
|
) -> ExecutionContext:
|
||||||
|
"""调试模式执行"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 节点注册表 (NodeTypeRegistry)
|
||||||
|
|
||||||
|
位置:[`registry.py`](../../src/langbot/pkg/workflow/registry.py)
|
||||||
|
|
||||||
|
单例模式管理所有节点类型:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class NodeTypeRegistry:
|
||||||
|
_instance: Optional['NodeTypeRegistry'] = None
|
||||||
|
|
||||||
|
def register(self, node_type: str, node_class: type[WorkflowNode]):
|
||||||
|
"""注册节点类型"""
|
||||||
|
|
||||||
|
def create_instance(self, node_type: str, node_id: str, config: dict) -> WorkflowNode:
|
||||||
|
"""创建节点实例"""
|
||||||
|
|
||||||
|
def list_all(self) -> list[dict]:
|
||||||
|
"""获取所有节点类型的 Schema"""
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 节点基类 (WorkflowNode)
|
||||||
|
|
||||||
|
位置:[`node.py`](../../src/langbot/pkg/workflow/node.py)
|
||||||
|
|
||||||
|
所有节点必须继承此基类:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WorkflowNode(abc.ABC):
|
||||||
|
# 节点元数据
|
||||||
|
type_name: str = ""
|
||||||
|
name: str = ""
|
||||||
|
description: str = ""
|
||||||
|
category: str = "misc"
|
||||||
|
icon: str = ""
|
||||||
|
|
||||||
|
# 端口定义
|
||||||
|
inputs: list[NodePort] = []
|
||||||
|
outputs: list[NodePort] = []
|
||||||
|
|
||||||
|
# 配置 Schema
|
||||||
|
config_schema: list[NodeConfig] = []
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
inputs: dict[str, Any],
|
||||||
|
context: ExecutionContext
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""执行节点逻辑"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 数据实体 (entities.py)
|
||||||
|
|
||||||
|
主要数据结构:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class WorkflowDefinition:
|
||||||
|
"""工作流定义"""
|
||||||
|
uuid: str
|
||||||
|
name: str
|
||||||
|
nodes: list[NodeDefinition]
|
||||||
|
edges: list[EdgeDefinition]
|
||||||
|
settings: WorkflowSettings
|
||||||
|
|
||||||
|
class ExecutionContext:
|
||||||
|
"""执行上下文"""
|
||||||
|
execution_id: str
|
||||||
|
workflow_id: str
|
||||||
|
status: ExecutionStatus
|
||||||
|
variables: dict
|
||||||
|
node_states: dict[str, NodeState]
|
||||||
|
history: list[ExecutionStep]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端组件
|
||||||
|
|
||||||
|
#### 1. WorkflowEditorComponent
|
||||||
|
|
||||||
|
主编辑器组件,基于 React Flow 实现:
|
||||||
|
|
||||||
|
- **画布交互**:拖拽、缩放、平移
|
||||||
|
- **节点连接**:自动验证端口类型
|
||||||
|
- **撤销/重做**:基于历史记录栈
|
||||||
|
- **复制/粘贴**:支持多选复制
|
||||||
|
|
||||||
|
关键功能:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function WorkflowEditorInner() {
|
||||||
|
const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useWorkflowStore();
|
||||||
|
|
||||||
|
// 拖放添加节点
|
||||||
|
const onDrop = useCallback((event: React.DragEvent) => {
|
||||||
|
const type = event.dataTransfer.getData('application/reactflow');
|
||||||
|
const position = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
addNode(type, position);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 复制粘贴
|
||||||
|
const handleCopy = useCallback(() => { ... }, []);
|
||||||
|
const handlePaste = useCallback(() => { ... }, []);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. NodePalette
|
||||||
|
|
||||||
|
节点面板组件,展示可用节点类型:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function NodePalette() {
|
||||||
|
// 按类别组织节点
|
||||||
|
const categories = [
|
||||||
|
{ id: 'trigger', name: '触发节点', icon: Zap },
|
||||||
|
{ id: 'ai', name: 'AI 节点', icon: Brain },
|
||||||
|
{ id: 'process', name: '处理节点', icon: Cpu },
|
||||||
|
{ id: 'control', name: '控制节点', icon: GitBranch },
|
||||||
|
{ id: 'action', name: '动作节点', icon: Send },
|
||||||
|
{ id: 'integration', name: '集成节点', icon: Plug },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 拖拽开始
|
||||||
|
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||||
|
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. PropertyPanel
|
||||||
|
|
||||||
|
属性面板组件,动态渲染节点配置表单:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function PropertyPanel() {
|
||||||
|
const { selectedNodeId, nodes, updateNodeData } = useWorkflowStore();
|
||||||
|
|
||||||
|
// 根据节点类型获取配置元数据
|
||||||
|
const selectedNode = nodes.find(n => n.id === selectedNodeId);
|
||||||
|
const nodeConfig = getNodeConfig(selectedNode?.data?.nodeType);
|
||||||
|
|
||||||
|
// 动态渲染配置字段
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{nodeConfig?.fields.map(field => (
|
||||||
|
<ConfigField key={field.name} field={field} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. WorkflowDebugger
|
||||||
|
|
||||||
|
调试器组件,支持实时调试:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function WorkflowDebugger({ workflowUuid, workflow }) {
|
||||||
|
const [debugState, setDebugState] = useState<DebugState>('idle');
|
||||||
|
const [executionId, setExecutionId] = useState<string>('');
|
||||||
|
const [logs, setLogs] = useState<ExecutionLog[]>([]);
|
||||||
|
|
||||||
|
// 启动调试
|
||||||
|
const startDebug = async () => {
|
||||||
|
const result = await backendClient.post(
|
||||||
|
`/api/v1/workflows/${workflowUuid}/debug/start`,
|
||||||
|
{ context, variables, breakpoints }
|
||||||
|
);
|
||||||
|
setExecutionId(result.execution_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (debugState === 'running') {
|
||||||
|
const interval = setInterval(fetchState, 500);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [debugState]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. useWorkflowStore
|
||||||
|
|
||||||
|
Zustand 状态管理:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface WorkflowState {
|
||||||
|
nodes: WorkflowNode[];
|
||||||
|
edges: WorkflowEdge[];
|
||||||
|
selectedNodeId: string | null;
|
||||||
|
history: HistoryEntry[];
|
||||||
|
historyIndex: number;
|
||||||
|
isDirty: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
addNode: (type: string, position: XYPosition) => void;
|
||||||
|
updateNodeData: (nodeId: string, data: Partial<NodeData>) => void;
|
||||||
|
deleteNode: (nodeId: string) => void;
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
|
// ... state and actions
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据库表结构
|
||||||
|
|
||||||
|
### workflows 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflows (
|
||||||
|
uuid VARCHAR(255) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
emoji VARCHAR(10) DEFAULT '🔄',
|
||||||
|
version INTEGER DEFAULT 1,
|
||||||
|
is_enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
definition JSON NOT NULL, -- 节点和边定义
|
||||||
|
global_config JSON DEFAULT '{}', -- 全局配置
|
||||||
|
extensions_preferences JSON, -- 插件和 MCP 配置
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### workflow_versions 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflow_versions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
workflow_uuid VARCHAR(255) NOT NULL,
|
||||||
|
version INTEGER NOT NULL,
|
||||||
|
definition JSON NOT NULL,
|
||||||
|
global_config JSON DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
created_by VARCHAR(255),
|
||||||
|
UNIQUE(workflow_uuid, version)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### workflow_executions 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflow_executions (
|
||||||
|
uuid VARCHAR(255) PRIMARY KEY,
|
||||||
|
workflow_uuid VARCHAR(255) NOT NULL,
|
||||||
|
workflow_version INTEGER NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL, -- pending/running/completed/failed/cancelled
|
||||||
|
trigger_type VARCHAR(50),
|
||||||
|
trigger_data JSON,
|
||||||
|
variables JSON,
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
end_time TIMESTAMP,
|
||||||
|
error TEXT,
|
||||||
|
created_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### workflow_node_executions 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflow_node_executions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
execution_uuid VARCHAR(255) NOT NULL,
|
||||||
|
node_id VARCHAR(100) NOT NULL,
|
||||||
|
node_type VARCHAR(50) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
inputs JSON,
|
||||||
|
outputs JSON,
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
end_time TIMESTAMP,
|
||||||
|
error TEXT,
|
||||||
|
retry_count INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### workflow_triggers 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE workflow_triggers (
|
||||||
|
uuid VARCHAR(255) PRIMARY KEY,
|
||||||
|
workflow_uuid VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL, -- message/cron/event/webhook
|
||||||
|
config JSON NOT NULL,
|
||||||
|
is_enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 接口文档
|
||||||
|
|
||||||
|
### Workflow CRUD
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 |
|
||||||
|
|-----|------|------|
|
||||||
|
| GET | `/api/v1/workflows` | 获取工作流列表 |
|
||||||
|
| POST | `/api/v1/workflows` | 创建工作流 |
|
||||||
|
| GET | `/api/v1/workflows/:uuid` | 获取单个工作流 |
|
||||||
|
| PUT | `/api/v1/workflows/:uuid` | 更新工作流 |
|
||||||
|
| DELETE | `/api/v1/workflows/:uuid` | 删除工作流 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/copy` | 复制工作流 |
|
||||||
|
|
||||||
|
### 执行相关
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 |
|
||||||
|
|-----|------|------|
|
||||||
|
| POST | `/api/v1/workflows/:uuid/execute` | 手动执行工作流 |
|
||||||
|
| GET | `/api/v1/workflows/:uuid/executions` | 获取执行记录 |
|
||||||
|
|
||||||
|
### 版本管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 |
|
||||||
|
|-----|------|------|
|
||||||
|
| GET | `/api/v1/workflows/:uuid/versions` | 获取版本列表 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/rollback/:version` | 回滚到指定版本 |
|
||||||
|
|
||||||
|
### 调试 API
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 |
|
||||||
|
|-----|------|------|
|
||||||
|
| POST | `/api/v1/workflows/:uuid/debug/start` | 启动调试 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/debug/:exec_id/pause` | 暂停执行 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/debug/:exec_id/resume` | 继续执行 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/debug/:exec_id/stop` | 停止执行 |
|
||||||
|
| POST | `/api/v1/workflows/:uuid/debug/:exec_id/step` | 单步执行 |
|
||||||
|
| GET | `/api/v1/workflows/:uuid/debug/:exec_id/state` | 获取调试状态 |
|
||||||
|
|
||||||
|
### 节点类型
|
||||||
|
|
||||||
|
| 方法 | 路径 | 描述 |
|
||||||
|
|-----|------|------|
|
||||||
|
| GET | `/api/v1/workflows/_/node-types` | 获取所有节点类型 |
|
||||||
|
| GET | `/api/v1/workflows/_/node-types/categories` | 按类别获取节点类型 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 如何添加新节点类型
|
||||||
|
|
||||||
|
### 步骤 1:创建节点类
|
||||||
|
|
||||||
|
在 `LangBot/src/langbot/pkg/workflow/nodes/` 下创建或修改文件:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ..node import WorkflowNode, NodePort, NodeConfig, workflow_node
|
||||||
|
from ..entities import ExecutionContext
|
||||||
|
|
||||||
|
@workflow_node('my_custom_node')
|
||||||
|
class MyCustomNode(WorkflowNode):
|
||||||
|
"""自定义节点"""
|
||||||
|
|
||||||
|
# 元数据
|
||||||
|
type_name = 'my_custom_node'
|
||||||
|
name = '我的自定义节点'
|
||||||
|
description = '这是一个自定义节点'
|
||||||
|
category = 'process' # trigger/process/control/action/integration
|
||||||
|
icon = '🔧'
|
||||||
|
|
||||||
|
# 输入端口
|
||||||
|
inputs = [
|
||||||
|
NodePort(name='input', type='string', description='输入数据', required=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 输出端口
|
||||||
|
outputs = [
|
||||||
|
NodePort(name='output', type='string', description='输出数据'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 配置字段
|
||||||
|
config_schema = [
|
||||||
|
NodeConfig(
|
||||||
|
name='option',
|
||||||
|
type='select',
|
||||||
|
required=True,
|
||||||
|
options=['选项A', '选项B'],
|
||||||
|
description='选择一个选项'
|
||||||
|
),
|
||||||
|
NodeConfig(
|
||||||
|
name='value',
|
||||||
|
type='string',
|
||||||
|
required=False,
|
||||||
|
default='默认值',
|
||||||
|
description='配置值'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
inputs: dict[str, Any],
|
||||||
|
context: ExecutionContext
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""执行节点逻辑"""
|
||||||
|
input_data = inputs.get('input', '')
|
||||||
|
option = self.get_config('option')
|
||||||
|
value = self.get_config('value', '')
|
||||||
|
|
||||||
|
# 处理逻辑
|
||||||
|
result = f"处理: {input_data} with {option} and {value}"
|
||||||
|
|
||||||
|
return {'output': result}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:注册节点
|
||||||
|
|
||||||
|
在 `LangBot/src/langbot/pkg/workflow/nodes/__init__.py` 中导入:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .process import (
|
||||||
|
CodeExecutorNode,
|
||||||
|
HttpRequestNode,
|
||||||
|
DataTransformNode,
|
||||||
|
MyCustomNode, # 添加新节点
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:添加前端配置
|
||||||
|
|
||||||
|
在 `LangBot/web/src/app/home/workflows/components/workflow-editor/node-configs/` 目录下添加配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// process-configs.ts
|
||||||
|
export const processNodeConfigs: NodeConfigMap = {
|
||||||
|
// ... 其他配置
|
||||||
|
|
||||||
|
my_custom_node: {
|
||||||
|
type: 'my_custom_node',
|
||||||
|
label: 'workflows.nodes.myCustomNode',
|
||||||
|
description: 'workflows.nodes.myCustomNodeDesc',
|
||||||
|
icon: 'Wrench',
|
||||||
|
category: 'process',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'option',
|
||||||
|
type: 'select',
|
||||||
|
label: 'workflows.fields.option',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ value: '选项A', label: '选项 A' },
|
||||||
|
{ value: '选项B', label: '选项 B' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
label: 'workflows.fields.value',
|
||||||
|
required: false,
|
||||||
|
defaultValue: '默认值',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4:添加国际化
|
||||||
|
|
||||||
|
在 `LangBot/web/src/i18n/locales/` 中添加翻译:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// zh-Hans.ts
|
||||||
|
workflows: {
|
||||||
|
nodes: {
|
||||||
|
myCustomNode: '我的自定义节点',
|
||||||
|
myCustomNodeDesc: '这是一个自定义节点',
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
option: '选项',
|
||||||
|
value: '值',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调试功能实现
|
||||||
|
|
||||||
|
### 后端调试状态管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DebugExecutionState:
|
||||||
|
"""调试执行状态"""
|
||||||
|
|
||||||
|
def __init__(self, execution_id: str, breakpoints: list[str] = None):
|
||||||
|
self.execution_id = execution_id
|
||||||
|
self.status: str = 'running'
|
||||||
|
self.is_paused: bool = False
|
||||||
|
self.is_stopped: bool = False
|
||||||
|
self.breakpoints: set[str] = set(breakpoints or [])
|
||||||
|
self.logs: list[ExecutionLog] = []
|
||||||
|
self._pause_event = asyncio.Event()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
"""暂停执行"""
|
||||||
|
self.is_paused = True
|
||||||
|
self._pause_event.clear()
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
"""继续执行"""
|
||||||
|
self.is_paused = False
|
||||||
|
self._pause_event.set()
|
||||||
|
|
||||||
|
async def wait_if_paused(self):
|
||||||
|
"""如果暂停则等待"""
|
||||||
|
if self.is_paused:
|
||||||
|
await self._pause_event.wait()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端调试流程
|
||||||
|
|
||||||
|
1. **设置断点**:点击节点设置断点
|
||||||
|
2. **启动调试**:调用 `/debug/start` 启动调试执行
|
||||||
|
3. **轮询状态**:定期调用 `/debug/:id/state` 获取状态
|
||||||
|
4. **控制执行**:调用 pause/resume/step/stop 控制执行
|
||||||
|
5. **查看日志**:实时显示执行日志和节点状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 调试状态轮询
|
||||||
|
const fetchDebugState = async () => {
|
||||||
|
const state = await backendClient.get(
|
||||||
|
`/api/v1/workflows/${workflowUuid}/debug/${executionId}/state`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新节点状态
|
||||||
|
setNodeStates(state.node_states);
|
||||||
|
|
||||||
|
// 追加新日志
|
||||||
|
if (state.new_logs.length > 0) {
|
||||||
|
setLogs(prev => [...prev, ...state.new_logs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查完成状态
|
||||||
|
if (state.status === 'completed' || state.status === 'error') {
|
||||||
|
setDebugState('idle');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展阅读
|
||||||
|
|
||||||
|
- [Workflow 功能设计文档](../../../plans/langbot-workflow-design.md)
|
||||||
|
- [用户使用指南](../user-guide/workflow-guide.md)
|
||||||
|
- [API 认证文档](../API_KEY_AUTH.md)
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "LangBot HTTP Bot Adapter",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Server-to-server HTTP integration for a LangBot pipeline. Inbound messages are POSTed to the unified webhook route; replies are delivered to a configured callback URL (one POST per reply part). All requests are HMAC-SHA256 signed. See docs/platforms/http-bot.md."
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/bots/{bot_uuid}": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Push a message into the pipeline (fire-and-collect)",
|
||||||
|
"description": "Returns 202 immediately. Replies arrive asynchronously on the configured callback URL. Reuse the same session_id within the aggregation window to merge multiple messages into one turn (N->1).",
|
||||||
|
"parameters": [
|
||||||
|
{ "$ref": "#/components/parameters/BotUuid" },
|
||||||
|
{ "$ref": "#/components/parameters/Timestamp" },
|
||||||
|
{ "$ref": "#/components/parameters/Signature" },
|
||||||
|
{ "$ref": "#/components/parameters/Idempotency" }
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InboundMessage" } } }
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Accepted (queued for the pipeline)",
|
||||||
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/AcceptedResponse" } } }
|
||||||
|
},
|
||||||
|
"400": { "$ref": "#/components/responses/Error" },
|
||||||
|
"401": { "$ref": "#/components/responses/Error" },
|
||||||
|
"409": { "$ref": "#/components/responses/Error" },
|
||||||
|
"413": { "$ref": "#/components/responses/Error" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/bots/{bot_uuid}/sync": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Push a message and wait for the collapsed reply",
|
||||||
|
"description": "Blocking convenience mode. Waits for is_final and returns all reply parts collapsed into one array. Lossy (no sequence/streaming). One in-flight sync per session_id.",
|
||||||
|
"parameters": [
|
||||||
|
{ "$ref": "#/components/parameters/BotUuid" },
|
||||||
|
{ "$ref": "#/components/parameters/Timestamp" },
|
||||||
|
{ "$ref": "#/components/parameters/Signature" }
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/InboundMessage" } } }
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The collapsed reply",
|
||||||
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/SyncResponse" } } }
|
||||||
|
},
|
||||||
|
"400": { "$ref": "#/components/responses/Error" },
|
||||||
|
"401": { "$ref": "#/components/responses/Error" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/bots/{bot_uuid}/reset": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Reset a session's conversation",
|
||||||
|
"parameters": [
|
||||||
|
{ "$ref": "#/components/parameters/BotUuid" },
|
||||||
|
{ "$ref": "#/components/parameters/Timestamp" },
|
||||||
|
{ "$ref": "#/components/parameters/Signature" }
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["session_id"],
|
||||||
|
"properties": {
|
||||||
|
"session_id": { "type": "string" },
|
||||||
|
"session_type": { "type": "string", "enum": ["person", "group"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": { "description": "Reset done" },
|
||||||
|
"400": { "$ref": "#/components/responses/Error" },
|
||||||
|
"401": { "$ref": "#/components/responses/Error" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"parameters": {
|
||||||
|
"BotUuid": {
|
||||||
|
"name": "bot_uuid", "in": "path", "required": true,
|
||||||
|
"schema": { "type": "string", "format": "uuid" }
|
||||||
|
},
|
||||||
|
"Timestamp": {
|
||||||
|
"name": "X-LB-Timestamp", "in": "header", "required": true,
|
||||||
|
"description": "Unix seconds; rejected if more than +/-300s from server time.",
|
||||||
|
"schema": { "type": "string" }
|
||||||
|
},
|
||||||
|
"Signature": {
|
||||||
|
"name": "X-LB-Signature", "in": "header", "required": true,
|
||||||
|
"description": "sha256=<hex> of HMAC-SHA256(secret, \"{timestamp}.\" + raw_body).",
|
||||||
|
"schema": { "type": "string" }
|
||||||
|
},
|
||||||
|
"Idempotency": {
|
||||||
|
"name": "X-LB-Idempotency-Key", "in": "header", "required": false,
|
||||||
|
"description": "Dedup key; a repeat within the dedup window returns 409.",
|
||||||
|
"schema": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"Segment": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"properties": {
|
||||||
|
"type": { "type": "string", "enum": ["Plain", "Image", "Voice", "File", "At", "Quote"] },
|
||||||
|
"text": { "type": "string", "description": "For type=Plain." },
|
||||||
|
"url": { "type": "string", "description": "For media types." },
|
||||||
|
"base64": { "type": "string", "description": "For media types (data URI or raw base64)." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"InboundMessage": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["session_id", "message"],
|
||||||
|
"properties": {
|
||||||
|
"session_id": { "type": "string", "description": "Caller-defined; maps 1:1 to a LangBot session." },
|
||||||
|
"session_type": { "type": "string", "enum": ["person", "group"], "default": "person" },
|
||||||
|
"sender": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"group_name": { "type": "string", "description": "For session_type=group." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AcceptedResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "integer", "example": 0 },
|
||||||
|
"msg": { "type": "string", "example": "accepted" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"session_id": { "type": "string" },
|
||||||
|
"accepted_message_id": { "type": "string", "example": "in_01H..." },
|
||||||
|
"aggregating": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SyncResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "integer", "example": 0 },
|
||||||
|
"msg": { "type": "string", "example": "ok" },
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"session_id": { "type": "string" },
|
||||||
|
"reply_to": { "type": "string" },
|
||||||
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Callback": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Delivered by LangBot to your callback_url, one POST per reply part. Signed with the outbound secret.",
|
||||||
|
"properties": {
|
||||||
|
"session_id": { "type": "string" },
|
||||||
|
"reply_to": { "type": "string", "description": "The accepted_message_id this answers." },
|
||||||
|
"sequence": { "type": "integer", "description": "1-based ordinal within the turn." },
|
||||||
|
"is_final": { "type": "boolean", "description": "True on the last part of the turn." },
|
||||||
|
"stream": { "type": "boolean" },
|
||||||
|
"message": { "type": "array", "items": { "$ref": "#/components/schemas/Segment" } },
|
||||||
|
"timestamp": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorEnvelope": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "integer", "example": 40101 },
|
||||||
|
"msg": { "type": "string", "example": "invalid signature: signature_mismatch" },
|
||||||
|
"data": { "nullable": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"Error": {
|
||||||
|
"description": "Error envelope",
|
||||||
|
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# HTTP Bot Adapter — Integration Guide
|
||||||
|
|
||||||
|
Integrate **any backend system** with a LangBot pipeline over plain HTTP. Push
|
||||||
|
messages in via a signed webhook; receive replies on a callback URL. No
|
||||||
|
long-lived connection, full support for message **aggregation** (many inbound
|
||||||
|
messages merged into one turn) and **multi-part replies** (one turn → many
|
||||||
|
outbound messages).
|
||||||
|
|
||||||
|
This is the right adapter for **server-to-server** integrations — ticketing
|
||||||
|
systems, CRMs, internal tools, custom web backends. (For an in-browser,
|
||||||
|
real-time chat widget, use the embeddable Web Page Bot instead.)
|
||||||
|
|
||||||
|
> **5-minute goal:** stand up a callback receiver, send a message, and watch a
|
||||||
|
> multi-part reply arrive — using the reference client in
|
||||||
|
> [`examples/http-bot/`](../../examples/http-bot/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Mental model
|
||||||
|
|
||||||
|
```
|
||||||
|
Your backend ──(1) POST signed message──► LangBot /bots/<bot_uuid>
|
||||||
|
(pipeline runs: aggregate → think → reply)
|
||||||
|
Your callback ◄─(2) POST signed reply(s)── LangBot one POST per reply part
|
||||||
|
```
|
||||||
|
|
||||||
|
- **(1) Inbound** is *fire-and-collect*: LangBot answers `202 Accepted`
|
||||||
|
immediately and does **not** return the pipeline result on that response.
|
||||||
|
- **(2) Outbound** replies arrive later as separate signed POSTs to your
|
||||||
|
`callback_url`. A single turn may produce **several** callbacks (e.g. a tool
|
||||||
|
call narration followed by the final answer).
|
||||||
|
- Everything is keyed by a **`session_id` you choose** (e.g. a ticket number).
|
||||||
|
Each `session_id` maps to one isolated LangBot conversation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Create the bot
|
||||||
|
|
||||||
|
1. In the LangBot dashboard, add a bot and choose the **HTTP Bot** platform.
|
||||||
|
2. Fill in the config:
|
||||||
|
|
||||||
|
| Field | Required | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| **Inbound Signing Secret** | yes | Your backend signs inbound requests with this. |
|
||||||
|
| **Outbound Callback URL** | yes | Where LangBot POSTs replies. **Config-only** — cannot be overridden per message (SSRF protection). |
|
||||||
|
| **Outbound Signing Secret** | no | LangBot signs callbacks with this; defaults to the inbound secret. |
|
||||||
|
| **Default Session Type** | no | `person` (default) or `group`. |
|
||||||
|
| **Require Inbound Signature** | no | Keep `true` in production. |
|
||||||
|
| **Callback Timeout / Max Retries** | no | Defaults: 15s, 3 retries. |
|
||||||
|
|
||||||
|
3. Bind the bot to a **pipeline** and **enable** it.
|
||||||
|
4. Copy the **Inbound Webhook URL** shown in the config — it looks like
|
||||||
|
`https://your-langbot/bots/<bot_uuid>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The signature scheme
|
||||||
|
|
||||||
|
Both directions use the same dependency-free HMAC-SHA256 scheme:
|
||||||
|
|
||||||
|
```
|
||||||
|
signing_string = "{timestamp}." + raw_body_bytes
|
||||||
|
signature = "sha256=" + hex(HMAC_SHA256(secret, signing_string))
|
||||||
|
```
|
||||||
|
|
||||||
|
Sent as headers:
|
||||||
|
|
||||||
|
| Header | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `X-LB-Timestamp` | Unix seconds. Rejected if more than **±300s** from server time. |
|
||||||
|
| `X-LB-Signature` | `sha256=<hex>` over `"{timestamp}." + body`. |
|
||||||
|
| `X-LB-Idempotency-Key` | *(optional, inbound)* dedup key; retries with the same key return `409`. |
|
||||||
|
|
||||||
|
Verify outbound callbacks the same way, using the **outbound** secret (or the
|
||||||
|
inbound secret if you left it blank).
|
||||||
|
|
||||||
|
A six-line reference implementation is in `examples/http-bot/client.py`
|
||||||
|
(`sign()` / `verify()`); a Node/TS version is in `client.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Send your first message (curl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOT="https://your-langbot/bots/<bot_uuid>"
|
||||||
|
SECRET="your-inbound-secret"
|
||||||
|
BODY='{"session_id":"ticket-10293","message":[{"type":"Plain","text":"Export keeps failing on the dashboard."}]}'
|
||||||
|
TS=$(date +%s)
|
||||||
|
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"
|
||||||
|
|
||||||
|
curl -sS -X POST "$BOT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-LB-Timestamp: $TS" \
|
||||||
|
-H "X-LB-Signature: $SIG" \
|
||||||
|
-d "$BODY"
|
||||||
|
# -> 202 {"code":0,"msg":"accepted","data":{"session_id":"ticket-10293","accepted_message_id":"in_...","aggregating":true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The reply(s) will be POSTed to your configured callback URL shortly after.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Inbound request format
|
||||||
|
|
||||||
|
`POST /bots/{bot_uuid}`
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"session_id": "ticket-10293", // REQUIRED. Your stable id. Maps 1:1 to a LangBot session.
|
||||||
|
"session_type": "person", // optional: "person" | "group"; default from config
|
||||||
|
"sender": { // optional metadata, surfaced to the pipeline/plugins
|
||||||
|
"id": "user-5567",
|
||||||
|
"name": "Alice"
|
||||||
|
},
|
||||||
|
"message": [ // REQUIRED. A LangBot MessageChain (array of segments).
|
||||||
|
{ "type": "Plain", "text": "Export keeps failing on the dashboard." },
|
||||||
|
{ "type": "Image", "url": "https://example.com/screenshot.png" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Message segments.** Text uses `{"type":"Plain","text":"..."}`. Images use
|
||||||
|
`{"type":"Image","url":"..."}` (or `base64`). Other supported types: `Voice`,
|
||||||
|
`File`, `At`, `Quote`.
|
||||||
|
|
||||||
|
> Note: the callback URL is **not** accepted in the body — it is taken only from
|
||||||
|
> bot config. This is deliberate (prevents an attacker who obtains the inbound
|
||||||
|
> secret from redirecting replies to an arbitrary host).
|
||||||
|
|
||||||
|
### Aggregation (N → 1)
|
||||||
|
|
||||||
|
If your pipeline has **message aggregation** enabled, send several messages with
|
||||||
|
the **same `session_id`** within the aggregation window and they are merged into
|
||||||
|
**one** pipeline turn. No special flag — just reuse the `session_id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Outbound callback format
|
||||||
|
|
||||||
|
LangBot POSTs each reply part to your `callback_url`:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"session_id": "ticket-10293", // echoes the inbound session
|
||||||
|
"reply_to": "in_01H...", // the accepted_message_id this answers
|
||||||
|
"sequence": 1, // 1-based ordinal within this turn
|
||||||
|
"is_final": false, // true on the last part of the turn
|
||||||
|
"stream": false, // true for streamed chunks
|
||||||
|
"message": [ { "type": "Plain", "text": "Looking into it…" } ],
|
||||||
|
"timestamp": "2026-06-22T09:00:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Your endpoint should return `2xx` quickly. Non-2xx / timeout → LangBot retries
|
||||||
|
with exponential backoff (up to `callback_max_retries`).
|
||||||
|
|
||||||
|
### Multi-part replies (1 → M)
|
||||||
|
|
||||||
|
One turn may emit multiple callbacks, delivered **in `sequence` order** for a
|
||||||
|
given session:
|
||||||
|
|
||||||
|
```
|
||||||
|
seq=1 is_final=false "Checking your export logs…"
|
||||||
|
seq=2 is_final=false "Found 2 failed exports."
|
||||||
|
seq=3 is_final=true "Fixed — please try again."
|
||||||
|
```
|
||||||
|
|
||||||
|
Stitch by `session_id` + `sequence`; the turn is complete when
|
||||||
|
`is_final: true` arrives.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Reset a session
|
||||||
|
|
||||||
|
Start a fresh conversation for a `session_id` (drops history):
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /bots/{bot_uuid}/reset
|
||||||
|
{ "session_id": "ticket-10293", "session_type": "person" }
|
||||||
|
→ 200 { "code":0, "msg":"reset", "data": { "session_id":"ticket-10293", "removed": true } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Signed exactly like an inbound message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Synchronous convenience mode
|
||||||
|
|
||||||
|
If you don't need streaming/multi-part and just want one reply back on the same
|
||||||
|
HTTP call, POST to `/sync`. LangBot waits for the turn to finish and returns all
|
||||||
|
parts **collapsed** into one array:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /bots/{bot_uuid}/sync
|
||||||
|
{ "session_id": "ticket-10293", "message": [ { "type":"Plain", "text":"hi" } ] }
|
||||||
|
→ 200 { "code":0, "msg":"ok",
|
||||||
|
"data": { "session_id":"ticket-10293", "reply_to":"in_...",
|
||||||
|
"message": [ {"type":"Plain","text":"..."}, ... ] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
This is **lossy** (you lose `sequence` / streaming boundaries) and blocks up to
|
||||||
|
`callback_timeout × 4` seconds. Prefer the callback model for anything
|
||||||
|
real-time or multi-part. Only one in-flight `/sync` per `session_id`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Error envelope
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "code": 40101, "msg": "invalid signature: signature_mismatch", "data": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
| HTTP | code | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| 202 | 0 | accepted |
|
||||||
|
| 400 | 40001 | malformed body / missing `session_id` or `message` |
|
||||||
|
| 401 | 40101 | bad/expired signature |
|
||||||
|
| 409 | 40901 | duplicate idempotency key |
|
||||||
|
| 413 | 41301 | message too large (>1 MiB) |
|
||||||
|
| 500 | 50001 | internal error |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Try it end-to-end in 5 minutes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/http-bot
|
||||||
|
pip install flask requests
|
||||||
|
|
||||||
|
# Terminal 1 — your callback receiver (point the bot's callback_url here, e.g. via a tunnel):
|
||||||
|
python client.py serve --port 8900 --secret SHARED_SECRET
|
||||||
|
|
||||||
|
# Terminal 2 — push a message:
|
||||||
|
python client.py push \
|
||||||
|
--url https://your-langbot/bots/<bot_uuid> \
|
||||||
|
--secret SHARED_SECRET \
|
||||||
|
--session ticket-1 \
|
||||||
|
--text "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch Terminal 1 print each reply part (`[part ]` / `[FINAL]`) with its
|
||||||
|
sequence number — that's 1→M working, signatures verified.
|
||||||
|
|
||||||
|
A machine-readable contract is in
|
||||||
|
[`docs/http-bot-openapi.json`](../http-bot-openapi.json).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Security checklist
|
||||||
|
|
||||||
|
- Keep **Require Inbound Signature** on in production.
|
||||||
|
- Use **HTTPS** callback URLs; the URL is config-only (no per-message override).
|
||||||
|
- Treat the secrets like passwords; rotate via the dashboard.
|
||||||
|
- The inbound route is unauthenticated at the framework level **by design** —
|
||||||
|
security comes entirely from the HMAC signature, so never disable it on a
|
||||||
|
public deployment.
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
# Box 系统架构深度分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> 相关文档: [SaaS 阻塞项](./box-issues.md) | [Session 作用域](./box-session-scope.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 全局架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LangBot 主进程 │
|
||||||
|
│ │
|
||||||
|
│ LocalAgentRunner ──> ToolManager ──> NativeToolLoader │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ exec / read / write / edit │
|
||||||
|
│ │ │ glob / grep │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> MCPLoader ──> BoxStdioSession │
|
||||||
|
│ │ │ (shared 容器, 多 process) │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> SkillToolLoader (activate 工具) │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ├──> SkillAuthoringToolLoader │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ └──> PluginToolLoader │
|
||||||
|
│ │ │
|
||||||
|
│ BoxService (门面) │
|
||||||
|
│ ├─ Profile 管理 (locked 字段) │
|
||||||
|
│ ├─ Host mount 校验 (allowed_mount_roots) │
|
||||||
|
│ ├─ Workspace quota 检查 │
|
||||||
|
│ ├─ 输出截断 (head+tail) │
|
||||||
|
│ ├─ Session ID 模板解析 (resolve_box_session_id) │
|
||||||
|
│ ├─ 技能挂载组装 (build_skill_extra_mounts) │
|
||||||
|
│ ├─ 重连循环 (_reconnect_loop, 指数退避) │
|
||||||
|
│ └─ BoxRuntimeConnector │
|
||||||
|
│ ├─ 心跳 loop (20s ping) │
|
||||||
|
│ └─ ActionRPCBoxClient │
|
||||||
|
│ │ Action RPC (stdio 或 WebSocket) │
|
||||||
|
│ │
|
||||||
|
│ SkillManager (skill_mgr) │
|
||||||
|
│ └─ 从 Box runtime 拉取 skills, 不可用时回落 data/skills │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Box Runtime 进程 (SDK 侧) │
|
||||||
|
│ │
|
||||||
|
│ BoxServerHandler (Action RPC 处理, INIT 配置注入) │
|
||||||
|
│ │ │
|
||||||
|
│ BoxRuntime (session 管理 / 进程生命周期 / TTL reaper) │
|
||||||
|
│ │ └─ session.managed_processes: dict[pid, _ManagedProcess]
|
||||||
|
│ │ │
|
||||||
|
│ Backend (启动时根据 box.backend 配置选择): │
|
||||||
|
│ DockerBackend ──┐ │
|
||||||
|
│ PodmanBackend ──┤── CLISandboxBackend │
|
||||||
|
│ NsjailBackend ──┘ (本地 CLI 或 fallback 到容器内 CLI) │
|
||||||
|
│ E2BBackend (云沙箱, 需要 E2B_API_KEY) │
|
||||||
|
│ │
|
||||||
|
│ BoxSkillStore │
|
||||||
|
│ ├─ list / get / create / update / delete │
|
||||||
|
│ ├─ scan_skill_directory / read_skill_file / write_skill_file │
|
||||||
|
│ └─ preview_skill_zip / install_skill_zip (zip 或 GitHub) │
|
||||||
|
│ │
|
||||||
|
│ aiohttp 单端口服务 (默认 :5410): │
|
||||||
|
│ /rpc/ws — Action RPC │
|
||||||
|
│ /v1/sessions/{id}/managed-process/ws — 默认 process │
|
||||||
|
│ /v1/sessions/{id}/managed-process/{pid}/ws — 指定 process │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 容器 / 沙箱 (Docker/Podman 容器, nsjail sandbox, 或 E2B 远程沙箱) │
|
||||||
|
│ - 隔离文件系统 / 网络 / PID 命名空间 │
|
||||||
|
│ - 资源限制 (CPU, 内存, PID 数, 可选 workspace 配额) │
|
||||||
|
│ - 主挂载 (host_path → mount_path) + 任意条 extra_mounts │
|
||||||
|
│ └─ Skills 通过 extra_mounts 挂在 /workspace/.skills/<name> │
|
||||||
|
│ - exec: 用户命令在此执行 │
|
||||||
|
│ - managed process: 多个长驻进程并存 (MCP Server / 自定义服务) │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心设计原则**:
|
||||||
|
- Box Runtime 作为独立进程运行,通过 Action RPC 与 LangBot 主进程通信,两者复用 SDK 的 IO 层(Handler → Connection → Controller)
|
||||||
|
- 一个 session_id 对应一个容器/沙箱实例。同一 session 内可并存多条 mount 与多个 managed process
|
||||||
|
- Skill / 默认 exec / MCP Server 共享同一个 session 容器(详见 [box-session-scope.md](./box-session-scope.md))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. LangBot 侧模块
|
||||||
|
|
||||||
|
### 2.1 BoxService (`pkg/box/service.py`, 722 行)
|
||||||
|
|
||||||
|
应用层门面,协调 Profile、安全校验、配额、连接、Skill 挂载与 Session 模板:
|
||||||
|
|
||||||
|
主要公开方法(按定义顺序):
|
||||||
|
|
||||||
|
```
|
||||||
|
BoxService
|
||||||
|
├─ initialize() 连接 Box Runtime + 默认 workspace 准备
|
||||||
|
├─ _on_runtime_disconnect(connector) 触发重连
|
||||||
|
├─ _reconnect_loop(connector) 指数退避重连
|
||||||
|
├─ available (property) 连接状态
|
||||||
|
│
|
||||||
|
├─ resolve_box_session_id(query) 从 pipeline 模板解析 session_id
|
||||||
|
├─ build_skill_extra_mounts(query) 组装 pipeline-bound skill 的挂载列表
|
||||||
|
│
|
||||||
|
├─ execute_tool(parameters, query) Agent 调用 exec 时的入口
|
||||||
|
│ ├─ _apply_profile / build_spec
|
||||||
|
│ ├─ _validate_host_mount
|
||||||
|
│ ├─ _enforce_workspace_quota (phase=pre)
|
||||||
|
│ ├─ client.execute(spec)
|
||||||
|
│ ├─ _enforce_workspace_quota (phase=post)
|
||||||
|
│ └─ _truncate (stdout/stderr)
|
||||||
|
│
|
||||||
|
├─ execute_spec_payload(spec_payload, ...) 内部入口(其他 loader 调用)
|
||||||
|
├─ create_session(spec_payload, ...) 显式创建 session
|
||||||
|
├─ start_managed_process(session_id, ...) 启动 managed process
|
||||||
|
├─ get_managed_process(session_id, pid) 查询进程状态(pid 默认 'default')
|
||||||
|
├─ stop_managed_process(session_id, pid) 单独停止某个 managed process
|
||||||
|
├─ get_managed_process_websocket_url(...) 返回 WS attach URL
|
||||||
|
│
|
||||||
|
├─ list_skills() / get_skill(name) Skill 元数据
|
||||||
|
├─ create_skill / update_skill / delete_skill Skill CRUD
|
||||||
|
├─ scan_skill_directory(path) 扫描目录
|
||||||
|
├─ list_skill_files / read_skill_file / write_skill_file
|
||||||
|
├─ preview_skill_zip / install_skill_zip zip / GitHub 安装
|
||||||
|
│
|
||||||
|
├─ shutdown() / dispose() 清理:RPC SHUTDOWN + 进程终止
|
||||||
|
├─ get_status() / get_sessions() / get_recent_errors()
|
||||||
|
└─ get_system_guidance() LLM 系统提示
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profile 系统**: 4 个内置 Profile(`default` / `offline_readonly` / `network_basic` / `network_extended`),`locked` frozenset 字段不可被 LLM 覆盖。参数合并顺序:Profile defaults → LLM 请求参数 → locked 强制值。
|
||||||
|
|
||||||
|
**输出截断**: 默认 4000 字符上限,保留前 60% + 后 40%,中间插入 `[...truncated...]`。
|
||||||
|
|
||||||
|
**Skill 挂载合并**: `execute_tool()` 调用时,`build_skill_extra_mounts(query)` 会把当前 pipeline-bound 的所有 skill 的 `package_root` 作为 `extra_mounts` 加入 BoxSpec,挂在 `/workspace/.skills/<name>`。LLM 通过 `activate` 工具显式激活某个 skill 后,工具调用才允许引用这个 skill 的虚拟路径。
|
||||||
|
|
||||||
|
### 2.2 BoxRuntimeConnector (`pkg/box/connector.py`, 357 行)
|
||||||
|
|
||||||
|
管理与 Box Runtime 的通信连接:
|
||||||
|
|
||||||
|
- **本地 stdio**: Unix/macOS 默认路径,fork `python -m langbot_plugin.cli.__init__ box -s --ws-control-port {port}` 子进程(与 plugin runtime 统一走 `lbp` CLI 入口)
|
||||||
|
- **本地 subprocess + WS**: Windows 本地(asyncio ProactorEventLoop 不支持 stdio pipe)
|
||||||
|
- **远程 WebSocket**: Docker 部署 / `box.runtime.endpoint` 显式配置时,连接 `ws://{host}:{port}/rpc/ws`
|
||||||
|
- **同步等待**: `asyncio.Event` + `wait_for(timeout=30s)` 模式确认连接
|
||||||
|
- **心跳**: `_heartbeat_loop()` 每 20s 调用 `ping()`,失败仅 DEBUG 日志(断开检测靠 connection close)
|
||||||
|
- **重连**: `runtime_disconnect_callback` 由 BoxService 提供,触发 `_reconnect_loop`
|
||||||
|
- **INIT 注入**: 连接建立后立即下发当前 `box.*` 配置子树(剔除 `runtime` 私有字段),Runtime 据此初始化 backend
|
||||||
|
|
||||||
|
> **历史改进**: 2026-04-16 版本本文档曾列 P0 「Box 无心跳 / 无重连」,已修复(commit `2dfd9d5d`、`c6882cf`、`5029d9c` 等)。
|
||||||
|
|
||||||
|
### 2.3 BoxWorkspaceSession 工具 (`pkg/box/workspace.py`, 413 行)
|
||||||
|
|
||||||
|
此文件目前提供两类能力:
|
||||||
|
|
||||||
|
1. **路径与命令重写工具函数** — `normalize_host_path` / `rewrite_mounted_path` / `unwrap_venv_path` / `rewrite_venv_command` / `infer_workspace_host_path`,被 MCP loader 与 Skill 路径解析共用。
|
||||||
|
2. **`BoxWorkspaceSession`** — 围绕 BoxService 的轻量包装,专供 MCP-in-Box 场景使用(管理一个共享 session 的 session_id、构建挂载 payload、stage host 文件到共享 workspace)。
|
||||||
|
|
||||||
|
**变化点**: 早期 Skill exec 会为每个 skill 创建独立 BoxWorkspaceSession(独占 session);当前实现已转为 `extra_mounts` 模式,Skill 不再独占容器,只追加挂载。这部分 wrapping 逻辑已从 native loader 移除。
|
||||||
|
|
||||||
|
### 2.4 policy.py (`pkg/box/policy.py`, 98 行) — 仍是死代码
|
||||||
|
|
||||||
|
三层安全策略设计(`SandboxPolicy` / `ToolPolicy` / `ElevatedPolicy`),全项目无任何导入或调用。详见 [SaaS 阻塞项 S2](./box-issues.md)。
|
||||||
|
|
||||||
|
### 2.5 SkillManager (`pkg/skill/manager.py`, 186 行)
|
||||||
|
|
||||||
|
```
|
||||||
|
SkillManager
|
||||||
|
├─ initialize() 调用 reload_skills()
|
||||||
|
├─ reload_skills() 先从 Box runtime list_skills(),
|
||||||
|
│ 不可用则回落 data/skills/ 扫描
|
||||||
|
├─ refresh_skill_from_disk() 单 skill 重新加载
|
||||||
|
├─ get_skill_by_name(name)
|
||||||
|
└─ get_managed_skills_root() 返回 Box 视角的 skills_root 路径
|
||||||
|
```
|
||||||
|
|
||||||
|
skill 元数据通过 `parse_frontmatter` 解析 `SKILL.md` 头部(`name` / `description` / `instructions`),不再做整体扫描的代价(典型 < 50 个)。
|
||||||
|
|
||||||
|
### 2.6 Skill activation (`pkg/skill/activation.py`, 33 行) + Skill loader 辅助
|
||||||
|
|
||||||
|
历史上 skill 通过 LLM 在文本中输出 `[ACTIVATE_SKILL:name]` 标记激活;当前已改为 **Tool Call 机制**:
|
||||||
|
|
||||||
|
- `SkillToolLoader` (`pkg/provider/tools/loaders/skill.py`, 157 行) 暴露 `activate` 工具,参数为 skill 名
|
||||||
|
- 工具实现调用 `register_activated_skill(query, skill_data)`,将激活态写入 `query.variables['_activated_skills']`
|
||||||
|
- 这种 KV-cache-friendly 模式对齐 Claude Code 设计;详见 [box-session-scope.md §4.3](./box-session-scope.md) 的 Tool Call 描述
|
||||||
|
|
||||||
|
`activation.py` 现仅保留对外辅助函数(pipeline 层调用 loader 的 `register_activated_skill`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SDK 侧模块
|
||||||
|
|
||||||
|
### 3.1 BoxRuntime (`box/runtime.py`, 599 行)
|
||||||
|
|
||||||
|
核心编排器,管理 session 生命周期与 backend 调度:
|
||||||
|
|
||||||
|
```
|
||||||
|
Session 生命周期:
|
||||||
|
|
||||||
|
Client EXEC / CREATE_SESSION
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_get_or_create_session(spec)
|
||||||
|
├─ _reap_expired_sessions_locked() 清理 TTL 过期 session
|
||||||
|
├─ 已存在? → _assert_session_compatible() → 复用
|
||||||
|
├─ Backend session 失踪? → 重建 (commit c6882cf)
|
||||||
|
└─ 新建? → backend.start_session(spec) → 创建容器
|
||||||
|
│ └─ 应用 spec.extra_mounts (多挂载)
|
||||||
|
▼
|
||||||
|
execute(spec)
|
||||||
|
├─ 获取 session lock (每 session 独立)
|
||||||
|
├─ backend.exec(session, spec) 在容器中执行命令
|
||||||
|
├─ 更新 last_used_at
|
||||||
|
└─ 超时? → 销毁 session
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Session 保持存活直到:
|
||||||
|
├─ TTL 过期 (默认 300s,下次操作时清理)
|
||||||
|
├─ 执行超时 (自动销毁)
|
||||||
|
├─ 客户端 DELETE_SESSION
|
||||||
|
└─ SHUTDOWN
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键设计**:
|
||||||
|
- 每 session 有独立 `asyncio.Lock`,同一 session 内的命令串行执行
|
||||||
|
- 每 session 维护 `managed_processes: dict[process_id, _ManagedProcess]`,支持多个长驻进程并存(MCP / 自定义)
|
||||||
|
- 全局 `_lock` 保护 `_sessions` dict 的读写
|
||||||
|
- 兼容性检查:比较核心 spec 字段,`image` 字段对不支持自定义镜像的 backend(nsjail/E2B)会跳过
|
||||||
|
|
||||||
|
**Backend 选择 (`_select_backend`)**: 优先级
|
||||||
|
1. 显式 `box.backend` 配置(`docker` / `nsjail` / `e2b`)
|
||||||
|
2. `local` (默认) → Docker / Podman / nsjail CLI 顺序探测
|
||||||
|
3. `get_status` 调用时若当前 backend 不可用,会尝试重新选择 (commit `e5617c7`)
|
||||||
|
|
||||||
|
### 3.2 Backend 系统
|
||||||
|
|
||||||
|
#### CLISandboxBackend (`box/backend.py`, 411 行)
|
||||||
|
|
||||||
|
Docker / Podman 公共基类:
|
||||||
|
|
||||||
|
```
|
||||||
|
start_session(spec):
|
||||||
|
1. validate_sandbox_security(spec)
|
||||||
|
2. docker/podman run -d --rm --name <name>
|
||||||
|
--network none (可选)
|
||||||
|
--cpus/--memory/--pids-limit
|
||||||
|
--read-only + --tmpfs /tmp
|
||||||
|
-v <host>:<mount>:<mode> 主挂载
|
||||||
|
-v <extra.host>:<extra.mount>:.. 额外挂载 (extra_mounts)
|
||||||
|
<image> sh -lc 'while true; do sleep 3600; done'
|
||||||
|
3. 返回 BoxSessionInfo
|
||||||
|
|
||||||
|
exec(session, spec):
|
||||||
|
docker/podman exec -e KEY=VAL <container>
|
||||||
|
sh -lc 'mkdir -p <workdir> && cd <workdir> && <cmd>'
|
||||||
|
|
||||||
|
start_managed_process(session, spec):
|
||||||
|
docker/podman exec -i <container>
|
||||||
|
sh -lc 'mkdir -p <cwd> && cd <cwd> && exec <command> <args>'
|
||||||
|
返回 asyncio.subprocess.Process (stdin/stdout PIPE)
|
||||||
|
```
|
||||||
|
|
||||||
|
容器以 idle 进程启动,实际命令通过 `docker exec` 执行。`--rm` 确保容器退出时自动清理。
|
||||||
|
|
||||||
|
**Windows 支持**: backend 内对 Windows 路径处理与 subprocess 调用做了适配(commit `120817a`)。
|
||||||
|
|
||||||
|
**孤儿清理**: 启动时枚举 `langbot.box=true` 标签的容器,instance_id 不匹配的强制删除。
|
||||||
|
|
||||||
|
#### NsjailBackend (`box/nsjail_backend.py`, 552 行)
|
||||||
|
|
||||||
|
轻量级 Linux 沙箱(无容器引擎依赖):
|
||||||
|
|
||||||
|
- 使用 namespace 隔离(user/mount/pid/ipc/uts/cgroup/net)
|
||||||
|
- 挂载宿主 `/usr`/`/lib`/`/bin`/`/sbin` 只读 + 选定 `/etc` 条目
|
||||||
|
- 每 session 创建独立目录(workspace/tmp/home)
|
||||||
|
- 资源限制: cgroup v2 优先,fallback 到 rlimit
|
||||||
|
- **CLI 兼容**: 通过 `shutil.which(self._nsjail_bin)` 检测系统安装版 nsjail;不存在时再尝试容器内 nsjail(commit `686fcc0`、`feed530`)
|
||||||
|
- **无自定义镜像**: 使用宿主 OS,`image` 字段固定为 `'host'`,兼容性检查跳过 image
|
||||||
|
|
||||||
|
#### E2BBackend (`box/e2b_backend.py`, 429 行)
|
||||||
|
|
||||||
|
云沙箱后端(commit `75b547f` 引入):
|
||||||
|
|
||||||
|
- 通过 `e2b` SDK 与 E2B 平台通信
|
||||||
|
- 配置:`box.e2b.api_key` / `api_url` / `template`
|
||||||
|
- 支持 `extra_mounts`(commit `0fea9b1` 同步上传文件)
|
||||||
|
- 无本地容器引擎依赖,适合无 Docker 的部署或 SaaS 多租户场景
|
||||||
|
- 不支持自定义 image 字段,由 template 控制
|
||||||
|
|
||||||
|
### 3.3 Server (`box/server.py`, 508 行)
|
||||||
|
|
||||||
|
单端口 aiohttp 服务(默认 5410),通过路径区分(commit `8c71ec5` 合并端口):
|
||||||
|
|
||||||
|
1. **Action RPC** (`/rpc/ws`): `BoxServerHandler` 处理所有 action,包括 `INIT` 配置注入、skill store 操作等
|
||||||
|
2. **WS Relay** (`/v1/sessions/{id}/managed-process/ws` 与 `/v1/sessions/{id}/managed-process/{pid}/ws`): 双向桥接 WebSocket ↔ 指定 managed process stdin/stdout
|
||||||
|
|
||||||
|
stdio 模式同样会在 5410 启动 aiohttp,专门承担 managed process attach;Action RPC 走 stdin/stdout。
|
||||||
|
|
||||||
|
### 3.4 Client (`box/client.py`, 377 行)
|
||||||
|
|
||||||
|
`ActionRPCBoxClient` 封装 `Handler.call_action()` 调用:
|
||||||
|
|
||||||
|
- 25+ 方法对应 25+ 个 RPC action(exec / session / managed-process / skill / status / shutdown)
|
||||||
|
- 错误还原: `_translate_action_error()` 通过字符串前缀匹配还原 SDK 侧异常类型
|
||||||
|
- `execute()` timeout = 300s,其他默认 15s
|
||||||
|
- `BoxRuntimeClient` 是 ABC,供后续可能的非 RPC 实现复用
|
||||||
|
|
||||||
|
包级别 `__init__.py` 显式导出:`BoxRuntimeClient`、`ActionRPCBoxClient`(commit `df9c722`)。
|
||||||
|
|
||||||
|
### 3.5 Actions (`box/actions.py`, 34 行)
|
||||||
|
|
||||||
|
`LangBotToBoxAction` 枚举共定义 **25 个** action:
|
||||||
|
|
||||||
|
| 类别 | Actions |
|
||||||
|
|------|---------|
|
||||||
|
| 控制 | `INIT`、`HEALTH`、`STATUS`、`GET_BACKEND_INFO`、`SHUTDOWN` |
|
||||||
|
| 执行 | `EXEC` |
|
||||||
|
| Session | `CREATE_SESSION` / `GET_SESSION` / `GET_SESSIONS` / `DELETE_SESSION` |
|
||||||
|
| Managed Process | `START_MANAGED_PROCESS` / `GET_MANAGED_PROCESS` / `STOP_MANAGED_PROCESS` |
|
||||||
|
| Skill | `LIST_SKILLS` / `GET_SKILL` / `CREATE_SKILL` / `UPDATE_SKILL` / `DELETE_SKILL` / `SCAN_SKILL_DIRECTORY` / `LIST_SKILL_FILES` / `READ_SKILL_FILE` / `WRITE_SKILL_FILE` / `PREVIEW_SKILL_ZIP` / `INSTALL_SKILL_ZIP` |
|
||||||
|
|
||||||
|
### 3.6 Models (`box/models.py`, 331 行)
|
||||||
|
|
||||||
|
核心数据模型:
|
||||||
|
|
||||||
|
| 模型 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `BoxNetworkMode` | `OFF` / `ON` |
|
||||||
|
| `BoxExecutionStatus` | `COMPLETED` / `TIMED_OUT` |
|
||||||
|
| `BoxHostMountMode` | `NONE` / `READ_ONLY` / `READ_WRITE` |
|
||||||
|
| `BoxManagedProcessStatus` | `RUNNING` / `EXITED` |
|
||||||
|
| `BoxMountSpec` | 单条挂载(host_path/mount_path/mode)— **新增** |
|
||||||
|
| `BoxSpec` | 执行请求;新增 `extra_mounts: list[BoxMountSpec]`、`persistent`、`workspace_quota_mb` |
|
||||||
|
| `BoxProfile` | 4 个内置 Profile + `locked` frozenset |
|
||||||
|
| `BoxSessionInfo` | Session 状态(含 backend_name/created_at/last_used_at) |
|
||||||
|
| `BoxManagedProcessSpec` | 长驻进程参数(process_id/command/args/env/cwd) |
|
||||||
|
| `BoxManagedProcessInfo` | 进程状态(status/exit_code/stderr_preview/attached) |
|
||||||
|
| `BoxExecutionResult` | 执行结果(status/exit_code/stdout/stderr/duration_ms) |
|
||||||
|
|
||||||
|
`BoxSpec` 校验器: `workdir` 默认继承 `mount_path`;`host_path` 支持 POSIX 和 Windows 路径;设置 `host_path` 时 `workdir` 必须在 `mount_path` 下。
|
||||||
|
|
||||||
|
### 3.7 BoxSkillStore (`box/skill_store.py`, 647 行)
|
||||||
|
|
||||||
|
新增模块(commit `4ab3502`),把 skill 持久化收归 Box runtime:
|
||||||
|
|
||||||
|
```
|
||||||
|
BoxSkillStore
|
||||||
|
├─ list_skills() / get_skill(name)
|
||||||
|
├─ create_skill(data) / update_skill(name, data) / delete_skill(name)
|
||||||
|
├─ scan_skill_directory(path) 扫描目录返回候选 skill 包列表
|
||||||
|
├─ list_skill_files(name, path) 浏览 skill 内文件树
|
||||||
|
├─ read_skill_file(name, path) / write_skill_file(name, path, content)
|
||||||
|
├─ preview_skill_zip(zip_bytes, ...) 不落盘预览 zip 内容
|
||||||
|
└─ install_skill_zip(zip_bytes, ...) 解压、校验、复制到 skills_root
|
||||||
|
└─ 支持 source_subdir / target_suffix(commit 1aa043f)
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub 安装路径:HTTP 层(`api/http/service/skill.py`)先 `git clone` 拉取,再走 `install_skill_zip` 或 directory 路径。Skill 文件存放于 `box.local.skills_root`(默认 `skills`,相对 `host_root`),容器内对应 `/workspace/.skills/`。
|
||||||
|
|
||||||
|
### 3.8 Security (`box/security.py`, 52 行)
|
||||||
|
|
||||||
|
`validate_sandbox_security()`: 黑名单校验 host_path,阻止挂载 `/etc`/`/proc`/`/sys`/`/dev`/`/root`/`/boot` 及 Docker/Podman socket。
|
||||||
|
|
||||||
|
**已知缺陷**: 根路径 `/` 未拦截,用户 home 目录未拦截,是 denylist 而非 allowlist 策略。详见 [SaaS 阻塞项 S5](./box-issues.md)。
|
||||||
|
|
||||||
|
### 3.9 Errors (`box/errors.py`, 33 行)
|
||||||
|
|
||||||
|
| 异常类型 | 含义 |
|
||||||
|
|----------|------|
|
||||||
|
| `BoxError` | 基类 |
|
||||||
|
| `BoxValidationError` | spec/参数校验失败 |
|
||||||
|
| `BoxBackendUnavailableError` | 无可用 backend |
|
||||||
|
| `BoxRuntimeUnavailableError` | Runtime 服务不可用 |
|
||||||
|
| `BoxSessionConflictError` | session 已存在但 spec 不兼容 |
|
||||||
|
| `BoxSessionNotFoundError` | session 不存在 |
|
||||||
|
| `BoxManagedProcessConflictError` | session 已有同名 process |
|
||||||
|
| `BoxManagedProcessNotFoundError` | process 不存在 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 工具系统集成
|
||||||
|
|
||||||
|
### 4.1 ToolManager 编排 (`toolmgr.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
ToolManager.initialize()
|
||||||
|
├─ NativeToolLoader (exec / read / write / edit / glob / grep)
|
||||||
|
├─ PluginToolLoader (插件工具)
|
||||||
|
├─ MCPLoader (MCP Server 工具)
|
||||||
|
├─ SkillToolLoader (activate 工具 — Tool Call 激活)
|
||||||
|
└─ SkillAuthoringToolLoader (Skill CRUD)
|
||||||
|
|
||||||
|
工具调用优先级: native → plugin → mcp → skill → skill_authoring
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Native Tools (`native.py`, 846 行)
|
||||||
|
|
||||||
|
| 工具 | 是否在 Box 中执行 | 是否访问宿主文件系统 |
|
||||||
|
|------|:---:|:---:|
|
||||||
|
| `exec` | 是 | 否 |
|
||||||
|
| `read` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `write` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `edit` | **否** | **是** — 直接 `open()` 宿主文件 |
|
||||||
|
| `glob` | **否** | **是** — 直接遍历宿主目录 |
|
||||||
|
| `grep` | **否** | **是** — 直接读宿主文件 |
|
||||||
|
|
||||||
|
**沙箱边界不对称**: 这是刻意的设计权衡 — `read`/`write`/`edit`/`glob`/`grep` 绕过沙箱以获得性能(避免容器 I/O 开销与跨进程拷贝),但意味着 LLM 可以直接读写 `allowed_mount_roots` 下任何文件。Skill 路径经 `_resolve_host_path()` 重写,禁止穿越 `package_root`。
|
||||||
|
|
||||||
|
**exec 的 Skill 分支**: 命令中引用 `/workspace/.skills/<name>` 的 skill 时:
|
||||||
|
1. 验证 skill 已激活
|
||||||
|
2. 单次 exec 只能引用一个 skill 包
|
||||||
|
3. 若 skill 是 Python 项目(有 `requirements.txt` 或 `pyproject.toml`),命令会被 venv bootstrap 包裹(在 skill 挂载点内创建 `.venv`)
|
||||||
|
4. 调用 `box_service.execute_tool()` → 走默认 session_id 与已组装好的 `extra_mounts`,**不再为每 skill 起独立 session**
|
||||||
|
|
||||||
|
### 4.3 MCP-in-Box (`mcp_stdio.py`, 354 行)
|
||||||
|
|
||||||
|
`BoxStdioSessionRuntime` 让 MCP stdio 服务器在 Box 容器中运行,**共享 session、多 process**模式(commit `529088e`):
|
||||||
|
|
||||||
|
```
|
||||||
|
initialize()
|
||||||
|
1. 复用/创建共享 session (session_id = _build_box_session_id())
|
||||||
|
- persistent=True,长期保持
|
||||||
|
2. workspace.execute_raw(install_cmd) 安装依赖 (可选)
|
||||||
|
3. 将每个 MCP server 文件 stage 到 /workspace/.mcp/<process_id>/
|
||||||
|
4. workspace.start_managed_process(process_id=<server>)
|
||||||
|
5. websocket_client(ws_url) 通过 WS relay 连接
|
||||||
|
6. ClientSession.initialize() MCP 协议握手
|
||||||
|
```
|
||||||
|
|
||||||
|
配置 (`MCPServerBoxConfig`): `network='on'` (MCP 服务器通常需要网络),`host_path_mode='ro'` (默认只读),`startup_timeout_sec=120` (留时间给 pip install)。
|
||||||
|
|
||||||
|
每条 MCP server 是同一 session 中的一个 managed process,独立的 `process_id`、独立 attach URL,互不阻塞。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 启动与生命周期
|
||||||
|
|
||||||
|
### 5.1 启动顺序 (`build_app.py`)
|
||||||
|
|
||||||
|
```
|
||||||
|
BuildAppStage.run(ap)
|
||||||
|
├─ ... (persistence, models, sessions) ...
|
||||||
|
│
|
||||||
|
├─ BoxService(ap)
|
||||||
|
├─ box_service.initialize()
|
||||||
|
│ └─ connector.initialize()
|
||||||
|
│ ├─ [stdio] fork box subprocess
|
||||||
|
│ ├─ [subprocess+WS] Windows 本地
|
||||||
|
│ └─ [remote WS] connect URL
|
||||||
|
│ └─ 启动心跳 _heartbeat_task
|
||||||
|
├─ ap.box_service = box_service
|
||||||
|
│
|
||||||
|
├─ ToolManager(ap)
|
||||||
|
├─ tool_mgr.initialize()
|
||||||
|
│ ├─ NativeToolLoader (检查 box_service.available)
|
||||||
|
│ ├─ PluginToolLoader
|
||||||
|
│ ├─ MCPLoader (Box 可用时,stdio MCP 走沙箱)
|
||||||
|
│ └─ SkillAuthoringToolLoader
|
||||||
|
├─ ap.tool_mgr = tool_mgr
|
||||||
|
│
|
||||||
|
├─ ... (platform, pipeline) ...
|
||||||
|
├─ SkillManager.initialize() (从 Box runtime 加载 skill 列表)
|
||||||
|
└─ ... (RAG, HTTP, plugins) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
BoxService 在 ToolManager **之前**初始化。ToolManager 创建 loader 时检查 `box_service.available`。
|
||||||
|
|
||||||
|
### 5.2 初始化失败处理
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
await self._runtime_connector.initialize()
|
||||||
|
self._available = True
|
||||||
|
except Exception as e:
|
||||||
|
self._available = False
|
||||||
|
logger.warning(f"Box runtime unavailable: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**静默降级**: Box 初始化失败不会阻止应用启动,仅导致 6 个 native tool、所有 Skill 工具和 MCP-in-Box 工具不暴露给 LLM。与 Plugin 的行为不同(Plugin 失败会抛异常)。
|
||||||
|
|
||||||
|
### 5.3 销毁流程
|
||||||
|
|
||||||
|
```
|
||||||
|
app.dispose()
|
||||||
|
└─ box_service.dispose()
|
||||||
|
├─ connector.dispose()
|
||||||
|
│ ├─ cancel _heartbeat_task
|
||||||
|
│ ├─ cancel _handler_task / _ctrl_task
|
||||||
|
│ └─ terminate subprocess (SIGTERM)
|
||||||
|
└─ loop.create_task(client.shutdown())
|
||||||
|
└─ RPC SHUTDOWN → Box Runtime 清理所有容器
|
||||||
|
```
|
||||||
|
|
||||||
|
Box 额外做了 RPC SHUTDOWN 通知 Runtime 主动清理容器,比 Plugin 的直接杀进程更安全。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 配置
|
||||||
|
|
||||||
|
### config.yaml (重构后)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
box:
|
||||||
|
enabled: true # 整个 Box 子系统的总开关。设为 false 时:
|
||||||
|
# - 不连接远程 Box runtime,不 fork 本地 stdio 子进程
|
||||||
|
# - sandbox 工具 (exec/read/write/edit/glob/grep) 不暴露给 LLM
|
||||||
|
# - skill 添加/编辑 / GitHub 安装 / 文件写入全部拒绝
|
||||||
|
# - stdio 模式的 MCP server 启动时报错(http/sse 模式不受影响)
|
||||||
|
# - skill 列表/读取保持只读可用
|
||||||
|
# BOX__ENABLED 环境变量可覆盖(统一约定)
|
||||||
|
backend: 'local' # 'local' (探测) / 'docker' / 'nsjail' / 'e2b'
|
||||||
|
# 由 box.backend / BOX__BACKEND 选择后端
|
||||||
|
runtime:
|
||||||
|
endpoint: '' # 外部 Runtime 的 WS 基地址 'ws://host:5410'
|
||||||
|
# 留空 = 本地自管 Runtime
|
||||||
|
local:
|
||||||
|
profile: 'default'
|
||||||
|
image: '' # 覆盖 profile 默认 image
|
||||||
|
host_root: './data/box' # 工作区挂载根,Docker 部署需绝对路径
|
||||||
|
default_workspace: '' # 默认 '<host_root>/default'
|
||||||
|
skills_root: 'skills' # Box 管理的 skill 包目录(相对 host_root)
|
||||||
|
allowed_mount_roots: # 默认 ['<host_root>']
|
||||||
|
- './data/box'
|
||||||
|
- '/tmp'
|
||||||
|
workspace_quota_mb: null # 配额覆盖,null = 走 profile
|
||||||
|
e2b:
|
||||||
|
api_key: '' # 也可走 E2B_API_KEY 环境变量
|
||||||
|
api_url: '' # 自托管 E2B 时填写
|
||||||
|
template: '' # 默认 template ID
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重大变更**: 较 2026-04-16 文档,配置结构完全重组(commit `eefdea4`)。原字段 `box.profile` / `box.runtime_url` / `box.shared_host_root` / `box.allowed_host_mount_roots` 全部迁入 `box.local.*` 子表,新增 `box.backend` 与 `box.e2b.*` 配置组。
|
||||||
|
|
||||||
|
### docker-compose.yaml
|
||||||
|
|
||||||
|
`langbot_box` 服务受 compose profile 控制,默认 `docker compose up` **不会**启动它。需要 sandbox 时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile box up # 启动 langbot + langbot_box + plugin runtime
|
||||||
|
docker compose --profile all up # 同上
|
||||||
|
docker compose up # 只起 langbot + plugin runtime (box 关闭)
|
||||||
|
```
|
||||||
|
|
||||||
|
若不起 `langbot_box`,需要同步在 `data/config.yaml` 中设 `box.enabled: false`(或 langbot 容器 env 加 `BOX__ENABLED=false`),否则 LangBot 会一直尝试连接不存在的 Box runtime 并报错。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# langbot_box 的关键 volume
|
||||||
|
volumes:
|
||||||
|
- ${LANGBOT_BOX_ROOT}:${LANGBOT_BOX_ROOT} # 工作区挂载(源/目标同路径)
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock # Docker backend 复用宿主 docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关闭/连接失败时的行为矩阵
|
||||||
|
|
||||||
|
`box.enabled = false` 与"启用但连接失败"在用户可观察行为上**完全一致**——都通过 `BoxService.available = False` 表达,只是 `get_status` 多返回 `enabled` 字段供前端区分文案。
|
||||||
|
|
||||||
|
| 消费方 | Box 可用 | Box 不可用(disabled 或 failed) |
|
||||||
|
|---|---|---|
|
||||||
|
| native exec/read/write/edit/glob/grep 工具 | 暴露给 LLM | **不暴露** |
|
||||||
|
| `activate` / `register_skill` 工具 | 暴露给 LLM | **不暴露** |
|
||||||
|
| stdio MCP server | 在 Box 内启动 | **`_init_stdio_python_server` 抛 RuntimeError** 拒绝;不退化到宿主 stdio |
|
||||||
|
| http/sse MCP server | 正常 | 正常(不依赖 Box) |
|
||||||
|
| Skill 列表/读取 (`list_skills`/`get_skill`/`read_skill_file`) | 走 Box runtime | 走 LangBot 本地 `data/skills/` 只读 fallback |
|
||||||
|
| Skill 创建/编辑/安装/写文件 | 走 Box runtime | **HTTP 400** + 明确错误信息(`_require_box_for_write`) |
|
||||||
|
| Pipeline AI 配置中 `box-session-id-template` | 正常生效 | **前端 banner** 提示字段无效 |
|
||||||
|
| Pipeline 扩展页 `enable_all_skills` / 绑定 skill | 可编辑 | **前端禁用** + banner |
|
||||||
|
| 仪表盘 Box 状态卡片 | 绿点 / "已连接" | 灰点 / "已禁用"(disabled) 或 红点 / "已断开"(failed) |
|
||||||
|
|
||||||
|
> 后端拒写的边界条件:如果 `ap.box_service` **完全没装**(老式 dev mode,没经过 BuildAppStage),`_require_box_for_write` 视作 no-op,保留 `data/skills/` 本地路径——以兼容历史测试与最小化设置。生产环境总会装 `ap.box_service`,因此该 fallback 不会被触发。
|
||||||
|
|
||||||
|
### Pipeline 配置 (templates/metadata/pipeline/ai.yaml)
|
||||||
|
|
||||||
|
`local-agent.config.box-session-id-template` 控制 session 作用域,预设:
|
||||||
|
|
||||||
|
- `{launcher_type}_{launcher_id}` — 每个会话 (推荐,默认)
|
||||||
|
- `{launcher_type}_{launcher_id}_{sender_id}` — 群聊每个用户
|
||||||
|
- `{launcher_type}_{launcher_id}_{conversation_id}` — 每个对话上下文
|
||||||
|
- `{query_id}` — 每条消息(完全隔离)
|
||||||
|
|
||||||
|
详见 [box-session-scope.md](./box-session-scope.md)。
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
| 端点 | 方法 | 说明 | 前端 |
|
||||||
|
|------|------|------|:---:|
|
||||||
|
| `/api/v1/box/status` | GET | 可用性、Profile、后端信息 | ✅ 监控页 |
|
||||||
|
| `/api/v1/box/sessions` | GET | 活跃 session 列表 | ❌ |
|
||||||
|
| `/api/v1/box/errors` | GET | 最近 50 条错误 | ❌ |
|
||||||
|
| `/api/v1/skills` 等 | GET/POST/PUT/DELETE | Skill CRUD、文件浏览、zip/GitHub 安装、preview | ✅ Skill 管理页 |
|
||||||
|
|
||||||
|
前端 `web/src/app/home/monitoring/components/overview-cards/SystemStatusCards.tsx` 已接入 `/api/v1/box/status`,展示 backend 名称、profile 与活跃 session 数。Sessions 与 errors API 仍未接入。
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Box 系统 — SaaS 发布前阻塞项
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> 相关文档: [架构分析](./box-architecture.md) | [Session 作用域](./box-session-scope.md) | [Runtime 对比](./box-vs-plugin-runtime.md) | [测试覆盖](./box-test-coverage.md) | [toB 分析](./box-tob-analysis.md)
|
||||||
|
|
||||||
|
## 范围说明
|
||||||
|
|
||||||
|
**自部署社区版已具备发布条件**:默认 stdio 模式、box 为可选项;box 关闭 / 不可用时后端、前端、工具、skill、stdio-MCP 均能干净降级(清晰报错、不崩溃);配置向后兼容(旧 `data/config.yaml` 可直接启动);无新增 ORM 模型、无迁移欠债;市场安装失败不会破坏实例。CI 全绿。
|
||||||
|
|
||||||
|
本清单**只保留发布 SaaS / 多租户 / 公网暴露前必须处理的阻塞项**。社区版(可信、单运营者、内网)不受这些项阻塞——它们的风险面在"不可信调用方能直接触达 Box 控制面"或"多租户共享资源"的场景才成立。
|
||||||
|
|
||||||
|
## 已解决(社区版发布前)
|
||||||
|
|
||||||
|
| 项 | 处理 |
|
||||||
|
|----|------|
|
||||||
|
| 工具调用循环无上限 (原 #13) | `localagent.py` 增加 `MAX_TOOL_CALL_ROUNDS=128`,超限优雅终止(`cafef1a3`) |
|
||||||
|
| 配额校验同步遍历阻塞事件循环 (原 #10) | `_enforce_workspace_quota` 改 async,工作区遍历走 `asyncio.to_thread`(`cafef1a3`) |
|
||||||
|
| `host_path` 挂载白名单 (原 #3 的 LangBot 侧) | `pkg/box/service.py` `allowed_mount_roots` 白名单,空列表时拒绝一切宿主挂载 |
|
||||||
|
| 重复的 `_is_path_under` (原 #12) | 已去重,仅保留一处定义 |
|
||||||
|
| 重连 / 心跳 / Windows 兼容 / nsjail image 字段 / 前端 Box 状态接入 | 见上一轮 review 记录,均已合入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SaaS 阻塞项
|
||||||
|
|
||||||
|
### S1. Box 控制面无认证 — Critical
|
||||||
|
|
||||||
|
- **位置**: SDK `box/server.py` — Action RPC WS (`/rpc/ws`) 与 managed-process relay (`/v1/sessions/{id}/managed-process/{pid}/ws`)
|
||||||
|
- **现状**: 两个 WS handler 在 `ws.prepare` 后直接服务,无任何 token / 鉴权;box 默认绑定 `0.0.0.0:5410`。任何能触达该端口者可发起 `EXEC`、创建 session、attach 任意 session 的 managed-process stdin/stdout、甚至 `SHUTDOWN`。LangBot→box 的 INIT 也未下发任何凭证。
|
||||||
|
- **缓解现状**: 默认 `docker-compose.yaml` 的 `langbot_box` 未把 5410 发布到宿主(爆炸半径限于内网 bridge);但 box 挂载了 `/var/run/docker.sock`,同网络的任意服务(含被攻破的插件)→ 宿主 root。若运营者把 5410 发布到宿主或独立以 `0.0.0.0` 起 box,则完全裸奔。
|
||||||
|
- **要求**: INIT 时下发 token,两个 WS 路由按连接校验(query/header)。这是 SaaS 的**头号**阻塞项。
|
||||||
|
|
||||||
|
### S2. 无 exec 授权模型(policy.py 死代码) — High
|
||||||
|
|
||||||
|
- **位置**: LangBot `pkg/box/policy.py`(`SandboxPolicy` / `ToolPolicy` / `ElevatedPolicy` 全项目无引用);`pkg/provider/tools/loaders/native.py`;`pkg/provider/tools/toolmgr.py`
|
||||||
|
- **现状**: 原生工具(`exec/read/write/edit/glob/grep`)按"box 是否可用"全有或全无地暴露,**无 per-pipeline 的 exec 网关 / 工具白名单 / 沙箱模式 / 权限提升控制**。只要 box 可用,任何使用 local-agent + 函数调用模型的 pipeline 都能跑任意 shell。
|
||||||
|
- **要求**: 接入 policy.py(或等价机制),按 pipeline 控制是否暴露 `exec`、可用工具白名单、沙箱网络/只读模式。
|
||||||
|
|
||||||
|
### S3. 会话资源无界(DoS) — High
|
||||||
|
|
||||||
|
- **#5 session 数量无上限**: SDK `box/runtime.py` `_get_or_create_session` 的 `_sessions` dict 无容量限制——可变 `session_id` 的恶意调用可无限创建容器,耗尽宿主 CPU/内存/PID/磁盘。
|
||||||
|
- **#8 无定时回收**: 过期 session 仅在 `_get_or_create_session` 时机会性清理,无独立周期任务;一波创建后转静默会永久泄漏容器。
|
||||||
|
- **要求**: `max_sessions` 上限(拒绝或 LRU),加独立周期 reaper(如 60s)。
|
||||||
|
|
||||||
|
### S4. 工作区配额无内核级限制(TOCTOU) — Med-High
|
||||||
|
|
||||||
|
- **位置**: LangBot `pkg/box/service.py` `_enforce_workspace_quota`(应用层 read-then-check);SDK 侧 `workspace_quota_mb` 仅记录/透传,无 `--storage-opt size=` 等内核/FS 限额
|
||||||
|
- **现状**: 执行前后两次检查之间存在竞态窗口;单条命令(`dd`/`fallocate`)可在检查间隙撑爆磁盘,事后检查只能补救。
|
||||||
|
- **要求**: Docker `--storage-opt size=` 做内核级限制,或 Redis 原子计数预留式配额。
|
||||||
|
|
||||||
|
### S5. 挂载校验缺口 — Med-High
|
||||||
|
|
||||||
|
- **位置**: SDK `box/security.py` `_BLOCKED_HOST_PATHS_POSIX`;`box/backend.py` 的 `extra_mounts` 处理
|
||||||
|
- **现状**: ① SDK 黑名单仍不含 `/`(前缀匹配,`host_path="/"` 可通过,挂载整个宿主 fs);用户 home、`/usr`、`/opt`、`/tmp` 也未拦截。② `validate_sandbox_security` 只校验 `spec.host_path`,**从不遍历 `spec.extra_mounts`**——LangBot 侧 `allowed_mount_roots` 也只校验 `host_path`。当前 `extra_mounts` 仅由 `build_skill_extra_mounts` 内部填充(agent 不可达),但缺乏纵深防御:一旦 S1 的无认证 RPC 被触达,extra_mounts 可挂任意宿主路径,两层都不拦。
|
||||||
|
- **要求**: SDK 黑名单加入 `/`(或改白名单);`extra_mounts` 在 SDK 与 LangBot 两侧都纳入挂载校验。
|
||||||
|
|
||||||
|
### S6. 容器加固缺失 — Med
|
||||||
|
|
||||||
|
- **位置**: SDK `box/backend.py` 的 `docker run` 组装
|
||||||
|
- **现状**: 未设置 `--cap-drop=ALL`、`--security-opt=no-new-privileges`、非 root `--user`;叠加挂载 docker.sock,逃逸面偏大。
|
||||||
|
- **要求**: 默认加上上述加固 flag(需回归常用 skill 不被破坏)。
|
||||||
|
|
||||||
|
### S7. 全局锁内执行慢操作(扩展性) — Med
|
||||||
|
|
||||||
|
- **位置**: SDK `box/runtime.py` `_get_or_create_session`:`self._lock` 持有期间调用 `backend.start_session()`(`docker run` / nsjail 启动 / E2B `Sandbox.create`)
|
||||||
|
- **影响**: 冷启动(镜像拉取数秒、E2B >1s)期间串行阻塞所有并发请求——多租户负载下整个 Box runtime 停顿。降级表现是延迟而非失败。
|
||||||
|
- **要求**: 锁内只做状态检查与注册,容器创建移到锁外。
|
||||||
|
|
||||||
|
### S8. 其他硬化 / 跟进 — Low
|
||||||
|
|
||||||
|
- **#9** SDK `box/server.py` 直接读 `runtime._sessions` 私有字段、绕过锁,并发下可能读到不一致状态——应加公共访问方法。
|
||||||
|
- **#16** `pkg/provider/tools/toolmgr.py` `execute_func_call` 按优先级分发,plugin/MCP 若有同名 `exec/read/write/...` 工具会被静默遮蔽——应加命名空间或冲突告警。
|
||||||
|
- **#4** SDK `box/runtime.py` INIT/handshake 与 backend 实例化的残留竞态(仅"纯远程 WS box 先启动、LangBot 后连"场景成立;stdio/compose 路径下 config 经 env 在 spawn 时已就位,无竞态)——应在 INIT 完成前拒绝业务 action。
|
||||||
|
- **#11** `extra_mounts` 在容器创建时固定(SDK `runtime.py` 兼容性检查不含 extra_mounts);长生命周期共享 session 后续新激活的 skill 不会挂上(当前缓解:创建时挂上 pipeline 绑定的全部 skill)——动态绑定场景需销毁重建或文档说明。
|
||||||
|
- **#21** 集成测试未进 CI:容器实际执行、E2B 真机、managed-process WS attach 仅本地可跑。安全关键路径缺自动化覆盖——SaaS 前建议加 Docker-in-Docker CI stage 或合并前手动 checklist。
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
# Box Session Scope Design
|
||||||
|
|
||||||
|
> Date: 2026-04-18 (last reviewed 2026-06-02)
|
||||||
|
> Status (2026-06-02): the self-hosted community edition is release-ready (box optional, clean degradation, no migration debt). Tool-call loop cap, async quota scan, and the host_path mount allowlist have landed. Remaining multi-tenant / security hardening is tracked in [box-issues.md](./box-issues.md).
|
||||||
|
> Branch: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
> Related: [Box Architecture](./box-architecture.md) | [Box vs Plugin Runtime](./box-vs-plugin-runtime.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Implementation Status (2026-05-19)
|
||||||
|
|
||||||
|
This document was authored as a design proposal. The current `feat/sandbox` branch
|
||||||
|
has shipped the design largely as written:
|
||||||
|
|
||||||
|
| Item | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `BoxMountSpec` + `BoxSpec.extra_mounts` | ✅ Shipped | SDK `box/models.py` |
|
||||||
|
| Docker / nsjail / E2B backends apply extra mounts | ✅ Shipped | Last gap closed by SDK commit `0fea9b1` (E2B) |
|
||||||
|
| `box-session-id-template` in `local-agent` pipeline config | ✅ Shipped | `templates/metadata/pipeline/ai.yaml`, default `{launcher_type}_{launcher_id}` |
|
||||||
|
| `BoxService.resolve_box_session_id(query)` | ✅ Shipped | `pkg/box/service.py:166` |
|
||||||
|
| `BoxService.build_skill_extra_mounts(query)` | ✅ Shipped | `pkg/box/service.py:189` |
|
||||||
|
| Skill exec uses unified container + extra mounts | ✅ Shipped | `pkg/provider/tools/loaders/native.py` skill branch |
|
||||||
|
| MCP-in-Box uses shared persistent session, multi-process | ✅ Shipped (earlier than originally scoped) | SDK commit `529088e`, LangBot `mcp_stdio.py:_build_box_session_id` |
|
||||||
|
| `BoxManagedProcessSpec.process_id` + multi-process per session | ✅ Shipped | `BoxRuntime` keeps `managed_processes: dict[pid, _ManagedProcess]` |
|
||||||
|
| Per-tenant / quota integration with templates | ❌ Not started | See [box-tob-analysis.md](./box-tob-analysis.md) |
|
||||||
|
|
||||||
|
The "Phase 2 deferred" note in §10 is **out of date** — MCP unification went in on
|
||||||
|
the same line. Pipeline-scoped (not user-scoped) MCP container is the realized
|
||||||
|
behavior: each pipeline's MCP servers share one `mcp-<pipeline>` session, and
|
||||||
|
user exec sessions use the template-derived id.
|
||||||
|
|
||||||
|
The remaining open work is multi-tenant overlays (tenant_id in session_id,
|
||||||
|
quota counters keyed by tenant), tracked in the toB analysis doc rather than here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Problems
|
||||||
|
|
||||||
|
### 1.1 Default exec: per-message containers
|
||||||
|
|
||||||
|
Currently, `BoxService.execute_tool()` sets `session_id = str(query.query_id)` — an
|
||||||
|
auto-incrementing integer per incoming message. Every user message creates a new sandbox
|
||||||
|
container. Dependencies installed and in-container state are lost between messages.
|
||||||
|
|
||||||
|
### 1.2 Three isolated container pools
|
||||||
|
|
||||||
|
Default exec, skills, and MCP servers each manage their own containers with
|
||||||
|
independent session IDs:
|
||||||
|
|
||||||
|
| Path | Session ID | Container |
|
||||||
|
|--------------|-----------------------------------------------|-------------|
|
||||||
|
| Default exec | `str(query_id)` (per message) | Ephemeral |
|
||||||
|
| Skill exec | `skill-{launcher}_{id}-{skill_name}` | Per skill |
|
||||||
|
| MCP stdio | `mcp-{server_uuid}` | Per server |
|
||||||
|
|
||||||
|
This means a single logical user interaction can spawn 3+ containers that cannot
|
||||||
|
share state, see each other's files, or reuse installed dependencies.
|
||||||
|
|
||||||
|
### 1.3 Single bind mount limitation
|
||||||
|
|
||||||
|
`BoxSpec` currently supports only **one** `host_path` → `mount_path` bind mount.
|
||||||
|
This prevents mounting both a default workspace and skill directories into the
|
||||||
|
same container.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Concept Model
|
||||||
|
|
||||||
|
```
|
||||||
|
Platform Message
|
||||||
|
→ Query (query_id: int, auto-increment, per message)
|
||||||
|
→ Session (launcher_type + launcher_id, per chat window)
|
||||||
|
→ Conversation (uuid, per dialogue context within a Session)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Concept | Key | Example | Scope |
|
||||||
|
|---------------|-------------------------------------|----------------------------|------------------------------|
|
||||||
|
| Query | `query_id` | `42` | Single message |
|
||||||
|
| Session | `launcher_type` + `launcher_id` | `group_123456` | Chat window (group or PM) |
|
||||||
|
| Conversation | `conversation_id` (UUID) | `a1b2c3d4-...` | Dialogue context within a Session |
|
||||||
|
| Sender | `sender_id` | `789` | Individual user |
|
||||||
|
|
||||||
|
Note: in a **group chat**, all users share the same Session (keyed by `group_id`). The
|
||||||
|
individual sender is tracked as `sender_id` but does not affect Session/Conversation routing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Target Scenarios
|
||||||
|
|
||||||
|
| # | Scenario | Box Granularity | Desired `session_id` |
|
||||||
|
|----|--------------------------------|------------------------------------------|---------------------------------------------------------|
|
||||||
|
| 1 | Personal assistant | 1 Box per user, long-lived | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 2 | Customer service | 1 Box per customer, cross-pipeline | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 3 | Internal employee tool | 1 Box per employee | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 4 | Group chat shared assistant | 1 Box per group | `{launcher_type}_{launcher_id}` |
|
||||||
|
| 5 | Group chat isolated per user | 1 Box per user within a group | `{launcher_type}_{launcher_id}_{sender_id}` |
|
||||||
|
| 6 | Teaching (cross-channel) | 1 Box per student across groups/PMs | `{sender_id}` |
|
||||||
|
| 7 | One-off execution | 1 Box per message (current behavior) | `{query_id}` |
|
||||||
|
| 8 | Multi-project development | 1 Box per conversation context | `{launcher_type}_{launcher_id}_{conversation_id}` |
|
||||||
|
|
||||||
|
No single fixed granularity covers all scenarios. A template-based approach is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Design Overview
|
||||||
|
|
||||||
|
Two key changes:
|
||||||
|
|
||||||
|
1. **Unified container**: exec, skills, and MCP all share the same container per
|
||||||
|
session scope. No more separate container pools.
|
||||||
|
2. **Configurable session scope**: `session_id` is generated from a template with
|
||||||
|
pipeline variables, configurable per pipeline.
|
||||||
|
|
||||||
|
### 4.1 Unified Container with Multiple Mounts
|
||||||
|
|
||||||
|
A single container per session scope is created on first use. It has:
|
||||||
|
|
||||||
|
- **Primary mount**: default workspace at `/workspace` (from `default_host_workspace`)
|
||||||
|
- **Skill mounts**: each pipeline-bound skill's `package_root` mounted at
|
||||||
|
`/workspace/.skills/{skill_name}/`
|
||||||
|
- **MCP servers**: run as managed processes inside the same container
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id = "group_123456")
|
||||||
|
/workspace/ ← default workspace (bind mount, rw)
|
||||||
|
/workspace/.skills/web-search/ ← skill package (bind mount, rw)
|
||||||
|
/workspace/.skills/data-analysis/ ← skill package (bind mount, rw)
|
||||||
|
[managed process: mcp-server-a] ← MCP server running inside
|
||||||
|
[managed process: mcp-server-b] ← MCP server running inside
|
||||||
|
```
|
||||||
|
|
||||||
|
This requires extending `BoxSpec` to support multiple mounts (see §5).
|
||||||
|
|
||||||
|
### 4.2 Session ID Template
|
||||||
|
|
||||||
|
A new field `box-session-id-template` in the `local-agent` pipeline runner config
|
||||||
|
controls the session scope:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# templates/metadata/pipeline/ai.yaml (under local-agent.config)
|
||||||
|
- name: box-session-id-template
|
||||||
|
label:
|
||||||
|
en_US: Sandbox Scope
|
||||||
|
zh_Hans: 沙箱作用域
|
||||||
|
description:
|
||||||
|
en_US: >-
|
||||||
|
Determines how sandbox environments are shared. Use variables to
|
||||||
|
control isolation granularity.
|
||||||
|
zh_Hans: >-
|
||||||
|
决定沙箱环境的共享方式。使用变量控制隔离粒度。
|
||||||
|
type: select
|
||||||
|
required: false
|
||||||
|
default: "{launcher_type}_{launcher_id}"
|
||||||
|
options:
|
||||||
|
- value: "{launcher_type}_{launcher_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per chat (Recommended)
|
||||||
|
zh_Hans: 每个会话(推荐)
|
||||||
|
- value: "{launcher_type}_{launcher_id}_{sender_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per user in chat
|
||||||
|
zh_Hans: 会话中每个用户
|
||||||
|
- value: "{launcher_type}_{launcher_id}_{conversation_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per conversation context
|
||||||
|
zh_Hans: 每个对话上下文
|
||||||
|
- value: "{query_id}"
|
||||||
|
label:
|
||||||
|
en_US: Per message (isolated)
|
||||||
|
zh_Hans: 每条消息(完全隔离)
|
||||||
|
```
|
||||||
|
|
||||||
|
Available template variables (populated by PreProcessor in `query.variables`):
|
||||||
|
|
||||||
|
| Variable | Source | Example |
|
||||||
|
|---------------------|---------------------------------|----------------------|
|
||||||
|
| `{launcher_type}` | `query.session.launcher_type` | `person` / `group` |
|
||||||
|
| `{launcher_id}` | `query.session.launcher_id` | `123456` |
|
||||||
|
| `{sender_id}` | `query.sender_id` | `789` |
|
||||||
|
| `{conversation_id}` | `conversation.uuid` | `a1b2c3d4-...` |
|
||||||
|
| `{query_id}` | `query.query_id` | `42` |
|
||||||
|
|
||||||
|
Default `{launcher_type}_{launcher_id}` covers scenarios 1–4 out of the box.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. SDK Changes: Multi-Mount BoxSpec
|
||||||
|
|
||||||
|
### 5.1 Model Extension
|
||||||
|
|
||||||
|
```python
|
||||||
|
# box/models.py
|
||||||
|
|
||||||
|
class BoxMountSpec(pydantic.BaseModel):
|
||||||
|
"""A single bind mount specification."""
|
||||||
|
host_path: str
|
||||||
|
mount_path: str
|
||||||
|
mode: BoxHostMountMode = BoxHostMountMode.READ_WRITE
|
||||||
|
|
||||||
|
class BoxSpec(pydantic.BaseModel):
|
||||||
|
# ... existing fields ...
|
||||||
|
host_path: str | None = None # Primary mount (backward compat)
|
||||||
|
host_path_mode: BoxHostMountMode = BoxHostMountMode.READ_WRITE
|
||||||
|
mount_path: str = DEFAULT_BOX_MOUNT_PATH
|
||||||
|
extra_mounts: list[BoxMountSpec] = [] # NEW: additional mounts
|
||||||
|
```
|
||||||
|
|
||||||
|
`extra_mounts` is additive — the existing `host_path` / `mount_path` pair remains
|
||||||
|
the primary mount for backward compatibility.
|
||||||
|
|
||||||
|
### 5.2 Backend: Apply Extra Mounts
|
||||||
|
|
||||||
|
```python
|
||||||
|
# box/backend.py — CLISandboxBackend.start_session()
|
||||||
|
|
||||||
|
# Primary mount (unchanged)
|
||||||
|
if spec.host_path is not None and spec.host_path_mode != BoxHostMountMode.NONE:
|
||||||
|
args.extend(['-v', f'{spec.host_path}:{spec.mount_path}:{spec.host_path_mode.value}'])
|
||||||
|
|
||||||
|
# Extra mounts (NEW)
|
||||||
|
for mount in spec.extra_mounts:
|
||||||
|
if mount.mode != BoxHostMountMode.NONE:
|
||||||
|
args.extend(['-v', f'{mount.host_path}:{mount.mount_path}:{mount.mode.value}'])
|
||||||
|
```
|
||||||
|
|
||||||
|
Same pattern for nsjail backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. LangBot Changes
|
||||||
|
|
||||||
|
### 6.1 Session ID Resolution
|
||||||
|
|
||||||
|
In `BoxService.execute_tool()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before:
|
||||||
|
spec_payload.setdefault('session_id', str(query.query_id))
|
||||||
|
|
||||||
|
# After:
|
||||||
|
template = (query.pipeline_config or {}).get('ai', {}) \
|
||||||
|
.get('local-agent', {}).get('box-session-id-template',
|
||||||
|
'{launcher_type}_{launcher_id}')
|
||||||
|
variables = query.variables or {}
|
||||||
|
session_id = template.format_map(collections.defaultdict(
|
||||||
|
lambda: 'unknown', variables
|
||||||
|
))
|
||||||
|
spec_payload.setdefault('session_id', session_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Skill Exec: Use Same Container
|
||||||
|
|
||||||
|
Currently `native.py:_invoke_exec` creates a separate `BoxWorkspaceSession` per
|
||||||
|
skill with `host_path=package_root`. Instead:
|
||||||
|
|
||||||
|
1. Use the **same session_id** as default exec (from the template).
|
||||||
|
2. Pass the skill's `package_root` as an **extra mount** at
|
||||||
|
`/workspace/.skills/{skill_name}/` instead of replacing `/workspace`.
|
||||||
|
3. The container already has the default workspace at `/workspace`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# native.py — _invoke_exec, skill branch (REVISED)
|
||||||
|
|
||||||
|
# Same session_id as default exec
|
||||||
|
session_id = resolve_box_session_id(query)
|
||||||
|
|
||||||
|
spec_payload = {
|
||||||
|
'cmd': rewritten_command,
|
||||||
|
'workdir': rewritten_workdir,
|
||||||
|
'session_id': session_id,
|
||||||
|
'extra_mounts': [{
|
||||||
|
'host_path': package_root,
|
||||||
|
'mount_path': f'/workspace/.skills/{selected_skill_name}',
|
||||||
|
'mode': 'rw',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
result = await self.ap.box_service.execute_spec_payload(spec_payload, query)
|
||||||
|
```
|
||||||
|
|
||||||
|
The virtual path `/workspace/.skills/{name}` no longer needs rewriting at the
|
||||||
|
command level — it maps directly to the bind mount path inside the container.
|
||||||
|
|
||||||
|
### 6.3 MCP: Use Same Container
|
||||||
|
|
||||||
|
MCP servers should run inside the same container as exec and skills. Changes:
|
||||||
|
|
||||||
|
1. `BoxStdioSessionRuntime` uses the pipeline's session_id template instead of
|
||||||
|
`mcp-{server_uuid}`.
|
||||||
|
2. MCP server's working directory is a subdirectory (e.g. `/workspace/.mcp/{name}/`).
|
||||||
|
3. MCP server's dependencies are mounted or installed into that subdirectory.
|
||||||
|
4. The MCP server runs as a managed process inside the shared container.
|
||||||
|
|
||||||
|
Since MCP servers start at LangBot boot (not per-query), the session must be
|
||||||
|
created eagerly. The container will be kept alive by the managed process
|
||||||
|
exemption in TTL reaping (`runtime.py:259`).
|
||||||
|
|
||||||
|
**Note**: MCP sessions are pipeline-scoped (not per-launcher), so their session_id
|
||||||
|
should be a **fixed identifier per pipeline** rather than the user-facing template.
|
||||||
|
This means one shared MCP container per pipeline, with user exec sessions separate.
|
||||||
|
|
||||||
|
Alternatively, in a future iteration, MCP managed processes could be launched
|
||||||
|
lazily into the user's container on first MCP tool call. This is more complex
|
||||||
|
but maximizes sharing. For V1, keeping MCP containers at pipeline scope is
|
||||||
|
simpler and more predictable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Mount Layout Summary
|
||||||
|
|
||||||
|
### Default exec (no skills activated)
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id from template)
|
||||||
|
/workspace/ ← default_host_workspace (rw)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exec with activated skills
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (same session_id)
|
||||||
|
/workspace/ ← default_host_workspace (rw)
|
||||||
|
/workspace/.skills/web-search/ ← skill package_root (rw)
|
||||||
|
/workspace/.skills/data-analysis/ ← skill package_root (rw)
|
||||||
|
```
|
||||||
|
|
||||||
|
Extra mounts are **additive** — they are added when the container is first
|
||||||
|
created (or on the first exec that references a skill). Since Docker bind
|
||||||
|
mounts are specified at container creation time, skills must be known at
|
||||||
|
creation time.
|
||||||
|
|
||||||
|
**Resolution**: When creating a container, inject `extra_mounts` for **all
|
||||||
|
pipeline-bound skills** (from `extensions_preferences`), not just the
|
||||||
|
currently activated one. This way any skill can be activated later without
|
||||||
|
recreating the container.
|
||||||
|
|
||||||
|
### MCP servers (V1: pipeline-scoped)
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (session_id = "mcp-pipeline-{pipeline_uuid}")
|
||||||
|
/workspace/ ← MCP shared workspace
|
||||||
|
/workspace/.mcp/server-a/ ← MCP server A files
|
||||||
|
/workspace/.mcp/server-b/ ← MCP server B files
|
||||||
|
[managed process: server-a]
|
||||||
|
[managed process: server-b]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Data Migration
|
||||||
|
|
||||||
|
Existing pipelines do not have `box-session-id-template`. The backend uses
|
||||||
|
`.get(..., default)` so missing keys fall back to `{launcher_type}_{launcher_id}`.
|
||||||
|
This changes behavior from per-message to per-launcher for existing pipelines.
|
||||||
|
|
||||||
|
Recommendation: **accept the behavior change** — per-launcher is the more
|
||||||
|
intuitive default, and the old per-message behavior was rarely desired.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Cloud Quota Implications
|
||||||
|
|
||||||
|
| Scope | Typical concurrent containers |
|
||||||
|
|-----------------------------------------------|-------------------------------|
|
||||||
|
| `{query_id}` (per message) | Many, short-lived |
|
||||||
|
| `{launcher_type}_{launcher_id}` (per chat) | = active chat count |
|
||||||
|
| `{sender_id}` (per user) | = active user count |
|
||||||
|
| `{conversation_id}` (per conversation) | Between per-chat and per-msg |
|
||||||
|
|
||||||
|
With the unified container model, each scope value maps to exactly **one**
|
||||||
|
container (instead of potentially 3+ per-message). This significantly reduces
|
||||||
|
resource usage.
|
||||||
|
|
||||||
|
Quota enforcement point: `BoxRuntime._get_or_create_session()` in the SDK.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Session scope + skill unification (this PR)
|
||||||
|
|
||||||
|
1. **SDK**: Extend `BoxSpec` with `extra_mounts: list[BoxMountSpec]`.
|
||||||
|
2. **SDK**: Update Docker/nsjail backends to apply extra mounts.
|
||||||
|
3. **LangBot**: Add `box-session-id-template` to `local-agent` YAML metadata
|
||||||
|
and default pipeline config JSON.
|
||||||
|
4. **LangBot**: Update `BoxService.execute_tool()` to use template interpolation.
|
||||||
|
5. **LangBot**: Update `native.py:_invoke_exec` skill branch to use same
|
||||||
|
session_id + extra mounts instead of separate `BoxWorkspaceSession`.
|
||||||
|
6. **LangBot**: On container creation, inject extra mounts for all
|
||||||
|
pipeline-bound skills.
|
||||||
|
7. **Frontend**: No code change — `DynamicFormComponent` renders `select` fields.
|
||||||
|
8. **Tests**: Unit tests for template interpolation and multi-mount specs.
|
||||||
|
|
||||||
|
### Phase 2: MCP unification (future)
|
||||||
|
|
||||||
|
1. Refactor `BoxStdioSessionRuntime` to use pipeline-scoped shared container.
|
||||||
|
2. MCP servers become managed processes in the shared container.
|
||||||
|
3. Support multiple concurrent managed processes per container.
|
||||||
|
|
||||||
|
MCP unification is deferred because it requires changes to the managed process
|
||||||
|
model (currently 1 managed process per session) and has startup ordering
|
||||||
|
concerns (MCP servers start at boot, before any user query determines
|
||||||
|
a session_id).
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Box 系统测试覆盖分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 测试文件清单
|
||||||
|
|
||||||
|
### LangBot 仓库
|
||||||
|
|
||||||
|
| 文件 | 行数 | CI 运行 | 覆盖范围 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| `tests/unit_tests/box/test_box_connector.py` | 106 | 是 | Connector 传输决策、WS relay URL、dispose、心跳/重连 |
|
||||||
|
| `tests/unit_tests/box/test_box_service.py` | 1224 | 是 | Service 核心逻辑(最全面) |
|
||||||
|
| `tests/unit_tests/box/test_workspace.py` | 147 | 是 | WorkspaceSession 路径重写、payload 构建 |
|
||||||
|
| `tests/unit_tests/provider/test_mcp_box_integration.py` | 707 | 是 | MCP Box 配置、路径重写、payload、shared-session/multi-process、runtime info |
|
||||||
|
| `tests/unit_tests/provider/test_localagent_sandbox_exec.py` | 444 | 是 | LocalAgent exec 流程、流式、Skill 激活 (Tool Call) |
|
||||||
|
| `tests/unit_tests/provider/test_tool_manager_native.py` | 249 | 是 | ToolManager 路由、native tool CRUD、路径穿越、6 工具暴露 |
|
||||||
|
| `tests/unit_tests/provider/test_skill_tools.py` | 582 | 是 | Skill 管理、Tool Call 激活、路径、authoring CRUD |
|
||||||
|
| `tests/unit_tests/test_skill_service.py` | 396 | 是 | HTTP service:skill CRUD、zip/GitHub install、文件浏览 |
|
||||||
|
| `tests/unit_tests/test_paths.py` | 23 | 是 | paths 工具 |
|
||||||
|
| `tests/unit_tests/test_preproc.py` | 134 | 是 | PreProcessor 注入 session 变量、bound skill 解析 |
|
||||||
|
| `tests/unit_tests/pipeline/test_chat_handler_logging.py` | 78 | 是 | Chat handler 日志相关回归 |
|
||||||
|
| `tests/integration_tests/box/test_box_integration.py` | 329 | **否** | 真实容器执行、超时、网络隔离 |
|
||||||
|
| `tests/integration_tests/box/test_box_mcp_integration.py` | 368 | **否** | Managed process、WS attach、shared-session 清理 |
|
||||||
|
|
||||||
|
### SDK 仓库
|
||||||
|
|
||||||
|
| 文件 | 行数 | CI 运行 | 覆盖范围 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| `tests/box/test_backend_selection.py` | 255 | 是 | 显式 backend / local 模式探测顺序 / 配置变更触发 reselect |
|
||||||
|
| `tests/box/test_nsjail_backend.py` | 452 | 是 | nsjail 可用性、安装版 CLI vs 容器内 CLI、session、arg 构建、资源限制 |
|
||||||
|
| `tests/box/test_e2b_backend.py` | 482 | 是 | E2B SDK mock、session 生命周期、extra_mounts 同步 |
|
||||||
|
| `tests/box/test_skill_store.py` | 88 | 是 | zip preview/install、基础 file CRUD |
|
||||||
|
|
||||||
|
**总计**: 17 个测试文件, ~6,500 行测试代码; 其中 2 个集成测试(约 700 行)在 CI 中不运行。
|
||||||
|
|
||||||
|
> 较 2026-04-16 版增加:`test_skill_service.py`、`test_paths.py`、`test_preproc.py`、`test_chat_handler_logging.py` (LangBot),`test_backend_selection.py`、`test_e2b_backend.py`、`test_skill_store.py` (SDK)。`test_nsjail_backend.py` 增加 CLI 兼容性 case (commit `feed530`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 覆盖良好的区域
|
||||||
|
|
||||||
|
| 区域 | 质量 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| BoxRuntime session 管理 | 优秀 | session 复用、冲突检测、TTL 配置、消失 session 重建 |
|
||||||
|
| BoxService Profile 系统 | 优秀 | 4 个内置 Profile、locked/unlocked 字段、timeout clamp |
|
||||||
|
| BoxService host mount 安全 | 优秀 | allowed_mount_roots、disallowed_roots、shared host root |
|
||||||
|
| BoxService workspace quota | 优秀 | 前置/后置配额检查、超额清理 |
|
||||||
|
| BoxService 输出截断 | 优秀 | 短/精确边界/长输出、独立 stderr |
|
||||||
|
| BoxService 可观测性 | 优秀 | 状态报告、error ring buffer、buffer 上限 |
|
||||||
|
| BoxService session 模板 | 良好 | `resolve_box_session_id` + `build_skill_extra_mounts` 在 service / native / mcp 三处都有覆盖 |
|
||||||
|
| RPC client/server 协议 | 优秀 | execute/get_sessions/delete/create/conflict error |
|
||||||
|
| BoxRuntimeConnector | 良好 | local/remote 模式、Docker 平台、relay URL、心跳与重连回调 |
|
||||||
|
| BoxWorkspaceSession | 良好 | payload 构建、managed process 路径重写、stage host file |
|
||||||
|
| BoxHostMountMode.NONE | 良好 | 枚举校验、workdir 约束 |
|
||||||
|
| NsjailBackend | 良好 | 可用性、安装版 vs 容器内、session 生命周期、arg 构建、资源限制 |
|
||||||
|
| E2BBackend | 良好 | mock SDK、session/extra_mounts 同步 |
|
||||||
|
| Backend selection | 良好 | 显式 backend 优先级、local 探测顺序、配置变更触发 reselect |
|
||||||
|
| MCP Box 集成 | 良好 | config model、路径重写、payload、shared-session 多 process |
|
||||||
|
| Native tool loader | 良好 | 6 工具(exec/read/write/edit/glob/grep)、路径穿越拦截 |
|
||||||
|
| LocalAgent exec 流程 | 良好 | 完整 tool call 循环、流式、system prompt 注入、Tool Call 激活 |
|
||||||
|
| Skill 系统 | 良好 | 加载、Tool Call 激活、marker、路径解析、authoring CRUD、HTTP service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 覆盖缺失的区域
|
||||||
|
|
||||||
|
### 3.1 零测试 / 严重不足
|
||||||
|
|
||||||
|
| 区域 | 源文件 | 影响 |
|
||||||
|
|------|--------|------|
|
||||||
|
| **`security.py`** | SDK `box/security.py` (52 行) | `validate_sandbox_security()` 无任何测试。阻止 `/etc`/`/proc`/Docker socket 等危险挂载的安全函数从未被验证 |
|
||||||
|
| **`policy.py`** | `pkg/box/policy.py` (98 行) | 三层安全策略无测试(也是死代码) |
|
||||||
|
| **`skill_store.py` 边缘场景** | SDK `box/skill_store.py` (647 行) vs 测试 88 行 | GitHub 安装路径、`source_subdir` / `target_suffix` 组合、损坏 zip、文件冲突等场景未覆盖 |
|
||||||
|
|
||||||
|
### 3.2 未测试的关键路径
|
||||||
|
|
||||||
|
| 区域 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **Session TTL 过期** | 测试配置了 `session_ttl_sec` 但从未推进时间验证过期清理 |
|
||||||
|
| **并发 session 访问** | 无并发 exec / 并发创建 / race condition 测试 |
|
||||||
|
| **Container backend (Docker)** | 仅通过集成测试覆盖(CI 不运行),单元测试全用 FakeBackend |
|
||||||
|
| **E2B 真实 sandbox** | 单测全是 mock,未对接真实 E2B API |
|
||||||
|
| **BoxRuntime shutdown()** | 在 test cleanup 中调用但未验证行为 |
|
||||||
|
| **BoxServerHandler 错误路径** | 畸形请求、未知 action 类型 |
|
||||||
|
| **WS relay** | 仅在集成测试中覆盖(CI 不运行) |
|
||||||
|
| **NsjailBackend managed process** | 完全未测试 |
|
||||||
|
| **MCP stdio 完整生命周期** | 依赖安装 → 进程启动 → 健康检查 → 多 process 并发 → 重试 |
|
||||||
|
| **BoxService start/stop_managed_process** | 单 process 流转有单测,多 process 互不阻塞主要靠集成测试 |
|
||||||
|
| **重连指数退避** | connector 单测覆盖回调接线,未实际跑完整重连周期 |
|
||||||
|
|
||||||
|
### 3.3 边缘情况缺失
|
||||||
|
|
||||||
|
| 区域 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| BoxSpec 校验 | 无效 session_id 格式、超长命令、env 特殊字符 |
|
||||||
|
| BoxSpec.extra_mounts | 重复 mount_path、与 host_path 冲突、绝对 vs 相对路径 |
|
||||||
|
| BoxExecutionResult | 仅 COMPLETED 和 TIMED_OUT,无 ERROR 状态测试 |
|
||||||
|
| 多后端 fallback | local 模式探测顺序仅靠 mock,无真实 Docker 不可用 → nsjail 真机 fallback 测试 |
|
||||||
|
| Profile YAML 加载 | 测试用硬编码字符串,未从真实 config.yaml 加载 |
|
||||||
|
| INIT 配置变更触发 backend 重建 | 单测仅在初始化场景验证 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 集成测试 vs CI 的差距
|
||||||
|
|
||||||
|
CI 仅运行 `tests/unit_tests/`,以下场景**从未在自动化中验证**:
|
||||||
|
|
||||||
|
- 真实容器的创建/执行/销毁
|
||||||
|
- 容器网络隔离(`--network none`)
|
||||||
|
- 容器资源限制生效(cpus/memory/pids_limit)
|
||||||
|
- Managed process 的 WS 双向 I/O
|
||||||
|
- 多 process 同 session 并发 I/O
|
||||||
|
- 孤儿容器清理
|
||||||
|
- Session 删除清理容器
|
||||||
|
- 进程退出检测
|
||||||
|
- E2B 真实 sandbox 行为
|
||||||
|
|
||||||
|
**建议**: 在 CI 中加一个可选的 Docker-in-Docker 集成测试 stage,至少覆盖核心执行路径(exec / MCP attach / session 销毁)。
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# Box 系统 toB 商业化分析
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 现有优势
|
||||||
|
|
||||||
|
| 能力 | toB 价值 | 代码位置 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| **沙箱隔离执行** | 企业安全运行不受信代码的基础能力 | SDK `box/backend.py` |
|
||||||
|
| **多后端支持** | 适配不同企业容器基础设施 (Podman/Docker/nsjail/E2B) | SDK `box/runtime.py` `_select_backend()` |
|
||||||
|
| **E2B 云沙箱** | SaaS / 无 Docker 部署的兜底执行环境 | SDK `box/e2b_backend.py` |
|
||||||
|
| **连接自愈** | 心跳 + 自动重连,单点 Box runtime 故障可恢复 | `pkg/box/connector.py` `_heartbeat_loop`, `pkg/box/service.py` `_reconnect_loop` |
|
||||||
|
| **Profile + locked 字段** | 运维锁定安全边界,LLM/用户无法绕过 | `pkg/box/service.py`, SDK `box/models.py` |
|
||||||
|
| **资源限制** | CPU/内存/PID 数限制防止资源滥用 | SDK `backend.py` `--cpus/--memory/--pids-limit` |
|
||||||
|
| **Workspace quota** | 磁盘用量控制 | `pkg/box/service.py` `_enforce_workspace_quota` |
|
||||||
|
| **静默降级** | Box 不可用不影响其他功能,降低部署门槛 | `pkg/box/service.py:78` `_available=False` |
|
||||||
|
| **孤儿容器清理** | 防止泄漏的容器持续占用资源 | SDK `backend.py` `cleanup_orphaned_containers` |
|
||||||
|
| **网络隔离** | `--network none` 防止数据外泄 | SDK `backend.py` start_session |
|
||||||
|
| **只读根文件系统** | `--read-only` 防止容器被持久篡改 | SDK `backend.py` start_session |
|
||||||
|
| **Host path 白名单** | `allowed_host_mount_roots` 限制可挂载目录 | `pkg/box/service.py` `_validate_host_mount` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. toB 差距分析
|
||||||
|
|
||||||
|
### 2.1 安全与合规
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **WS relay 认证** | 无认证,任何人可 attach | 至少 token 认证 | **P0** |
|
||||||
|
| **安全策略** | policy.py 是死代码,实际无细粒度控制 | 工具级 allow/deny、沙箱模式控制 | **P0** |
|
||||||
|
| **审计日志** | 仅内存中 50 条 `_recent_errors` | 持久化审计:谁何时执行了什么、结果如何 | **P0** |
|
||||||
|
| **Host path 校验** | 黑名单策略,`/` 未拦截 | 白名单策略,默认拒绝 | **P1** |
|
||||||
|
| **数据驻留** | 无控制 | GDPR / 等保要求的数据隔离 | **P2** |
|
||||||
|
|
||||||
|
### 2.2 多租户
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **租户隔离** | 无租户概念 | BoxSpec/Profile 绑定 tenant_id | **P0** |
|
||||||
|
| **RBAC** | 仅 token 认证 | admin/operator/viewer 角色权限 | **P0** |
|
||||||
|
| **资源配额** | 单一 workspace quota | 每租户 CPU 时间/内存/并发/执行次数配额 | **P1** |
|
||||||
|
| **Session 隔离** | 所有 session 共享 dict | 按租户分区,互不可见 | **P1** |
|
||||||
|
|
||||||
|
### 2.3 可靠性
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **连接恢复** | 已实现:20s 心跳 + `_reconnect_loop` 指数退避 | 已满足基本要求 | 已有 |
|
||||||
|
| **Session 清理** | 机会性(仅新建时触发) | 定时清理 + 独立 reaper | **P1** |
|
||||||
|
| **水平扩展** | 单 Box Runtime 实例 | 多实例负载均衡(按 tenant 路由) | **P1** |
|
||||||
|
| **优雅降级** | 已有(_available=False) | 已满足基本要求 | 已有 |
|
||||||
|
| **Backend 自愈** | 已实现:`get_status` 时若 backend 不可用会重新选择 | 已满足基本要求 | 已有 |
|
||||||
|
|
||||||
|
### 2.4 可观测性
|
||||||
|
|
||||||
|
| 维度 | 现状 | toB 要求 | 优先级 |
|
||||||
|
|------|------|---------|--------|
|
||||||
|
| **监控指标** | 无 Prometheus metrics | session 数/执行延迟/资源用量/错误率 | **P1** |
|
||||||
|
| **结构化日志** | Python logging, 无结构化 | JSON 格式日志,含 trace_id/tenant_id | **P1** |
|
||||||
|
| **前端面板** | 监控页接入 `/api/v1/box/status`(backend 名 + 活跃 session 数);`sessions` / `errors` 仍未接入 | 完整状态面板 + 历史错误/审计列表 | **P2** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SaaS 部署架构建议
|
||||||
|
|
||||||
|
### 3.1 方案 A: 共享 Box Runtime Pool (快速上线)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot Instance ──> Box Runtime (共享)
|
||||||
|
├─ tenant_id 标签隔离
|
||||||
|
├─ Redis 配额计数器
|
||||||
|
└─ Container labels: langbot.tenant_id=xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 改动最小,加 tenant_id 到 BoxSpec/labels 即可
|
||||||
|
- **缺点**: 容器引擎共享,安全隔离弱
|
||||||
|
|
||||||
|
### 3.2 方案 B: 每租户 K8s Namespace + gVisor (推荐中期)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot ──> K8s API
|
||||||
|
├─ namespace: tenant-xxx
|
||||||
|
│ ├─ RuntimeClass: gVisor (runsc)
|
||||||
|
│ ├─ ResourceQuota
|
||||||
|
│ └─ NetworkPolicy
|
||||||
|
└─ namespace: tenant-yyy
|
||||||
|
└─ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 强隔离(namespace + gVisor),原生 K8s 配额
|
||||||
|
- **缺点**: 需要重写 backend 为 K8s Job,部署复杂度高
|
||||||
|
|
||||||
|
### 3.3 方案 C: K8s Job 直接编排 (长期)
|
||||||
|
|
||||||
|
```
|
||||||
|
LangBot ──> K8s Job per execution
|
||||||
|
├─ 每次执行创建 Job
|
||||||
|
├─ Pod Security Standards
|
||||||
|
├─ 自动调度和资源分配
|
||||||
|
└─ Job TTL Controller 自动清理
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 最强隔离,天然水平扩展
|
||||||
|
- **缺点**: 冷启动延迟,架构重写
|
||||||
|
|
||||||
|
**推荐演进路径**: A → B → C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配额体系建议
|
||||||
|
|
||||||
|
### 三层配额
|
||||||
|
|
||||||
|
| 层 | 实现 | 作用 |
|
||||||
|
|----|------|------|
|
||||||
|
| **内核层** | Docker `--cpus`/`--memory`/`--storage-opt` | 硬性资源上限,不可绕过 |
|
||||||
|
| **应用层** | Redis 原子计数器 | 并发 session 数/执行次数/CPU 时间预算 |
|
||||||
|
| **计费层** | 月度聚合 | 按租户计费(session-hours/execution-count) |
|
||||||
|
|
||||||
|
### Profile 与套餐映射
|
||||||
|
|
||||||
|
| 套餐 | Profile | locked 字段 | 配额 |
|
||||||
|
|------|---------|------------|------|
|
||||||
|
| Free | `offline_readonly` | network, host_path_mode, rootfs | 10 exec/天, 0.5 CPU, 256MB |
|
||||||
|
| Pro | `default` | (无) | 100 exec/天, 1 CPU, 512MB |
|
||||||
|
| Enterprise | `network_extended` | (按需) | 无限, 2 CPU, 1GB, 自定义镜像 |
|
||||||
|
|
||||||
|
### TOCTOU 配额修复
|
||||||
|
|
||||||
|
当前 `_enforce_workspace_quota` 的 TOCTOU 问题可通过两种方式解决:
|
||||||
|
|
||||||
|
1. **预留式配额** (应用层): Redis `INCRBY` 预扣额度 → 执行 → 成功则扣减,失败则回滚
|
||||||
|
2. **内核级限制** (Docker): `--storage-opt size=500m` 直接限制容器可写层大小
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 优先实施路线
|
||||||
|
|
||||||
|
### Phase 1 (2-4 周): 安全基线
|
||||||
|
|
||||||
|
- [ ] WS relay 加 token 认证
|
||||||
|
- [ ] 接入或删除 policy.py
|
||||||
|
- [x] ~~Box 加重连和心跳~~(已完成,见 [box-issues.md 已解决](./box-issues.md))
|
||||||
|
- [ ] 审计日志持久化(至少写文件/数据库)
|
||||||
|
- [ ] `security.py` 加 `/` 拦截,考虑白名单
|
||||||
|
- [ ] INIT 与 backend 初始化顺序整理(避免 backend 在配置到达前实例化)
|
||||||
|
|
||||||
|
### Phase 2 (4-8 周): 多租户基础
|
||||||
|
|
||||||
|
- [ ] BoxSpec 加 `tenant_id` 字段
|
||||||
|
- [ ] 容器 labels 加 tenant 标识
|
||||||
|
- [ ] Redis 配额计数器(并发/执行次数/时间)
|
||||||
|
- [ ] RBAC 基础框架
|
||||||
|
- [ ] 定时 session reaper
|
||||||
|
|
||||||
|
### Phase 3 (8-16 周): 生产就绪
|
||||||
|
|
||||||
|
- [ ] Prometheus metrics exporter
|
||||||
|
- [ ] 前端 Box 状态面板
|
||||||
|
- [ ] K8s backend 支持 (方案 B)
|
||||||
|
- [ ] 结构化日志 (JSON, trace_id)
|
||||||
|
- [ ] 水平扩展支持
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Box Runtime vs Plugin Runtime: 连接架构对比
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-02
|
||||||
|
> 状态更新: 自部署社区版已具备发布条件(box 可选、降级完善、无迁移欠债);工具调用循环上限、配额遍历异步化、`host_path` 挂载白名单等已落地。剩余多租户 / 安全硬化项见 [SaaS 阻塞项清单](./box-issues.md)。
|
||||||
|
> 分支: `feat/sandbox` (LangBot + langbot-plugin-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 总体差异
|
||||||
|
|
||||||
|
| 维度 | Plugin Runtime | Box Runtime |
|
||||||
|
|------|---------------|-------------|
|
||||||
|
| **继承关系** | `PluginRuntimeConnector(ManagedRuntimeConnector)` | `BoxRuntimeConnector`(独立类) |
|
||||||
|
| **传输分支** | 3 条 (Docker/WS, Win32/subprocess+WS, Unix/stdio) | 3 条 (本地 stdio, Win32/subprocess+WS, 远程 WS) |
|
||||||
|
| **心跳** | 20s ping loop | 20s ping loop(`_heartbeat_loop`) |
|
||||||
|
| **重连** | WS 模式: sleep 3s → re-initialize | 由 BoxService `_reconnect_loop` 处理,指数退避 |
|
||||||
|
| **Handler 类型** | `RuntimeConnectionHandler` (1132 行, 25+ action) | 基础 `Handler` + `BoxServerHandler`(SDK 端 25 action) |
|
||||||
|
| **Client 抽象** | Handler 即 API | 独立 `ActionRPCBoxClient` 封装 Handler |
|
||||||
|
| **启用/禁用** | `is_enable_plugin` 开关 | 无开关(可用/不可用由初始化结果决定) |
|
||||||
|
| **初始化失败** | 异常上抛 | 静默降级 `_available=False` |
|
||||||
|
| **Shutdown** | 直接杀进程 | RPC SHUTDOWN → 清理容器 → 再杀进程 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 传输决策
|
||||||
|
|
||||||
|
### Plugin: 3-路决策
|
||||||
|
|
||||||
|
```python
|
||||||
|
# pkg/plugin/connector.py:106-165
|
||||||
|
if get_platform() == 'docker' or use_websocket_to_connect_plugin_runtime():
|
||||||
|
# Docker/WS → ws://langbot_plugin_runtime:5400/control/ws
|
||||||
|
elif get_platform() == 'win32':
|
||||||
|
# Windows → 起子进程(无 pipe) + ws://localhost:5400/control/ws
|
||||||
|
else:
|
||||||
|
# Unix/Mac → StdioClientController(python -m langbot_plugin.cli rt -s)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Box: 3-路决策
|
||||||
|
|
||||||
|
```python
|
||||||
|
# pkg/box/connector.py
|
||||||
|
if self._uses_websocket():
|
||||||
|
if platform.get_platform() == 'win32' and not self.configured_runtime_url:
|
||||||
|
await self._start_subprocess_then_ws() # subprocess + ws://localhost:5410/rpc/ws
|
||||||
|
else:
|
||||||
|
await self._connect_remote_ws() # ws://{host}:5410/rpc/ws
|
||||||
|
else:
|
||||||
|
await self._start_local_stdio() # StdioClientController
|
||||||
|
```
|
||||||
|
|
||||||
|
> 历史:2026-04-16 版本本文档曾把 Box 描述为 2 路决策(缺 Windows 分支)。现已对齐 Plugin 的 3 路设计。
|
||||||
|
|
||||||
|
### 决策矩阵
|
||||||
|
|
||||||
|
| 环境 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Docker | WS → `:5400` | WS → `:5410/rpc/ws` |
|
||||||
|
| `--standalone-box` | N/A | WS → `localhost:5410/rpc/ws` |
|
||||||
|
| Windows 非 Docker | subprocess + WS (`:5400`) | subprocess + WS (`localhost:5410/rpc/ws`) |
|
||||||
|
| Unix/Mac 非 Docker | stdio | stdio |
|
||||||
|
| 手动配置 URL | 通过配置项 | WS → 用户配置的 URL |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 连接建立
|
||||||
|
|
||||||
|
### 同步模式差异
|
||||||
|
|
||||||
|
**Plugin**: `new_connection_callback` 内直接 ping + await handler_task,`initialize()` 通过 `create_task()` 异步启动,不阻塞等待连接。
|
||||||
|
|
||||||
|
**Box**: 使用 `asyncio.Event` + `wait_for(timeout=30s)` 模式,`initialize()` 同步等待连接成功或超时。
|
||||||
|
|
||||||
|
### Box stdio 路径
|
||||||
|
|
||||||
|
```
|
||||||
|
connector._start_local_stdio()
|
||||||
|
├─ connected = asyncio.Event()
|
||||||
|
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.cli.__init__', 'box', '-s', '--ws-control-port', N])
|
||||||
|
├─ _ctrl_task = create_task(ctrl.run(callback))
|
||||||
|
│ callback:
|
||||||
|
│ handler = Handler(connection) ← 基础 Handler, 无 disconnect_callback
|
||||||
|
│ client.set_handler(handler)
|
||||||
|
│ _handler_task = create_task(handler.run())
|
||||||
|
│ call_action(PING, {}) ← 握手, timeout=15s
|
||||||
|
│ connected.set() ← 通知外层
|
||||||
|
│ await _handler_task ← 阻塞直到断开
|
||||||
|
└─ await wait_for(connected.wait(), 30s) ← 同步等待
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin stdio 路径
|
||||||
|
|
||||||
|
```
|
||||||
|
connector.initialize()
|
||||||
|
├─ ctrl = StdioClientController(python, ['-m', 'langbot_plugin.cli', 'rt', '-s'])
|
||||||
|
├─ task = ctrl.run(callback)
|
||||||
|
│ callback:
|
||||||
|
│ disconnect_callback:
|
||||||
|
│ [WS] → runtime_disconnect_callback → 重连
|
||||||
|
│ [stdio] → 仅日志, 不重连
|
||||||
|
│ handler = RuntimeConnectionHandler(conn, disconnect_cb, ap)
|
||||||
|
│ create_task(handler.run())
|
||||||
|
│ handler.ping() ← 握手, timeout=10s
|
||||||
|
│ await handler_task ← 阻塞直到断开
|
||||||
|
├─ create_task(heartbeat_loop()) ← 20s ping loop
|
||||||
|
└─ create_task(task) ← 不等待连接
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 心跳与重连
|
||||||
|
|
||||||
|
### 心跳
|
||||||
|
|
||||||
|
| 维度 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| 有心跳? | 是 | 是(`connector.py` `_heartbeat_loop`) |
|
||||||
|
| 间隔 | 20s | 20s |
|
||||||
|
| 失败处理 | 仅 DEBUG 日志,不触发重连 | 仅 DEBUG 日志,依赖 connection close 触发重连 |
|
||||||
|
| 生命周期 | 整个应用生命周期 | 连接建立后启动;`dispose()` 时 cancel |
|
||||||
|
|
||||||
|
### 重连
|
||||||
|
|
||||||
|
| 维度 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Docker/WS 断开 | `runtime_disconnect_callback` → sleep 3s → re-initialize | `runtime_disconnect_callback` → `BoxService._reconnect_loop()`(指数退避) |
|
||||||
|
| WS 连接失败 | 同上 | 同上;初次失败时 `_available=False`,重连成功后恢复 |
|
||||||
|
| stdio 断开 | 仅日志,不重连 | 接同样回调;stdio 重连需重新 fork 子进程 |
|
||||||
|
| 重连退避 | 固定 3s,无 backoff | 指数退避 |
|
||||||
|
|
||||||
|
> 历史:2026-04-16 版本本文档曾把心跳与重连标记为 Box 缺失。这两项已在 commit `2dfd9d5d` / `c6882cf` / `5029d9c` 等修复(详见 [box-issues.md 已解决](./box-issues.md))。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 共享 IO 层
|
||||||
|
|
||||||
|
两者复用同一套 SDK IO 基础设施:
|
||||||
|
|
||||||
|
```
|
||||||
|
Handler ← ABC (runtime/io/handler.py)
|
||||||
|
├── RuntimeConnectionHandler (Plugin 用, LangBot 侧)
|
||||||
|
├── ControlConnectionHandler (Plugin 用, SDK 侧)
|
||||||
|
├── BoxServerHandler (Box 用, SDK 侧)
|
||||||
|
└── 匿名 Handler 实例 (Box 用, LangBot 侧)
|
||||||
|
|
||||||
|
Connection ← ABC
|
||||||
|
├── StdioConnection (stdio: 16KB chunks, 应用层分帧协议)
|
||||||
|
└── WebSocketConnection (WS: 64KB chunks, 原生 WS 分帧)
|
||||||
|
|
||||||
|
Controller ← ABC
|
||||||
|
├── StdioClientController (fork 子进程, pipe stdin/stdout)
|
||||||
|
├── StdioServerController (接管当前进程 stdin/stdout)
|
||||||
|
├── WebSocketClientController (连接 WS 服务端)
|
||||||
|
└── WebSocketServerController (监听 WS 端口)
|
||||||
|
```
|
||||||
|
|
||||||
|
共享的核心机制:
|
||||||
|
- `call_action()` / `call_action_generator()` — RPC 调用/流式调用
|
||||||
|
- `ActionRequest` / `ActionResponse` — 请求/响应协议
|
||||||
|
- `seq_id` 关联 — 并发请求复用单连接
|
||||||
|
- `CommonAction.PING` — 两者都用于初始握手
|
||||||
|
- 文件传输 (`send_file`) — Plugin 用,Box 不用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 端口方案
|
||||||
|
|
||||||
|
| 服务 | Plugin | Box |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Action RPC (stdio) | stdin/stdout | stdin/stdout |
|
||||||
|
| Action RPC (WS) | `:5400` | `:5410/rpc/ws` |
|
||||||
|
| 辅助服务 | debug WS `:5401` | managed process WS relay `:5410/v1/sessions/{id}/managed-process/ws` |
|
||||||
|
|
||||||
|
**Box 特点**: 单端口 aiohttp 服务(默认 5410),通过路径区分 Action RPC 和 managed process relay。即使在 stdio 模式,也在 `:5410` 启动 aiohttp 用于 managed process attach。Plugin 在 stdio 模式不开额外端口。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 销毁对比
|
||||||
|
|
||||||
|
### Plugin
|
||||||
|
|
||||||
|
```python
|
||||||
|
dispose():
|
||||||
|
if stdio: ctrl.process.terminate()
|
||||||
|
_dispose_subprocess() # Windows 子进程
|
||||||
|
heartbeat_task.cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Box
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector.dispose():
|
||||||
|
_handler_task.cancel()
|
||||||
|
_ctrl_task.cancel()
|
||||||
|
_subprocess.terminate()
|
||||||
|
|
||||||
|
service.dispose():
|
||||||
|
connector.dispose()
|
||||||
|
loop.create_task(client.shutdown()) # RPC SHUTDOWN → 清理所有容器
|
||||||
|
```
|
||||||
|
|
||||||
|
Box 的 RPC SHUTDOWN 确保容器被正确停止,不会成为孤儿。Plugin 直接杀进程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 改进建议
|
||||||
|
|
||||||
|
### P0
|
||||||
|
|
||||||
|
1. **两者都加 WS 认证**: 至少 token 认证(INIT 时下发,连接时校验)
|
||||||
|
|
||||||
|
### P1
|
||||||
|
|
||||||
|
2. **考虑 Box 继承 ManagedRuntimeConnector**: 复用 `_start_runtime_subprocess` / `_wait_until_ready` / `_dispose_subprocess`,减少重复代码
|
||||||
|
3. **Plugin 重连加退避**: 固定 3s 无 backoff 可能造成日志洪水,建议向 Box 的指数退避看齐
|
||||||
|
4. **统一连接管理模式**: Event-based (Box) vs direct-await (Plugin),考虑收敛为一种
|
||||||
|
|
||||||
|
### 已完成(自上一轮)
|
||||||
|
|
||||||
|
- ~~Box 加重连~~(commit `2dfd9d5d`)
|
||||||
|
- ~~Box 加心跳~~(20s loop 与 Plugin 一致)
|
||||||
|
- ~~Box 加 Windows 支持~~(commit `120817a` / `fafb7a4`)
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# MCP Resources PR #2215 Review
|
||||||
|
|
||||||
|
> 更新日期: 2026-06-29
|
||||||
|
> 分支: `mcp_resources`
|
||||||
|
> PR: langbot-app/LangBot#2215
|
||||||
|
> 主题: MCP Resources 在 LangBot 中的产品价值、AgentRunner 集成方式与后续架构方向
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
PR #2215 对 LangBot 有明确价值:它补齐了 MCP 协议中 Resources 这一重要能力,让 MCP server 不再只暴露 tools,也可以暴露文档、代码片段、配置、日志、图片等上下文资源。管理端可以发现和预览资源,Agent 也可以通过当前实现按需列出和读取资源。
|
||||||
|
|
||||||
|
但当前 AgentRunner 层的接入方式更接近一个可用的第一阶段方案,而不是最终架构。现在 MCP Resources 被包装成两个 synthetic tools:
|
||||||
|
|
||||||
|
- `langbot_mcp_list_resources`
|
||||||
|
- `langbot_mcp_read_resource`
|
||||||
|
|
||||||
|
这让模型可以通过 function calling 主动探索资源,落地成本低,也复用了已有 `ToolManager` / `LocalAgentRunner` 的工具调用链路。不过从 MCP 规范和主流实现来看,Resources 更适合作为一种一等上下文来源,而不是长期隐藏在工具列表里。
|
||||||
|
|
||||||
|
建议保留当前 synthetic tools 作为探索能力,同时把后续主线设计调整为:MCP Resources 是 pipeline / conversation / message 级别可选择、可固定、可审计的上下文输入。
|
||||||
|
|
||||||
|
## 当前实现判断
|
||||||
|
|
||||||
|
当前 AgentRunner 集成路径如下:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pipeline 绑定 MCP server
|
||||||
|
-> query.variables['_pipeline_bound_mcp_servers']
|
||||||
|
-> Preproc 为 local-agent 加载工具
|
||||||
|
-> ToolManager.get_all_tools()
|
||||||
|
-> MCPLoader 注入 synthetic resource tools
|
||||||
|
-> LocalAgentRunner 将工具 schema 传给模型
|
||||||
|
-> 模型发起 list/read tool call
|
||||||
|
-> ToolManager.execute_func_call()
|
||||||
|
-> MCPLoader 调 MCP session.list_resources/read_resource
|
||||||
|
-> tool result 回灌给模型
|
||||||
|
```
|
||||||
|
|
||||||
|
这个路径的优点是:
|
||||||
|
|
||||||
|
- 复用现有工具调用机制,改动范围小。
|
||||||
|
- Agent 可以按需探索资源,不需要每轮预先读取所有资源。
|
||||||
|
- 可以沿用 pipeline 绑定的 MCP server 范围,避免越权读取未绑定 server。
|
||||||
|
- 对已有 MCP tools 行为影响较小。
|
||||||
|
|
||||||
|
主要问题是:
|
||||||
|
|
||||||
|
- Resources 在语义上被降级成 tools,和 MCP 规范里的 resource primitive 不完全一致。
|
||||||
|
- 模型必须先理解并主动调用 `list/read`,资源不会自然成为上下文。
|
||||||
|
- pipeline 不能配置“默认携带某些资源”或“本轮附加某些资源”。
|
||||||
|
- UI 资源 tab 目前是管理端预览能力,和 Agent 上下文选择没有打通。
|
||||||
|
- 对 blob、图片、大文件、结构化资源的处理还比较粗糙。
|
||||||
|
- 缺少 resource templates、订阅更新、缓存、chunk、token budget、trace 与审计策略。
|
||||||
|
|
||||||
|
## 主流项目做法
|
||||||
|
|
||||||
|
### MCP 官方规范
|
||||||
|
|
||||||
|
MCP Resources 是 server 暴露上下文数据的协议能力。规范没有要求 resources 必须以 tool call 形式给模型使用,而是把如何选择、过滤、读取和纳入上下文交给 Host application。
|
||||||
|
|
||||||
|
这意味着比较正统的集成方式是:LangBot 作为 Host,在 pipeline、会话或消息层决定哪些 resources 进入模型上下文。
|
||||||
|
|
||||||
|
参考: https://modelcontextprotocol.io/specification/2025-06-18/server/resources
|
||||||
|
|
||||||
|
### VS Code Copilot
|
||||||
|
|
||||||
|
VS Code 把 MCP Resources 做成 chat context 的一部分。用户可以通过 `Add Context > MCP Resources` 或命令浏览 MCP resources,并把选中的资源附加到一次 chat request。
|
||||||
|
|
||||||
|
这是目前最值得 LangBot 参考的产品形态:资源不是模型工具,而是用户和 Host 可控的上下文附件。
|
||||||
|
|
||||||
|
参考: https://code.visualstudio.com/docs/agent-customization/mcp-servers
|
||||||
|
|
||||||
|
### Anthropic SDK
|
||||||
|
|
||||||
|
Anthropic 的 client-side MCP helpers 提供资源读取和转换能力,例如把 MCP resource 转为 Claude message content 或 file。也就是说,应用先读取 resource,再显式放进模型消息。
|
||||||
|
|
||||||
|
这同样是 application-owned context injection,而不是把 resource 伪装成模型工具。
|
||||||
|
|
||||||
|
参考: https://platform.claude.com/docs/en/agents-and-tools/mcp-connector
|
||||||
|
|
||||||
|
### LangChain MCP Adapters
|
||||||
|
|
||||||
|
LangChain 把 MCP Resources 更像 data loader / document input 来处理,可以把资源加载成 `Blob`,再进入 LangChain 的文档、检索或上下文处理链路。
|
||||||
|
|
||||||
|
这说明 Resources 很适合作为知识源、文档源或上下文源,而不只是即时工具调用。
|
||||||
|
|
||||||
|
参考: https://docs.langchain.com/oss/python/langchain/mcp
|
||||||
|
|
||||||
|
### OpenAI Agents SDK
|
||||||
|
|
||||||
|
OpenAI Agents SDK 主路径仍偏向 MCP tools,但底层 MCP server API 已经有 `list_resources`、`list_resource_templates`、`read_resource` 等能力。当前形态说明 resources 是 client 能力,但并未默认变成 agent-visible tools。
|
||||||
|
|
||||||
|
参考: https://openai.github.io/openai-agents-python/mcp/
|
||||||
|
|
||||||
|
### Cline
|
||||||
|
|
||||||
|
Cline 会拉取 MCP tools、resources、resourceTemplates、prompts,并通过类似 `access_mcp_resource` 的内置访问方式让模型读取资源。这个方向和 LangBot 当前 synthetic tools 比较接近。
|
||||||
|
|
||||||
|
这种模式适合让 Agent 自主探索,但更像 Host 自定义的模型访问协议,不应成为唯一集成路径。
|
||||||
|
|
||||||
|
参考: https://github.com/cline/cline/blob/main/src/services/mcp/McpHub.ts
|
||||||
|
|
||||||
|
## 建议架构方向
|
||||||
|
|
||||||
|
### 1. 保留探索型工具
|
||||||
|
|
||||||
|
保留当前两个 synthetic tools:
|
||||||
|
|
||||||
|
- `langbot_mcp_list_resources`
|
||||||
|
- `langbot_mcp_read_resource`
|
||||||
|
|
||||||
|
它们适合处理“用户没有显式选择资源,但 Agent 判断需要探索 MCP server 上下文”的场景。后续可以优化工具描述、返回格式、资源大小限制和错误信息。
|
||||||
|
|
||||||
|
### 2. 增加一等 Resource Context
|
||||||
|
|
||||||
|
新增一个 Host 层资源上下文概念,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PipelineResourceBinding
|
||||||
|
ConversationResourceAttachment
|
||||||
|
MessageResourceAttachment
|
||||||
|
```
|
||||||
|
|
||||||
|
Preproc 或独立的 `ResourceContextProvider` 在模型调用前读取这些资源,按 MIME 类型、大小、token budget 转为模型可消费的上下文。
|
||||||
|
|
||||||
|
### 3. 打通 UI 与 Agent 上下文
|
||||||
|
|
||||||
|
当前 MCP 详情页的 Resources tab 可以继续作为资源发现和预览入口。建议增加操作:
|
||||||
|
|
||||||
|
- 添加到本轮上下文
|
||||||
|
- 固定到当前 pipeline
|
||||||
|
- 固定到当前 bot / conversation
|
||||||
|
- 查看资源读取历史和错误
|
||||||
|
|
||||||
|
这样 UI 资源管理能力才能真正影响 Agent 行为。
|
||||||
|
|
||||||
|
### 4. 支持 resource templates
|
||||||
|
|
||||||
|
MCP resource templates 允许 server 暴露参数化资源,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
repo://{owner}/{repo}/file/{path}
|
||||||
|
log://{service}/{date}
|
||||||
|
```
|
||||||
|
|
||||||
|
LangBot 后续应支持模板发现、参数填写、实例化和绑定。否则只能使用静态 resources,覆盖面会受限。
|
||||||
|
|
||||||
|
### 5. 增加资源处理策略
|
||||||
|
|
||||||
|
建议补齐:
|
||||||
|
|
||||||
|
- 文本资源 token budget 与截断策略。
|
||||||
|
- 大文件 chunk 与摘要策略。
|
||||||
|
- 图片/blob 的模型能力判断与 fallback。
|
||||||
|
- MIME 类型白名单与安全限制。
|
||||||
|
- 缓存与过期策略。
|
||||||
|
- `resources/listChanged` 或订阅更新。
|
||||||
|
- resource read trace,便于审计 Agent 读取了什么上下文。
|
||||||
|
|
||||||
|
## 推荐落地顺序
|
||||||
|
|
||||||
|
### Phase 1: 完成当前 PR 可用性
|
||||||
|
|
||||||
|
- 保留 synthetic tools。
|
||||||
|
- 明确文档说明当前 Agent 集成是 tool-mediated。
|
||||||
|
- 完善资源工具描述,降低模型误用概率。
|
||||||
|
- 给 read/list 增加大小限制和更清晰的 MIME 处理。
|
||||||
|
- 前端 Resources tab 与 Tools tab 分离,保持管理端清晰。
|
||||||
|
|
||||||
|
### Phase 2: 做 Host-owned context attachments
|
||||||
|
|
||||||
|
- 在 pipeline 或 conversation 层新增 resource attachment 配置。
|
||||||
|
- Preproc 读取已绑定 resources,注入模型上下文。
|
||||||
|
- UI 支持“添加到上下文 / 固定到 pipeline”。
|
||||||
|
- 记录每轮实际注入的 resource URI 和 token 消耗。
|
||||||
|
|
||||||
|
### Phase 3: 做完整 MCP Resources 能力
|
||||||
|
|
||||||
|
- 支持 resource templates。
|
||||||
|
- 支持资源订阅更新。
|
||||||
|
- 支持 chunk、summary、RAG 化接入。
|
||||||
|
- 为 DifyAgentRunner、LocalAgentRunner 等不同 runner 定义统一资源上下文接口。
|
||||||
|
|
||||||
|
## 最终建议
|
||||||
|
|
||||||
|
PR #2215 可以作为 MCP Resources 的第一阶段实现继续推进。它让 LangBot 快速拥有“资源发现、预览、按需读取”的闭环,也给 Agent 探索资源提供了可运行路径。
|
||||||
|
|
||||||
|
但在正式设计上,不建议把 “Resources == Tools” 固化为长期抽象。LangBot 更应该把 MCP Resources 定位为上下文来源,与 tools、prompts、knowledge base 并列:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tools -> Agent 可以执行的动作
|
||||||
|
Resources -> Host/用户/Agent 可以选择的上下文数据
|
||||||
|
Prompts -> 可复用的任务模板
|
||||||
|
Knowledge -> 可检索、可索引的长期知识
|
||||||
|
```
|
||||||
|
|
||||||
|
这样既尊重 MCP 协议语义,也能让 LangBot 在 Agent 工作流、企业知识接入和多 MCP server 管理上走得更稳。
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"url": "https://langbot.app"
|
"url": "https://langbot.app"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "AGPL-3.0",
|
"name": "Apache-2.0",
|
||||||
"url": "https://github.com/langbot-app/LangBot/blob/master/LICENSE"
|
"url": "https://github.com/langbot-app/LangBot/blob/master/LICENSE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
# Workflow 用户指南
|
||||||
|
|
||||||
|
本文档帮助您了解和使用 LangBot 的 Workflow(工作流)功能,通过可视化方式构建自动化的对话处理流程。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [功能介绍](#功能介绍)
|
||||||
|
- [快速入门](#快速入门)
|
||||||
|
- [节点类型说明](#节点类型说明)
|
||||||
|
- [编辑器使用指南](#编辑器使用指南)
|
||||||
|
- [调试功能](#调试功能)
|
||||||
|
- [常见问题解答](#常见问题解答)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
### 什么是 Workflow?
|
||||||
|
|
||||||
|
Workflow(工作流)是 LangBot 提供的可视化自动化编排系统。通过拖拽节点、连接边的方式,您可以:
|
||||||
|
|
||||||
|
- 📝 **构建复杂的对话流程**:使用条件分支、循环等控制节点
|
||||||
|
- 🤖 **调用 AI 能力**:集成 LLM、知识库检索、参数提取
|
||||||
|
- 🔗 **连接外部服务**:集成 Dify、n8n、Coze 等平台
|
||||||
|
- ⚡ **自动化任务执行**:消息触发、定时触发、Webhook 触发
|
||||||
|
|
||||||
|
### Workflow vs Pipeline
|
||||||
|
|
||||||
|
| 对比项 | Pipeline | Workflow |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| 配置方式 | 表单配置 | 可视化拖拽 |
|
||||||
|
| 流程控制 | 线性执行 | 支持分支、循环、并行 |
|
||||||
|
| 适用场景 | 简单对话 | 复杂流程 |
|
||||||
|
| 学习曲线 | 低 | 中等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速入门
|
||||||
|
|
||||||
|
### 第一步:创建 Workflow
|
||||||
|
|
||||||
|
1. 在侧边栏点击 **Workflow** 进入工作流列表
|
||||||
|
2. 点击右上角 **创建工作流** 按钮
|
||||||
|
3. 填写基本信息:
|
||||||
|
- **名称**:给工作流起一个描述性的名字
|
||||||
|
- **描述**:可选,说明工作流的用途
|
||||||
|
- **图标**:选择一个 emoji 作为标识
|
||||||
|
|
||||||
|
### 第二步:添加节点
|
||||||
|
|
||||||
|
进入编辑器后,左侧是节点面板,中间是画布区域,右侧是属性面板。
|
||||||
|
|
||||||
|
1. **添加触发节点**:从左侧面板拖拽一个"消息触发"节点到画布
|
||||||
|
2. **添加 AI 节点**:拖拽一个"LLM 调用"节点
|
||||||
|
3. **添加回复节点**:拖拽一个"回复消息"节点
|
||||||
|
|
||||||
|
### 第三步:连接节点
|
||||||
|
|
||||||
|
1. 将鼠标悬停在触发节点的输出端口(右侧小圆点)
|
||||||
|
2. 按住鼠标拖拽到 LLM 节点的输入端口(左侧小圆点)
|
||||||
|
3. 同样方式连接 LLM 节点和回复节点
|
||||||
|
|
||||||
|
```
|
||||||
|
[消息触发] ──▶ [LLM 调用] ──▶ [回复消息]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第四步:配置节点
|
||||||
|
|
||||||
|
点击 LLM 调用节点,在右侧属性面板配置:
|
||||||
|
|
||||||
|
- **运行方式**:选择"本地 Agent"
|
||||||
|
- **系统提示词**:描述 AI 的角色和行为
|
||||||
|
- **模型**:选择要使用的 LLM 模型
|
||||||
|
|
||||||
|
点击回复消息节点配置:
|
||||||
|
|
||||||
|
- **消息内容**:设置为 `{{nodes.llm_call.outputs.response}}`(引用 LLM 输出)
|
||||||
|
|
||||||
|
### 第五步:保存并绑定
|
||||||
|
|
||||||
|
1. 点击工具栏的 **保存** 按钮
|
||||||
|
2. 返回 Bot 配置页面
|
||||||
|
3. 在 Bot 的绑定设置中选择 **Workflow**,然后选择刚创建的工作流
|
||||||
|
|
||||||
|
恭喜!您已经创建了第一个 Workflow。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 节点类型说明
|
||||||
|
|
||||||
|
### 触发节点 (Trigger)
|
||||||
|
|
||||||
|
触发节点是工作流的入口,定义何时启动执行。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 输出 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 消息触发 | 收到消息时触发 | message, sender_id, platform |
|
||||||
|
| 定时触发 | 按 Cron 表达式定时触发 | timestamp |
|
||||||
|
| Webhook 触发 | 收到 HTTP 请求时触发 | request_body, headers |
|
||||||
|
| 事件触发 | 系统事件触发 | event_type, event_data |
|
||||||
|
|
||||||
|
**消息触发配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
触发条件:
|
||||||
|
- 关键词匹配: ["帮助", "help"]
|
||||||
|
- 平台: ["wechat", "qq"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI 节点
|
||||||
|
|
||||||
|
AI 节点用于调用各种 AI 能力。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 典型用途 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| LLM 调用 | 调用大语言模型 | 生成回复、理解意图 |
|
||||||
|
| 问题分类器 | 对用户问题分类 | 路由到不同处理分支 |
|
||||||
|
| 参数提取器 | 从文本提取结构化数据 | 提取订单号、日期等 |
|
||||||
|
| 知识库检索 | 查询知识库 | RAG 增强回复 |
|
||||||
|
|
||||||
|
**LLM 调用配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
运行方式: 本地 Agent
|
||||||
|
模型: gpt-4
|
||||||
|
系统提示词: |
|
||||||
|
你是一个友好的客服助手。
|
||||||
|
请根据用户的问题提供帮助。
|
||||||
|
温度: 0.7
|
||||||
|
最大 Token 数: 2000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 处理节点 (Process)
|
||||||
|
|
||||||
|
处理节点用于数据处理和外部调用。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 典型用途 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| 代码执行 | 执行 Python/JavaScript 代码 | 数据处理、格式转换 |
|
||||||
|
| HTTP 请求 | 发送 HTTP 请求 | 调用外部 API |
|
||||||
|
| 数据转换 | JSON/模板转换 | 数据格式化 |
|
||||||
|
|
||||||
|
**HTTP 请求配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
URL: https://api.example.com/data
|
||||||
|
方法: POST
|
||||||
|
请求头:
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: Bearer {{variables.api_key}}
|
||||||
|
请求体: |
|
||||||
|
{"query": "{{message.content}}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 控制节点 (Control)
|
||||||
|
|
||||||
|
控制节点用于流程控制。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 用途 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 条件分支 | 二选一分支 | if-else 逻辑 |
|
||||||
|
| 多路分支 | 多选一分支 | switch-case 逻辑 |
|
||||||
|
| 循环 | 遍历数组 | 批量处理 |
|
||||||
|
| 并行 | 同时执行多分支 | 并发处理 |
|
||||||
|
| 等待 | 暂停执行 | 延时处理 |
|
||||||
|
| 合并 | 合并多个分支 | 汇总结果 |
|
||||||
|
|
||||||
|
**条件分支配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
条件表达式: "{{nodes.classifier.outputs.category}}" == "complaint"
|
||||||
|
真分支: 投诉处理
|
||||||
|
假分支: 普通咨询
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动作节点 (Action)
|
||||||
|
|
||||||
|
动作节点执行具体操作。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 用途 |
|
||||||
|
|-----|------|------|
|
||||||
|
| 发送消息 | 主动发送消息 | 通知、推送 |
|
||||||
|
| 回复消息 | 回复当前消息 | 对话回复 |
|
||||||
|
| 存储数据 | 保存数据到存储 | 持久化 |
|
||||||
|
| 调用 Pipeline | 调用现有 Pipeline | 复用现有流程 |
|
||||||
|
|
||||||
|
**回复消息配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
消息内容: |
|
||||||
|
感谢您的咨询!
|
||||||
|
|
||||||
|
{{nodes.llm_call.outputs.response}}
|
||||||
|
|
||||||
|
如有其他问题,随时联系我。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 集成节点 (Integration)
|
||||||
|
|
||||||
|
集成节点连接外部平台。
|
||||||
|
|
||||||
|
| 节点 | 说明 | 平台 |
|
||||||
|
|-----|------|------|
|
||||||
|
| Dify 工作流 | 调用 Dify 应用 | Dify |
|
||||||
|
| Dify 知识库 | 查询 Dify 知识库 | Dify |
|
||||||
|
| n8n 工作流 | 调用 n8n 流程 | n8n |
|
||||||
|
| Langflow | 调用 Langflow 流程 | Langflow |
|
||||||
|
| Coze Bot | 调用扣子 Bot | Coze |
|
||||||
|
|
||||||
|
**Dify 工作流配置示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
API 地址: https://api.dify.ai/v1
|
||||||
|
API Key: sk-xxxxx
|
||||||
|
应用类型: workflow
|
||||||
|
同步对话历史: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 编辑器使用指南
|
||||||
|
|
||||||
|
### 画布操作
|
||||||
|
|
||||||
|
| 操作 | 方式 |
|
||||||
|
|-----|------|
|
||||||
|
| 平移画布 | 按住鼠标中键/空格+左键 拖拽 |
|
||||||
|
| 缩放画布 | 鼠标滚轮 / 工具栏按钮 |
|
||||||
|
| 框选多个节点 | 按住 Shift + 拖拽框选 |
|
||||||
|
| 适应视图 | 点击工具栏"适应"按钮 |
|
||||||
|
|
||||||
|
### 节点操作
|
||||||
|
|
||||||
|
| 操作 | 方式 |
|
||||||
|
|-----|------|
|
||||||
|
| 添加节点 | 从左侧面板拖拽到画布 |
|
||||||
|
| 移动节点 | 点击节点拖拽 |
|
||||||
|
| 删除节点 | 选中后按 Delete / 点击工具栏删除 |
|
||||||
|
| 复制节点 | 选中后 Ctrl+C / 工具栏复制 |
|
||||||
|
| 粘贴节点 | Ctrl+V / 工具栏粘贴 |
|
||||||
|
|
||||||
|
### 连接操作
|
||||||
|
|
||||||
|
| 操作 | 方式 |
|
||||||
|
|-----|------|
|
||||||
|
| 创建连接 | 从输出端口拖拽到输入端口 |
|
||||||
|
| 删除连接 | 点击连接线后按 Delete |
|
||||||
|
| 选中连接 | 点击连接线 |
|
||||||
|
|
||||||
|
### 快捷键
|
||||||
|
|
||||||
|
| 快捷键 | 功能 |
|
||||||
|
|-------|------|
|
||||||
|
| Ctrl + Z | 撤销 |
|
||||||
|
| Ctrl + Shift + Z | 重做 |
|
||||||
|
| Ctrl + C | 复制 |
|
||||||
|
| Ctrl + V | 粘贴 |
|
||||||
|
| Delete | 删除选中 |
|
||||||
|
| Ctrl + S | 保存 |
|
||||||
|
|
||||||
|
### 工具栏功能
|
||||||
|
|
||||||
|
```
|
||||||
|
[撤销] [重做] | [放大] [缩小] [适应] | [复制] [粘贴] [删除] | [保存] [调试]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调试功能
|
||||||
|
|
||||||
|
### 启动调试
|
||||||
|
|
||||||
|
1. 点击工具栏的 **调试** 按钮
|
||||||
|
2. 在调试面板中配置初始数据:
|
||||||
|
- **输入消息**:模拟用户发送的消息
|
||||||
|
- **会话 ID**:可选,用于测试会话变量
|
||||||
|
- **变量**:设置初始变量值
|
||||||
|
|
||||||
|
3. 点击 **开始调试** 按钮
|
||||||
|
|
||||||
|
### 调试控制
|
||||||
|
|
||||||
|
| 按钮 | 功能 |
|
||||||
|
|-----|------|
|
||||||
|
| ▶️ 开始/继续 | 开始或继续执行 |
|
||||||
|
| ⏸️ 暂停 | 暂停执行 |
|
||||||
|
| ⏹️ 停止 | 停止执行 |
|
||||||
|
| ⏭️ 单步 | 执行下一个节点 |
|
||||||
|
|
||||||
|
### 断点
|
||||||
|
|
||||||
|
- **设置断点**:点击节点上的断点图标
|
||||||
|
- **断点触发**:执行到断点时自动暂停
|
||||||
|
- **查看状态**:在暂停时查看节点的输入输出
|
||||||
|
|
||||||
|
### 执行日志
|
||||||
|
|
||||||
|
调试面板下方显示实时日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INFO] 2024-01-15 10:30:00 - Starting debug execution
|
||||||
|
[INFO] 2024-01-15 10:30:00 - Executing node: message_trigger
|
||||||
|
[DEBUG] 2024-01-15 10:30:00 - Node inputs: {"message": "你好"}
|
||||||
|
[INFO] 2024-01-15 10:30:01 - Node completed in 50ms
|
||||||
|
[INFO] 2024-01-15 10:30:01 - Executing node: llm_call
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 节点状态颜色
|
||||||
|
|
||||||
|
| 颜色 | 状态 |
|
||||||
|
|-----|------|
|
||||||
|
| 灰色 | 待执行 |
|
||||||
|
| 蓝色 | 执行中 |
|
||||||
|
| 绿色 | 已完成 |
|
||||||
|
| 红色 | 失败 |
|
||||||
|
| 黄色 | 已跳过 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题解答
|
||||||
|
|
||||||
|
### Q1:如何在节点间传递数据?
|
||||||
|
|
||||||
|
使用表达式语法引用其他节点的输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
{{nodes.节点ID.outputs.输出名称}}
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- `{{nodes.llm_call.outputs.response}}` - 引用 LLM 节点的响应
|
||||||
|
- `{{nodes.http_request.outputs.body}}` - 引用 HTTP 请求的响应体
|
||||||
|
|
||||||
|
### Q2:如何使用变量?
|
||||||
|
|
||||||
|
Workflow 支持三种变量类型:
|
||||||
|
|
||||||
|
1. **工作流变量**:`{{variables.变量名}}`
|
||||||
|
2. **会话变量**:`{{conversation_variables.变量名}}`
|
||||||
|
3. **消息上下文**:`{{message.content}}`、`{{message.sender_id}}`
|
||||||
|
|
||||||
|
### Q3:条件分支如何写条件表达式?
|
||||||
|
|
||||||
|
支持以下运算符:
|
||||||
|
|
||||||
|
- 比较:`==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||||
|
- 逻辑:`and`, `or`, `not`
|
||||||
|
- 包含:`in`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```python
|
||||||
|
# 字符串比较
|
||||||
|
"{{nodes.classifier.outputs.intent}}" == "purchase"
|
||||||
|
|
||||||
|
# 数值比较
|
||||||
|
{{nodes.extractor.outputs.amount}} > 1000
|
||||||
|
|
||||||
|
# 包含检查
|
||||||
|
"退款" in "{{message.content}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q4:如何处理错误?
|
||||||
|
|
||||||
|
1. **节点级重试**:在节点配置中设置重试次数
|
||||||
|
2. **全局错误处理**:在 Workflow 设置中配置错误处理策略
|
||||||
|
3. **条件分支**:使用条件节点检查上一节点的状态
|
||||||
|
|
||||||
|
### Q5:如何查看执行历史?
|
||||||
|
|
||||||
|
1. 进入 Workflow 详情页
|
||||||
|
2. 点击 **执行历史** 标签
|
||||||
|
3. 查看每次执行的状态、耗时、输入输出
|
||||||
|
|
||||||
|
### Q6:Workflow 可以被多个 Bot 使用吗?
|
||||||
|
|
||||||
|
是的。一个 Workflow 可以被多个 Bot 绑定使用,但每个 Bot 只能绑定一个处理单元(Pipeline 或 Workflow)。
|
||||||
|
|
||||||
|
### Q7:如何复制现有的 Workflow?
|
||||||
|
|
||||||
|
在 Workflow 列表页,点击工作流卡片右上角的菜单,选择"复制"即可创建副本。
|
||||||
|
|
||||||
|
### Q8:支持版本回滚吗?
|
||||||
|
|
||||||
|
支持。每次保存都会创建新版本。在 Workflow 详情页可以查看版本历史并回滚到指定版本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 合理命名
|
||||||
|
|
||||||
|
- 为节点和 Workflow 使用描述性名称
|
||||||
|
- 使用统一的命名规范
|
||||||
|
|
||||||
|
### 2. 模块化设计
|
||||||
|
|
||||||
|
- 将复杂流程拆分为多个小 Workflow
|
||||||
|
- 使用"调用 Pipeline"节点复用现有流程
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
|
||||||
|
- 为关键节点设置重试机制
|
||||||
|
- 使用条件分支处理异常情况
|
||||||
|
- 添加日志记录便于排查问题
|
||||||
|
|
||||||
|
### 4. 测试先行
|
||||||
|
|
||||||
|
- 使用调试功能充分测试
|
||||||
|
- 准备多种测试场景
|
||||||
|
- 检查边界情况
|
||||||
|
|
||||||
|
### 5. 性能优化
|
||||||
|
|
||||||
|
- 避免不必要的节点
|
||||||
|
- 使用并行节点提高效率
|
||||||
|
- 合理设置超时时间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 更多资源
|
||||||
|
|
||||||
|
- [开发者文档](../development/workflow-system.md)
|
||||||
|
- [设计文档](../../../plans/langbot-workflow-design.md)
|
||||||
|
- [API 文档](../service-api-openapi.json)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# HTTP Bot Adapter — Reference Clients
|
||||||
|
|
||||||
|
> English | [中文](./README.zh.md)
|
||||||
|
|
||||||
|
Minimal, dependency-light clients for the LangBot **HTTP Bot** platform adapter.
|
||||||
|
They show the whole loop: signing a request, pushing a message, and receiving
|
||||||
|
multi-part replies on a callback endpoint.
|
||||||
|
|
||||||
|
Full guide: [docs.langbot.app — HTTP Bot](https://docs.langbot.app/en/usage/platforms/http-bot).
|
||||||
|
Machine-readable contract: [`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `playground.py` | **Interactive browser debug console** — a single-file web app you open in a browser to chat with a running `http_bot` bot and watch signing / 202 / callbacks live. Zero extra deps. |
|
||||||
|
| `client.py` | Python client + Flask callback receiver (`pip install flask requests`). |
|
||||||
|
| `client.ts` | TypeScript/Node 18+ client + callback receiver, **zero deps** (`npx tsx client.ts`). |
|
||||||
|
|
||||||
|
All three implement the identical HMAC-SHA256 scheme
|
||||||
|
(`sha256=hex(HMAC(secret, "{timestamp}." + body))`) — verified byte-for-byte
|
||||||
|
against the adapter.
|
||||||
|
|
||||||
|
## Interactive playground (recommended first run)
|
||||||
|
|
||||||
|
A self-contained web console: type a message in your browser, it is signed and
|
||||||
|
POSTed to a **running** `http_bot` bot, and the bot's replies stream back into
|
||||||
|
the page — with a debug panel showing the signature, the `202` ack, and each
|
||||||
|
callback's `sequence` / signature-verification.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the LangBot repo root, with the backend already running:
|
||||||
|
PUBLIC_IP=<your-host-ip> ./.venv/bin/python examples/http-bot/playground.py
|
||||||
|
# then open http://<your-host-ip>:8920/
|
||||||
|
```
|
||||||
|
|
||||||
|
On startup it reads the LangBot API key + the `http_bot` bot from
|
||||||
|
`data/langbot.db`, and configures that bot (inbound/outbound secret +
|
||||||
|
`callback_url`) to point back at itself via the LangBot API — the bot reloads
|
||||||
|
live, no restart needed. Requirements: an enabled `http_bot` bot bound to a
|
||||||
|
working pipeline, and port `8920` reachable from your browser.
|
||||||
|
|
||||||
|
Env knobs: `PUBLIC_IP` (default `127.0.0.1`), `PLAYGROUND_PORT` (default `8920`).
|
||||||
|
|
||||||
|
## Headless clients
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python — Terminal 1: callback receiver (your callback_url target)
|
||||||
|
python client.py serve --port 8900 --secret SHARED_SECRET
|
||||||
|
|
||||||
|
# Python — Terminal 2: push a message
|
||||||
|
python client.py push --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1 --text "hello"
|
||||||
|
|
||||||
|
# blocking sync mode
|
||||||
|
python client.py sync --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1 --text "hello"
|
||||||
|
|
||||||
|
# reset a session
|
||||||
|
python client.py reset --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TypeScript (Node 18+)
|
||||||
|
npx tsx client.ts serve 8900 SHARED_SECRET
|
||||||
|
npx tsx client.ts push https://your-langbot/bots/<BOT_UUID> SHARED_SECRET ticket-1 "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
When the bot replies, the receiver prints each part with its `sequence` and an
|
||||||
|
`[FINAL]` marker on the last one — that's the 1→M multi-reply model in action.
|
||||||
|
|
||||||
|
> The bot's `callback_url` must be reachable from LangBot. For local testing,
|
||||||
|
> expose your receiver with a tunnel (cloudflared / ngrok) and set that URL in
|
||||||
|
> the bot config.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# HTTP Bot 适配器 —— 参考客户端
|
||||||
|
|
||||||
|
> [English](./README.md) | 中文
|
||||||
|
|
||||||
|
面向 LangBot **HTTP Bot** 平台适配器的极简、低依赖客户端示例。
|
||||||
|
它们完整展示了整条链路:对请求签名、推送一条消息、在回调端点接收
|
||||||
|
1→M 的多段回复。
|
||||||
|
|
||||||
|
完整指南:[docs.langbot.app —— HTTP Bot](https://docs.langbot.app/zh/usage/platforms/http-bot)。
|
||||||
|
机器可读的接口契约:[`docs/http-bot-openapi.json`](../../docs/http-bot-openapi.json)。
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 文件 | 是什么 |
|
||||||
|
|---|---|
|
||||||
|
| `playground.py` | **浏览器交互式调试台** —— 单文件 Web 应用,在浏览器里和一个运行中的 `http_bot` bot 对话,实时观察签名 / 202 / 回调。零额外依赖。 |
|
||||||
|
| `client.py` | Python 客户端 + Flask 回调接收端(`pip install flask requests`)。 |
|
||||||
|
| `client.ts` | TypeScript/Node 18+ 客户端 + 回调接收端,**零依赖**(`npx tsx client.ts`)。 |
|
||||||
|
|
||||||
|
三者实现完全一致的 HMAC-SHA256 签名方案
|
||||||
|
(`sha256=hex(HMAC(secret, "{timestamp}." + body))`)—— 已与适配器逐字节比对验证。
|
||||||
|
|
||||||
|
## 交互式 playground(推荐先跑这个)
|
||||||
|
|
||||||
|
一个自包含的 Web 控制台:在浏览器里输入消息,它会被签名并 POST 给一个
|
||||||
|
**运行中**的 `http_bot` bot,bot 的回复会流式回到页面上 —— 调试面板会显示
|
||||||
|
签名、`202` 确认,以及每条回调的 `sequence` / 签名验证结果。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在 LangBot 仓库根目录、后端已启动的前提下:
|
||||||
|
PUBLIC_IP=<你的主机IP> ./.venv/bin/python examples/http-bot/playground.py
|
||||||
|
# 然后打开 http://<你的主机IP>:8920/
|
||||||
|
```
|
||||||
|
|
||||||
|
启动时它会从 `data/langbot.db` 读取 LangBot API key 和 `http_bot` bot,
|
||||||
|
并通过 LangBot API 把该 bot 配好(入站/出站密钥 + `callback_url`)指回自己 ——
|
||||||
|
bot 会热加载,无需重启。前提:有一个已启用、绑定了可用 pipeline 的
|
||||||
|
`http_bot` bot,且端口 `8920` 能从你的浏览器访问到。
|
||||||
|
|
||||||
|
可调环境变量:`PUBLIC_IP`(默认 `127.0.0.1`)、`PLAYGROUND_PORT`(默认 `8920`)。
|
||||||
|
|
||||||
|
## 无头客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python —— 终端 1:回调接收端(你的 callback_url 指向它)
|
||||||
|
python client.py serve --port 8900 --secret SHARED_SECRET
|
||||||
|
|
||||||
|
# Python —— 终端 2:推送一条消息
|
||||||
|
python client.py push --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1 --text "hello"
|
||||||
|
|
||||||
|
# 阻塞式同步模式
|
||||||
|
python client.py sync --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1 --text "hello"
|
||||||
|
|
||||||
|
# 重置一个会话
|
||||||
|
python client.py reset --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-1
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# TypeScript(Node 18+)
|
||||||
|
npx tsx client.ts serve 8900 SHARED_SECRET
|
||||||
|
npx tsx client.ts push https://your-langbot/bots/<BOT_UUID> SHARED_SECRET ticket-1 "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
当 bot 回复时,接收端会逐条打印,带上各自的 `sequence`,并在最后一条标记
|
||||||
|
`[FINAL]` —— 这就是 1→M 多段回复模型的实际效果。
|
||||||
|
|
||||||
|
> bot 的 `callback_url` 必须能从 LangBot 访问到。本地测试时,可用隧道
|
||||||
|
> (cloudflared / ngrok)把你的接收端暴露出去,并把那个 URL 填进 bot 配置。
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""LangBot HTTP Bot adapter — reference client (Python).
|
||||||
|
|
||||||
|
Two things in one file:
|
||||||
|
|
||||||
|
1. ``push()`` / ``push_sync()`` — send a message into a LangBot ``http_bot`` bot.
|
||||||
|
2. A tiny Flask callback receiver that verifies signatures and prints replies,
|
||||||
|
so you can watch N->1 aggregation and 1->M multi-reply working live.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
pip install flask requests
|
||||||
|
|
||||||
|
# Terminal 1 — start the callback receiver (this is your callback_url):
|
||||||
|
python client.py serve --port 8900 --secret SHARED_SECRET
|
||||||
|
|
||||||
|
# Terminal 2 — push a message (async; reply lands on the receiver):
|
||||||
|
python client.py push \
|
||||||
|
--url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET \
|
||||||
|
--session ticket-10293 \
|
||||||
|
--text "Export keeps failing on the dashboard."
|
||||||
|
|
||||||
|
# Or push and block for the collapsed reply (sync convenience mode):
|
||||||
|
python client.py sync --url https://your-langbot/bots/<BOT_UUID> \
|
||||||
|
--secret SHARED_SECRET --session ticket-10293 --text "hi"
|
||||||
|
|
||||||
|
The signing scheme is HMAC-SHA256 over ``"{timestamp}." + raw_body``; see
|
||||||
|
``sign()`` below — it is intentionally tiny and easy to port.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
HEADER_TIMESTAMP = 'X-LB-Timestamp'
|
||||||
|
HEADER_SIGNATURE = 'X-LB-Signature'
|
||||||
|
HEADER_IDEMPOTENCY = 'X-LB-Idempotency-Key'
|
||||||
|
REPLAY_WINDOW = 300
|
||||||
|
|
||||||
|
|
||||||
|
def sign(secret: str, body: bytes, timestamp: int | None = None) -> tuple[str, str]:
|
||||||
|
"""Return (timestamp, signature) for *body*."""
|
||||||
|
ts = str(timestamp if timestamp is not None else int(time.time()))
|
||||||
|
mac = hmac.new(secret.encode(), f'{ts}.'.encode() + body, hashlib.sha256)
|
||||||
|
return ts, 'sha256=' + mac.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify(secret: str, body: bytes, timestamp: str | None, signature: str | None) -> bool:
|
||||||
|
"""Verify an inbound signature (used by the callback receiver)."""
|
||||||
|
if not timestamp or not signature:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if abs(int(time.time()) - int(float(timestamp))) > REPLAY_WINDOW:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
_, expected = sign(secret, body, int(float(timestamp)))
|
||||||
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
|
||||||
|
def _post(url: str, secret: str, payload: dict, idempotency: bool = True):
|
||||||
|
import requests
|
||||||
|
|
||||||
|
body = json.dumps(payload, ensure_ascii=False).encode()
|
||||||
|
ts, sig = sign(secret, body)
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
HEADER_TIMESTAMP: ts,
|
||||||
|
HEADER_SIGNATURE: sig,
|
||||||
|
}
|
||||||
|
if idempotency:
|
||||||
|
headers[HEADER_IDEMPOTENCY] = uuid.uuid4().hex
|
||||||
|
resp = requests.post(url, data=body, headers=headers, timeout=30)
|
||||||
|
print(f'-> {resp.status_code} {resp.text}')
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def push(url: str, secret: str, session: str, text: str, session_type: str = 'person'):
|
||||||
|
"""Fire-and-collect: returns 202 immediately; reply arrives on your callback."""
|
||||||
|
payload = {
|
||||||
|
'session_id': session,
|
||||||
|
'session_type': session_type,
|
||||||
|
'message': [{'type': 'Plain', 'text': text}],
|
||||||
|
}
|
||||||
|
return _post(url.rstrip('/'), secret, payload)
|
||||||
|
|
||||||
|
|
||||||
|
def push_sync(url: str, secret: str, session: str, text: str, session_type: str = 'person'):
|
||||||
|
"""Blocking convenience: POST to /sync and get the collapsed reply back."""
|
||||||
|
payload = {
|
||||||
|
'session_id': session,
|
||||||
|
'session_type': session_type,
|
||||||
|
'message': [{'type': 'Plain', 'text': text}],
|
||||||
|
}
|
||||||
|
resp = _post(url.rstrip('/') + '/sync', secret, payload, idempotency=False)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def reset(url: str, secret: str, session: str, session_type: str = 'person'):
|
||||||
|
"""Reset a session's conversation (next message starts fresh)."""
|
||||||
|
payload = {'session_id': session, 'session_type': session_type}
|
||||||
|
return _post(url.rstrip('/') + '/reset', secret, payload, idempotency=False)
|
||||||
|
|
||||||
|
|
||||||
|
def serve(port: int, secret: str):
|
||||||
|
"""Run a callback receiver that verifies signatures and prints replies."""
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/', methods=['POST'])
|
||||||
|
def recv():
|
||||||
|
raw = request.get_data()
|
||||||
|
ok = verify(secret, raw, request.headers.get(HEADER_TIMESTAMP), request.headers.get(HEADER_SIGNATURE))
|
||||||
|
if not ok:
|
||||||
|
print('!! signature verification FAILED — rejecting')
|
||||||
|
return {'error': 'bad signature'}, 401
|
||||||
|
data = json.loads(raw)
|
||||||
|
text_parts = [c.get('text', '') for c in data.get('message', []) if c.get('type') == 'Plain']
|
||||||
|
marker = 'FINAL' if data.get('is_final') else 'part '
|
||||||
|
print(
|
||||||
|
f'[{marker}] session={data["session_id"]} seq={data["sequence"]} '
|
||||||
|
f'reply_to={data.get("reply_to")}: {" ".join(text_parts)}'
|
||||||
|
)
|
||||||
|
return {'ok': True}
|
||||||
|
|
||||||
|
print(f'callback receiver listening on http://0.0.0.0:{port}/ (Ctrl-C to stop)')
|
||||||
|
app.run(host='0.0.0.0', port=port)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
p = argparse.ArgumentParser(description='LangBot HTTP Bot reference client')
|
||||||
|
sub = p.add_subparsers(dest='cmd', required=True)
|
||||||
|
|
||||||
|
sp = sub.add_parser('serve', help='run the callback receiver')
|
||||||
|
sp.add_argument('--port', type=int, default=8900)
|
||||||
|
sp.add_argument('--secret', required=True)
|
||||||
|
|
||||||
|
for name in ('push', 'sync', 'reset'):
|
||||||
|
c = sub.add_parser(name)
|
||||||
|
c.add_argument('--url', required=True, help='https://host/bots/<BOT_UUID>')
|
||||||
|
c.add_argument('--secret', required=True)
|
||||||
|
c.add_argument('--session', required=True)
|
||||||
|
c.add_argument('--session-type', default='person', choices=['person', 'group'])
|
||||||
|
if name != 'reset':
|
||||||
|
c.add_argument('--text', required=True)
|
||||||
|
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
if args.cmd == 'serve':
|
||||||
|
serve(args.port, args.secret)
|
||||||
|
elif args.cmd == 'push':
|
||||||
|
push(args.url, args.secret, args.session, args.text, args.session_type)
|
||||||
|
elif args.cmd == 'sync':
|
||||||
|
push_sync(args.url, args.secret, args.session, args.text, args.session_type)
|
||||||
|
elif args.cmd == 'reset':
|
||||||
|
reset(args.url, args.secret, args.session, args.session_type)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* LangBot HTTP Bot adapter — reference client (TypeScript / Node 18+).
|
||||||
|
*
|
||||||
|
* Zero runtime dependencies (uses global `fetch`, `crypto`, and `http`).
|
||||||
|
*
|
||||||
|
* - `push()` : fire-and-collect; reply lands on your callback URL.
|
||||||
|
* - `pushSync()` : POST /sync and await the collapsed reply.
|
||||||
|
* - `reset()` : reset a session's conversation.
|
||||||
|
* - `startReceiver()` : a callback server that verifies signatures and logs
|
||||||
|
* replies, so you can watch N->1 and 1->M live.
|
||||||
|
*
|
||||||
|
* Run the demos:
|
||||||
|
* npx tsx client.ts serve 8900 SHARED_SECRET
|
||||||
|
* npx tsx client.ts push https://host/bots/<UUID> SHARED_SECRET ticket-1 "hello"
|
||||||
|
* npx tsx client.ts sync https://host/bots/<UUID> SHARED_SECRET ticket-1 "hello"
|
||||||
|
* npx tsx client.ts reset https://host/bots/<UUID> SHARED_SECRET ticket-1
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
|
||||||
|
const HEADER_TIMESTAMP = 'X-LB-Timestamp';
|
||||||
|
const HEADER_SIGNATURE = 'X-LB-Signature';
|
||||||
|
const HEADER_IDEMPOTENCY = 'X-LB-Idempotency-Key';
|
||||||
|
const REPLAY_WINDOW = 300;
|
||||||
|
|
||||||
|
/** Compute the `sha256=<hex>` signature over `"{ts}." + body`. */
|
||||||
|
export function sign(secret: string, body: Buffer | string, timestamp?: number): [string, string] {
|
||||||
|
const ts = String(timestamp ?? Math.floor(Date.now() / 1000));
|
||||||
|
const buf = typeof body === 'string' ? Buffer.from(body) : body;
|
||||||
|
const mac = createHmac('sha256', secret).update(Buffer.concat([Buffer.from(`${ts}.`), buf])).digest('hex');
|
||||||
|
return [ts, `sha256=${mac}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verify an inbound signature (used by the callback receiver). */
|
||||||
|
export function verify(secret: string, body: Buffer, timestamp?: string, signature?: string): boolean {
|
||||||
|
if (!timestamp || !signature) return false;
|
||||||
|
if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > REPLAY_WINDOW) return false;
|
||||||
|
const [, expected] = sign(secret, body, Number(timestamp));
|
||||||
|
const a = Buffer.from(expected);
|
||||||
|
const b = Buffer.from(signature);
|
||||||
|
return a.length === b.length && timingSafeEqual(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Segment { type: string; text?: string; url?: string; [k: string]: unknown }
|
||||||
|
|
||||||
|
async function post(url: string, secret: string, payload: object, idempotency = true) {
|
||||||
|
const body = Buffer.from(JSON.stringify(payload));
|
||||||
|
const [ts, sig] = sign(secret, body);
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
[HEADER_TIMESTAMP]: ts,
|
||||||
|
[HEADER_SIGNATURE]: sig,
|
||||||
|
};
|
||||||
|
if (idempotency) headers[HEADER_IDEMPOTENCY] = randomUUID();
|
||||||
|
const resp = await fetch(url, { method: 'POST', headers, body });
|
||||||
|
const text = await resp.text();
|
||||||
|
console.log(`-> ${resp.status} ${text}`);
|
||||||
|
return { status: resp.status, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fire-and-collect: 202 now, reply later on your callback URL. */
|
||||||
|
export function push(url: string, secret: string, session: string, text: string, sessionType = 'person') {
|
||||||
|
return post(url.replace(/\/$/, ''), secret, {
|
||||||
|
session_id: session,
|
||||||
|
session_type: sessionType,
|
||||||
|
message: [{ type: 'Plain', text }] as Segment[],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Blocking convenience: POST /sync, get the collapsed reply. */
|
||||||
|
export function pushSync(url: string, secret: string, session: string, text: string, sessionType = 'person') {
|
||||||
|
return post(`${url.replace(/\/$/, '')}/sync`, secret, {
|
||||||
|
session_id: session,
|
||||||
|
session_type: sessionType,
|
||||||
|
message: [{ type: 'Plain', text }] as Segment[],
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset a session's conversation. */
|
||||||
|
export function reset(url: string, secret: string, session: string, sessionType = 'person') {
|
||||||
|
return post(`${url.replace(/\/$/, '')}/reset`, secret, { session_id: session, session_type: sessionType }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run a callback receiver that verifies signatures and prints replies. */
|
||||||
|
export function startReceiver(port: number, secret: string) {
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
if (req.method !== 'POST') { res.writeHead(405).end(); return; }
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
req.on('data', (c) => chunks.push(c));
|
||||||
|
req.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks);
|
||||||
|
const ok = verify(secret, raw, req.headers[HEADER_TIMESTAMP.toLowerCase()] as string,
|
||||||
|
req.headers[HEADER_SIGNATURE.toLowerCase()] as string);
|
||||||
|
if (!ok) {
|
||||||
|
console.log('!! signature verification FAILED — rejecting');
|
||||||
|
res.writeHead(401, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'bad signature' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(raw.toString());
|
||||||
|
const parts = (data.message as Segment[]).filter((c) => c.type === 'Plain').map((c) => c.text).join(' ');
|
||||||
|
const marker = data.is_final ? 'FINAL' : 'part ';
|
||||||
|
console.log(`[${marker}] session=${data.session_id} seq=${data.sequence} reply_to=${data.reply_to}: ${parts}`);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.listen(port, () => console.log(`callback receiver listening on http://0.0.0.0:${port}/ (Ctrl-C to stop)`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CLI ---
|
||||||
|
const [cmd, ...rest] = process.argv.slice(2);
|
||||||
|
if (cmd === 'serve') {
|
||||||
|
startReceiver(Number(rest[0] ?? 8900), rest[1] ?? 'SHARED_SECRET');
|
||||||
|
} else if (cmd === 'push') {
|
||||||
|
push(rest[0], rest[1], rest[2], rest[3]);
|
||||||
|
} else if (cmd === 'sync') {
|
||||||
|
pushSync(rest[0], rest[1], rest[2], rest[3]);
|
||||||
|
} else if (cmd === 'reset') {
|
||||||
|
reset(rest[0], rest[1], rest[2]);
|
||||||
|
} else if (cmd) {
|
||||||
|
console.error(`unknown command: ${cmd}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""LangBot HTTP Bot — interactive playground (public, browser-based).
|
||||||
|
|
||||||
|
This is a REAL end-to-end demo against the RUNNING LangBot instance on this
|
||||||
|
host. It is NOT a mock and NOT an in-process import: every message you type in
|
||||||
|
the browser is signed and POSTed to the live `http_bot` bot at
|
||||||
|
http://127.0.0.1:5300/bots/<uuid>, and the bot's replies come back to this
|
||||||
|
server's /callback endpoint over real HTTP, then stream to your browser via SSE.
|
||||||
|
|
||||||
|
What it does on startup:
|
||||||
|
1. Reads the LangBot API key + the http_bot bot from data/langbot.db.
|
||||||
|
2. Configures the bot via the LangBot API (PUT /api/v1/platform/bots/<uuid>):
|
||||||
|
sets inbound_secret + outbound_secret + callback_url to point back here.
|
||||||
|
(LangBot reloads the bot live — no server restart needed.)
|
||||||
|
3. Serves a chat page on 0.0.0.0:<PORT> so you can open it from the internet.
|
||||||
|
|
||||||
|
Run: ./.venv/bin/python examples/http-bot/playground.py
|
||||||
|
Then open: http://<this-host-public-ip>:<PORT>/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
sys.path.insert(0, os.path.join(REPO, 'src'))
|
||||||
|
|
||||||
|
from aiohttp import web # noqa: E402
|
||||||
|
import aiohttp # noqa: E402
|
||||||
|
|
||||||
|
from langbot.pkg.platform.sources import http_bot_signing as sg # noqa: E402
|
||||||
|
|
||||||
|
# ---- config -----------------------------------------------------------------
|
||||||
|
LANGBOT_BASE = 'http://127.0.0.1:5300'
|
||||||
|
DB_PATH = os.path.join(REPO, 'data', 'langbot.db')
|
||||||
|
PUBLIC_IP = os.environ.get('PUBLIC_IP', '127.0.0.1')
|
||||||
|
PORT = int(os.environ.get('PLAYGROUND_PORT', '8920'))
|
||||||
|
SECRET = 'playground-shared-secret'
|
||||||
|
|
||||||
|
# SSE subscribers: list of asyncio.Queue
|
||||||
|
subscribers: list[asyncio.Queue] = []
|
||||||
|
|
||||||
|
|
||||||
|
def db_lookup() -> tuple[str, str]:
|
||||||
|
"""Return (api_key, http_bot_uuid) from the LangBot DB."""
|
||||||
|
db = sqlite3.connect(DB_PATH)
|
||||||
|
db.row_factory = sqlite3.Row
|
||||||
|
api_key = db.execute('SELECT key FROM api_keys LIMIT 1').fetchone()['key']
|
||||||
|
bot = db.execute("SELECT uuid FROM bots WHERE adapter='http_bot' LIMIT 1").fetchone()
|
||||||
|
if not bot:
|
||||||
|
raise SystemExit('No http_bot bot found. Create one in the WebUI first.')
|
||||||
|
return api_key, bot['uuid']
|
||||||
|
|
||||||
|
|
||||||
|
async def configure_bot(api_key: str, bot_uuid: str, callback_url: str):
|
||||||
|
"""Point the live bot at this playground via the LangBot API.
|
||||||
|
|
||||||
|
update_bot() runs a raw SQL UPDATE with whatever keys we send, so we send a
|
||||||
|
MINIMAL payload: only adapter_config (built from scratch, not read back —
|
||||||
|
the GET masks secrets). LangBot reloads + reruns the bot live.
|
||||||
|
"""
|
||||||
|
cfg = {
|
||||||
|
'inbound_secret': SECRET,
|
||||||
|
'outbound_secret': SECRET,
|
||||||
|
'callback_url': callback_url,
|
||||||
|
'signature_required': True,
|
||||||
|
'default_session_type': 'person',
|
||||||
|
'callback_timeout': 15,
|
||||||
|
'callback_max_retries': 3,
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession() as s:
|
||||||
|
async with s.put(
|
||||||
|
f'{LANGBOT_BASE}/api/v1/platform/bots/{bot_uuid}',
|
||||||
|
headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'},
|
||||||
|
json={'adapter_config': cfg},
|
||||||
|
) as r:
|
||||||
|
txt = await r.text()
|
||||||
|
print(f'[configure] PUT adapter_config -> {r.status} {txt[:200]}')
|
||||||
|
return r.status < 400
|
||||||
|
|
||||||
|
|
||||||
|
async def broadcast(event: dict):
|
||||||
|
for q in list(subscribers):
|
||||||
|
try:
|
||||||
|
q.put_nowait(event)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---- HTTP handlers ----------------------------------------------------------
|
||||||
|
async def index(request: web.Request):
|
||||||
|
return web.Response(text=PAGE, content_type='text/html')
|
||||||
|
|
||||||
|
|
||||||
|
async def send(request: web.Request):
|
||||||
|
"""Browser -> here -> signed POST -> live LangBot bot."""
|
||||||
|
body_in = await request.json()
|
||||||
|
session_id = body_in.get('session_id') or 'playground-1'
|
||||||
|
text = body_in.get('text', '')
|
||||||
|
bot_uuid = request.app['bot_uuid']
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'session_id': session_id,
|
||||||
|
'sender': {'id': 'browser-user', 'name': 'You'},
|
||||||
|
'message': [{'type': 'Plain', 'text': text}],
|
||||||
|
}
|
||||||
|
raw = json.dumps(payload, ensure_ascii=False).encode()
|
||||||
|
ts, sig = sg.sign(SECRET, raw)
|
||||||
|
url = f'{LANGBOT_BASE}/bots/{bot_uuid}'
|
||||||
|
|
||||||
|
# echo what we send to the browser timeline
|
||||||
|
await broadcast(
|
||||||
|
{'dir': 'out', 'kind': 'request', 'session_id': session_id, 'text': text, 'url': url, 'sig': sig[:24] + '…'}
|
||||||
|
)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as s:
|
||||||
|
async with s.post(
|
||||||
|
url,
|
||||||
|
data=raw,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
sg.HEADER_TIMESTAMP: ts,
|
||||||
|
sg.HEADER_SIGNATURE: sig,
|
||||||
|
},
|
||||||
|
) as r:
|
||||||
|
status = r.status
|
||||||
|
try:
|
||||||
|
jr = await r.json()
|
||||||
|
except Exception:
|
||||||
|
jr = {'raw': await r.text()}
|
||||||
|
await broadcast({'dir': 'in', 'kind': 'ack', 'status': status, 'data': jr})
|
||||||
|
return web.json_response({'status': status, 'data': jr})
|
||||||
|
|
||||||
|
|
||||||
|
async def callback(request: web.Request):
|
||||||
|
"""Live LangBot bot -> here. Verify signature, stream to browser."""
|
||||||
|
raw = await request.read()
|
||||||
|
ok, why = sg.verify(SECRET, raw, request.headers.get(sg.HEADER_TIMESTAMP), request.headers.get(sg.HEADER_SIGNATURE))
|
||||||
|
data = json.loads(raw)
|
||||||
|
text = ' '.join(c.get('text', '') for c in data.get('message', []) if c.get('type') == 'Plain')
|
||||||
|
await broadcast(
|
||||||
|
{
|
||||||
|
'dir': 'in',
|
||||||
|
'kind': 'reply',
|
||||||
|
'session_id': data.get('session_id'),
|
||||||
|
'sequence': data.get('sequence'),
|
||||||
|
'is_final': data.get('is_final'),
|
||||||
|
'sig_ok': ok,
|
||||||
|
'sig_why': why,
|
||||||
|
'text': text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return web.json_response({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
async def events(request: web.Request):
|
||||||
|
"""SSE stream to the browser."""
|
||||||
|
resp = web.StreamResponse(
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await resp.prepare(request)
|
||||||
|
q: asyncio.Queue = asyncio.Queue()
|
||||||
|
subscribers.append(q)
|
||||||
|
try:
|
||||||
|
await resp.write(b': connected\n\n')
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
ev = await asyncio.wait_for(q.get(), timeout=15)
|
||||||
|
await resp.write(f'data: {json.dumps(ev, ensure_ascii=False)}\n\n'.encode())
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await resp.write(b': ping\n\n')
|
||||||
|
except (asyncio.CancelledError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if q in subscribers:
|
||||||
|
subscribers.remove(q)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
PAGE = r"""<!doctype html>
|
||||||
|
<html lang="zh"><head><meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||||
|
<title>LangBot HTTP Bot · 调试台</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#f7f8fa; --panel:#ffffff; --line:#e8eaed; --ink:#1f2329; --mut:#8a909a;
|
||||||
|
--brand:#2563eb; --brand-soft:#eef3ff; --ok:#16a34a; --bad:#dc2626; --code:#f3f4f6;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
|
body{margin:0;background:var(--bg);color:var(--ink);
|
||||||
|
font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif}
|
||||||
|
.top{height:52px;background:var(--panel);border-bottom:1px solid var(--line);
|
||||||
|
display:flex;align-items:center;gap:10px;padding:0 18px}
|
||||||
|
.logo{width:26px;height:26px;border-radius:7px;background:var(--brand);display:grid;place-items:center;color:#fff;font-weight:700;font-size:14px}
|
||||||
|
.top b{font-size:15px} .top .ver{font-size:12px;color:var(--mut)}
|
||||||
|
.dot{width:8px;height:8px;border-radius:50%;background:#cbd2dc;display:inline-block;margin-right:5px;vertical-align:middle}
|
||||||
|
.dot.on{background:var(--ok)} .dot.off{background:var(--bad)}
|
||||||
|
.conn{margin-left:auto;font-size:12px;color:var(--mut)}
|
||||||
|
.wrap{max-width:1080px;margin:0 auto;padding:18px;display:grid;grid-template-columns:1fr 360px;gap:16px}
|
||||||
|
@media(max-width:880px){.wrap{grid-template-columns:1fr}}
|
||||||
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;min-height:0}
|
||||||
|
.card h3{margin:0;padding:12px 16px;font-size:13px;font-weight:600;color:#4b5563;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:8px}
|
||||||
|
.chat{height:62vh}
|
||||||
|
.msgs{flex:1;overflow:auto;padding:16px;display:flex;flex-direction:column;gap:12px}
|
||||||
|
.row{display:flex;flex-direction:column;gap:4px;max-width:82%}
|
||||||
|
.row.me{align-self:flex-end;align-items:flex-end}
|
||||||
|
.row.bot{align-self:flex-start}
|
||||||
|
.bub{padding:9px 13px;border-radius:12px;white-space:pre-wrap;word-break:break-word}
|
||||||
|
.me .bub{background:var(--brand);color:#fff;border-bottom-right-radius:3px}
|
||||||
|
.bot .bub{background:#f1f3f6;color:var(--ink);border-bottom-left-radius:3px}
|
||||||
|
.meta{font-size:11px;color:var(--mut)}
|
||||||
|
.meta .ok{color:var(--ok)} .meta .bad{color:var(--bad)}
|
||||||
|
.sys{align-self:center;font-size:12px;color:var(--mut);background:#f1f3f6;border-radius:8px;padding:4px 12px}
|
||||||
|
.bar{display:flex;gap:8px;padding:12px;border-top:1px solid var(--line)}
|
||||||
|
.bar input{flex:1;border:1px solid var(--line);border-radius:9px;padding:10px 12px;font-size:14px;outline:none}
|
||||||
|
.bar input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-soft)}
|
||||||
|
.bar button{background:var(--brand);color:#fff;border:0;border-radius:9px;padding:0 18px;font-size:14px;font-weight:500;cursor:pointer}
|
||||||
|
.bar button:disabled{opacity:.5;cursor:default}
|
||||||
|
.side{height:62vh}
|
||||||
|
.kv{padding:12px 16px;border-bottom:1px solid var(--line);font-size:12px}
|
||||||
|
.kv .k{color:var(--mut)} .kv .v{color:var(--ink);word-break:break-all}
|
||||||
|
.kv code{background:var(--code);border-radius:5px;padding:1px 5px;font-size:11px}
|
||||||
|
.sessrow{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line);font-size:12px}
|
||||||
|
.sessrow input{flex:1;border:1px solid var(--line);border-radius:7px;padding:5px 8px;font-size:12px}
|
||||||
|
.sessrow button{border:1px solid var(--line);background:#fff;border-radius:7px;padding:5px 9px;font-size:12px;cursor:pointer;color:#4b5563}
|
||||||
|
.trace{flex:1;overflow:auto;padding:10px 12px;font:11px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||||
|
.ev{padding:6px 8px;border-radius:7px;margin-bottom:6px;border:1px solid var(--line)}
|
||||||
|
.ev .t{font-weight:600;font-size:10px;letter-spacing:.3px;text-transform:uppercase}
|
||||||
|
.ev.out{background:#f5f8ff;border-color:#dbe6ff}.ev.out .t{color:var(--brand)}
|
||||||
|
.ev.ack{background:#f4f6f8}.ev.ack .t{color:#6b7280}
|
||||||
|
.ev.reply{background:#f1faf3;border-color:#cdeed6}.ev.reply .t{color:var(--ok)}
|
||||||
|
.ev pre{margin:3px 0 0;white-space:pre-wrap;word-break:break-all;color:#374151}
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<div class="top">
|
||||||
|
<div class="logo">L</div>
|
||||||
|
<b>HTTP Bot 调试台</b><span class="ver">examples/http-bot</span>
|
||||||
|
<span class="conn"><span class="dot off" id="cdot"></span><span id="conn">连接中…</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="wrap">
|
||||||
|
<!-- chat -->
|
||||||
|
<div class="card chat">
|
||||||
|
<h3>对话 · 真实发往运行中的 http_bot</h3>
|
||||||
|
<div class="msgs" id="msgs"></div>
|
||||||
|
<div class="bar">
|
||||||
|
<input id="msg" placeholder="输入消息,回车发送…" autofocus/>
|
||||||
|
<button id="send">发送</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- debug -->
|
||||||
|
<div class="card side">
|
||||||
|
<h3>调试信息</h3>
|
||||||
|
<div class="kv"><span class="k">入站地址</span><br><span class="v"><code id="endpoint">/bots/<uuid></code></span></div>
|
||||||
|
<div class="kv"><span class="k">签名</span> <span class="v">HMAC-SHA256 · <code>X-LB-Signature</code></span></div>
|
||||||
|
<div class="sessrow">
|
||||||
|
<span class="k">会话</span>
|
||||||
|
<input id="sid" value="playground-1"/>
|
||||||
|
<button id="reset">新会话</button>
|
||||||
|
</div>
|
||||||
|
<div class="trace" id="trace"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const $=s=>document.querySelector(s);
|
||||||
|
const msgs=$('#msgs'),trace=$('#trace'),inp=$('#msg'),btn=$('#send'),
|
||||||
|
conn=$('#conn'),cdot=$('#cdot'),sidIn=$('#sid');
|
||||||
|
function el(c){const d=document.createElement('div');d.className=c;return d}
|
||||||
|
function atBottom(n){n.scrollTop=n.scrollHeight}
|
||||||
|
function bubble(side,text,metaHtml){
|
||||||
|
const r=el('row '+side),b=el('bub');b.textContent=text;r.appendChild(b);
|
||||||
|
if(metaHtml){const m=el('meta');m.innerHTML=metaHtml;r.appendChild(m)}
|
||||||
|
msgs.appendChild(r);atBottom(msgs)}
|
||||||
|
function sys(t){const d=el('sys');d.textContent=t;msgs.appendChild(d);atBottom(msgs)}
|
||||||
|
function logEv(kind,title,obj){
|
||||||
|
const e=el('ev '+kind),t=el('t');t.textContent=title;e.appendChild(t);
|
||||||
|
if(obj!==undefined){const p=document.createElement('pre');
|
||||||
|
p.textContent=typeof obj==='string'?obj:JSON.stringify(obj,null,2);e.appendChild(p)}
|
||||||
|
trace.appendChild(e);atBottom(trace)}
|
||||||
|
|
||||||
|
const es=new EventSource('/events');
|
||||||
|
es.onopen=()=>{conn.textContent='SSE 已连接';cdot.className='dot on'};
|
||||||
|
es.onerror=()=>{conn.textContent='SSE 断开,重连…';cdot.className='dot off'};
|
||||||
|
es.onmessage=e=>{const ev=JSON.parse(e.data);
|
||||||
|
if(ev.kind==='request'){
|
||||||
|
if(ev.endpoint)$('#endpoint').textContent=ev.url||ev.endpoint;
|
||||||
|
logEv('out','出站 · 已签名 POST',{url:ev.url,session_id:ev.session_id,'X-LB-Signature':ev.sig});
|
||||||
|
}else if(ev.kind==='ack'){
|
||||||
|
const id=ev.data&&ev.data.data&&ev.data.data.accepted_message_id;
|
||||||
|
sys(`LangBot 已接收 · HTTP ${ev.status}`);
|
||||||
|
logEv('ack','入站确认 202',{status:ev.status,accepted_message_id:id||'-'});
|
||||||
|
}else if(ev.kind==='reply'){
|
||||||
|
const sig=ev.sig_ok?'<span class=ok>验签通过</span>':'<span class=bad>验签失败</span>';
|
||||||
|
bubble('bot',ev.text,`seq=${ev.sequence} · ${ev.is_final?'<b>FINAL</b>':'中间段'} · ${sig}`);
|
||||||
|
logEv('reply',`回调 · seq ${ev.sequence}${ev.is_final?' · FINAL':''}`,
|
||||||
|
{session_id:ev.session_id,sequence:ev.sequence,is_final:ev.is_final,sig_ok:ev.sig_ok,text:ev.text});
|
||||||
|
}};
|
||||||
|
|
||||||
|
async function send(){
|
||||||
|
const t=inp.value.trim();if(!t)return;inp.value='';btn.disabled=true;
|
||||||
|
bubble('me',t,'已签名 → POST /bots/<uuid>');
|
||||||
|
try{await fetch('/send',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||||
|
body:JSON.stringify({session_id:sidIn.value.trim()||'playground-1',text:t})});}
|
||||||
|
catch(e){sys('发送失败:'+e)}
|
||||||
|
btn.disabled=false;inp.focus();}
|
||||||
|
btn.onclick=send;inp.addEventListener('keydown',e=>{if(e.key==='Enter')send()});
|
||||||
|
$('#reset').onclick=()=>{sidIn.value='playground-'+Math.random().toString(36).slice(2,7);
|
||||||
|
sys('已切换到新会话 '+sidIn.value);};
|
||||||
|
sys('调试台就绪 · 每条消息都会真实发往运行中的 http_bot,右侧可观察签名 / 202 / 回调全过程。');
|
||||||
|
</script>
|
||||||
|
</body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
api_key, bot_uuid = db_lookup()
|
||||||
|
callback_url = f'http://{PUBLIC_IP}:{PORT}/callback'
|
||||||
|
print(f'[init] http_bot uuid = {bot_uuid}')
|
||||||
|
print(f'[init] callback_url = {callback_url}')
|
||||||
|
ok = await configure_bot(api_key, bot_uuid, callback_url)
|
||||||
|
if not ok:
|
||||||
|
print('[warn] bot config update failed; check the API key / payload shape')
|
||||||
|
|
||||||
|
app = web.Application()
|
||||||
|
app['bot_uuid'] = bot_uuid
|
||||||
|
app.router.add_get('/', index)
|
||||||
|
app.router.add_post('/send', send)
|
||||||
|
app.router.add_post('/callback', callback)
|
||||||
|
app.router.add_get('/events', events)
|
||||||
|
|
||||||
|
runner = web.AppRunner(app)
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, '0.0.0.0', PORT)
|
||||||
|
await site.start()
|
||||||
|
print(f'\n ▶ 打开: http://{PUBLIC_IP}:{PORT}/\n')
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Page Bot Adapter — Embed Demo
|
||||||
|
|
||||||
|
> English | [中文](./README.zh.md)
|
||||||
|
|
||||||
|
A single self-contained HTML page that demos the LangBot **Page Bot**
|
||||||
|
(`web_page_bot`) embeddable chat widget — the one you drop onto any website with
|
||||||
|
a single `<script>` tag.
|
||||||
|
|
||||||
|
Full guide: [docs.langbot.app — Page Bot](https://docs.langbot.app/en/usage/platforms/webpage).
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `index.html` | **Browser demo** — open it, point it at a running LangBot instance + a Page Bot you created, and it loads the live embed widget so you can chat with the bot exactly as a site visitor would. Zero deps, no build step. |
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
1. In the LangBot WebUI, create a bot with the **Page Bot** (`页面机器人`)
|
||||||
|
adapter and bind it to a working pipeline. Copy its **bot UUID** from the
|
||||||
|
generated embed code.
|
||||||
|
2. Open `index.html` in a browser. Any of these work:
|
||||||
|
- double-click the file, or
|
||||||
|
- serve the folder: `python3 -m http.server 8930` then open
|
||||||
|
`http://localhost:8930/examples/web-page-bot/`.
|
||||||
|
3. Fill in:
|
||||||
|
- **LangBot base URL** — where your instance is reachable from the browser
|
||||||
|
(e.g. `http://localhost:5300`, or your public address).
|
||||||
|
- **Page Bot UUID** — from step 1.
|
||||||
|
- **Widget title** — optional, sets the `data-title` attribute.
|
||||||
|
4. Click **Load widget**. A floating chat bubble appears in the bottom-right
|
||||||
|
corner — click it and chat.
|
||||||
|
|
||||||
|
The page also renders the exact `<script>` snippet you'd paste into your own
|
||||||
|
site (before `</body>`), and updates it live as you edit the fields.
|
||||||
|
|
||||||
|
## What it demonstrates
|
||||||
|
|
||||||
|
- The embed contract: `<script data-title="…" src="<base>/api/v1/embed/<uuid>/widget.js"></script>`.
|
||||||
|
- `widget.js` is served by LangBot pre-configured for that bot UUID — title,
|
||||||
|
bubble icon, language and optional Cloudflare Turnstile protection all come
|
||||||
|
from the bot's config, no page changes needed.
|
||||||
|
- Messages travel over a WebSocket to the bot's bound pipeline; replies stream
|
||||||
|
back into the bubble.
|
||||||
|
|
||||||
|
> The widget loads `widget.js` from your LangBot instance, so the **base URL
|
||||||
|
> must be reachable from the browser** you open this page in. If LangBot runs on
|
||||||
|
> a server, use its public address instead of `localhost`.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 页面机器人适配器 —— 嵌入演示
|
||||||
|
|
||||||
|
> [English](./README.md) | 中文
|
||||||
|
|
||||||
|
一个自包含的单文件 HTML 页面,用于演示 LangBot **页面机器人**
|
||||||
|
(`web_page_bot`) 的可嵌入聊天组件 —— 也就是你用一行 `<script>` 标签就能放到任意
|
||||||
|
网站上的那个组件。
|
||||||
|
|
||||||
|
完整指南:[docs.langbot.app —— 页面机器人](https://docs.langbot.app/zh/usage/platforms/webpage)。
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 文件 | 是什么 |
|
||||||
|
|---|---|
|
||||||
|
| `index.html` | **浏览器演示页** —— 打开它,填上一个运行中的 LangBot 实例地址 + 你创建的页面机器人,它就会加载真实的嵌入组件,让你像网站访客一样和机器人对话。零依赖,无需构建。 |
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 在 LangBot WebUI 中,用 **页面机器人**(`web_page_bot`)适配器创建一个机器人,
|
||||||
|
并绑定一个可用的流水线。从生成的嵌入代码里复制它的 **机器人 UUID**。
|
||||||
|
2. 在浏览器中打开 `index.html`,以下任一方式皆可:
|
||||||
|
- 直接双击该文件;或
|
||||||
|
- 起一个静态服务:`python3 -m http.server 8930`,然后打开
|
||||||
|
`http://localhost:8930/examples/web-page-bot/`。
|
||||||
|
3. 填写:
|
||||||
|
- **LangBot base URL** —— 你的实例在该浏览器中可访问的地址
|
||||||
|
(例如 `http://localhost:5300`,或你的公网地址)。
|
||||||
|
- **页面机器人 UUID** —— 第 1 步里复制的。
|
||||||
|
- **组件标题** —— 可选,对应 `data-title` 属性。
|
||||||
|
4. 点击 **Load widget**。页面右下角会出现一个浮动聊天气泡 —— 点开即可对话。
|
||||||
|
|
||||||
|
页面还会实时渲染出你需要粘贴到自己网站(放在 `</body>` 前)的那段 `<script>`
|
||||||
|
代码,并随着你编辑输入框同步更新。
|
||||||
|
|
||||||
|
## 它演示了什么
|
||||||
|
|
||||||
|
- 嵌入契约:`<script data-title="…" src="<base>/api/v1/embed/<uuid>/widget.js"></script>`。
|
||||||
|
- `widget.js` 由 LangBot 针对该机器人 UUID 预配置后下发 —— 标题、气泡图标、语言
|
||||||
|
以及可选的 Cloudflare Turnstile 防护,全部来自机器人配置,无需改动页面。
|
||||||
|
- 消息通过 WebSocket 发往机器人绑定的流水线,回复流式回到气泡中。
|
||||||
|
|
||||||
|
> 组件会从你的 LangBot 实例加载 `widget.js`,因此 **base URL 必须能从你打开本页
|
||||||
|
> 的浏览器访问到**。如果 LangBot 部署在服务器上,请用它的公网地址而非
|
||||||
|
> `localhost`。
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>LangBot Page Bot · Embed Demo</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #f7f8fa; --panel: #ffffff; --line: #e8eaed; --ink: #1f2329;
|
||||||
|
--mut: #8a909a; --brand: #2563eb; --brand-soft: #eef3ff;
|
||||||
|
--ok: #16a34a; --bad: #dc2626; --code: #f3f4f6;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
margin: 0; background: var(--bg); color: var(--ink);
|
||||||
|
font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
height: 52px; background: var(--panel); border-bottom: 1px solid var(--line);
|
||||||
|
display: flex; align-items: center; gap: 10px; padding: 0 18px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 26px; height: 26px; border-radius: 7px; background: var(--brand);
|
||||||
|
display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 14px;
|
||||||
|
}
|
||||||
|
.top b { font-size: 15px; }
|
||||||
|
.top .ver { font-size: 12px; color: var(--mut); }
|
||||||
|
.wrap { max-width: 760px; margin: 0 auto; padding: 28px 18px 80px; }
|
||||||
|
.hero h1 { margin: 8px 0 6px; font-size: 22px; }
|
||||||
|
.hero p { margin: 0 0 4px; color: var(--mut); }
|
||||||
|
.card {
|
||||||
|
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||||||
|
padding: 20px; margin-top: 20px;
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
margin: 0 0 14px; font-size: 14px; font-weight: 600; color: #4b5563;
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.card h3 .num {
|
||||||
|
width: 20px; height: 20px; border-radius: 50%; background: var(--brand-soft);
|
||||||
|
color: var(--brand); display: grid; place-items: center; font-size: 12px; font-weight: 700;
|
||||||
|
}
|
||||||
|
.field { margin-bottom: 14px; }
|
||||||
|
.field:last-child { margin-bottom: 0; }
|
||||||
|
.field label { display: block; font-size: 12px; color: var(--mut); margin-bottom: 5px; }
|
||||||
|
.field input {
|
||||||
|
width: 100%; border: 1px solid var(--line); border-radius: 9px;
|
||||||
|
padding: 10px 12px; font-size: 14px; outline: none; font-family: inherit;
|
||||||
|
}
|
||||||
|
.field input:focus { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
|
||||||
|
.hint { font-size: 12px; color: var(--mut); margin-top: 5px; }
|
||||||
|
.hint code { background: var(--code); border-radius: 5px; padding: 1px 5px; font-size: 11px; }
|
||||||
|
.actions { display: flex; gap: 10px; margin-top: 18px; align-items: center; }
|
||||||
|
button {
|
||||||
|
border: 0; border-radius: 9px; padding: 10px 18px; font-size: 14px;
|
||||||
|
font-weight: 500; cursor: pointer; font-family: inherit;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--brand); color: #fff; }
|
||||||
|
.btn-primary:disabled { opacity: .5; cursor: default; }
|
||||||
|
.btn-ghost { background: #fff; border: 1px solid var(--line); color: #4b5563; }
|
||||||
|
.status { font-size: 13px; color: var(--mut); }
|
||||||
|
.status .ok { color: var(--ok); }
|
||||||
|
.status .bad { color: var(--bad); }
|
||||||
|
pre {
|
||||||
|
background: #0f172a; color: #e2e8f0; border-radius: 10px; padding: 14px 16px;
|
||||||
|
overflow: auto; font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.snippet-row { position: relative; }
|
||||||
|
.snippet-row .copy {
|
||||||
|
position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,.12);
|
||||||
|
color: #fff; border: 0; border-radius: 7px; padding: 5px 10px; font-size: 12px; cursor: pointer;
|
||||||
|
}
|
||||||
|
ul.steps { margin: 0; padding-left: 18px; color: #4b5563; }
|
||||||
|
ul.steps li { margin-bottom: 6px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top">
|
||||||
|
<div class="logo">L</div>
|
||||||
|
<b>Page Bot · Embed Demo</b>
|
||||||
|
<span class="ver">examples/web-page-bot</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="hero">
|
||||||
|
<h1>Try the LangBot Page Bot widget</h1>
|
||||||
|
<p>Point this page at a running LangBot instance and a <strong>Page Bot</strong> you created,</p>
|
||||||
|
<p>then load the live embed widget below to chat with it — exactly as your site visitors would.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><span class="num">1</span> Connect your Page Bot</h3>
|
||||||
|
<div class="field">
|
||||||
|
<label for="base">LangBot base URL</label>
|
||||||
|
<input id="base" placeholder="http://localhost:5300" value="http://localhost:5300" />
|
||||||
|
<div class="hint">The address where your LangBot instance is reachable from this browser. No trailing slash.</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="uuid">Page Bot UUID</label>
|
||||||
|
<input id="uuid" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
|
||||||
|
<div class="hint">Create a bot with the <code>Page Bot</code> adapter in the WebUI, then copy its UUID from the embed code.</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="title">Widget title (optional)</label>
|
||||||
|
<input id="title" placeholder="LangBot" value="LangBot" />
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="load" class="btn-primary">Load widget</button>
|
||||||
|
<button id="unload" class="btn-ghost">Remove widget</button>
|
||||||
|
<span class="status" id="status">Not loaded.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><span class="num">2</span> The embed snippet</h3>
|
||||||
|
<p style="margin:0 0 12px;color:var(--mut)">This is exactly what you paste into your own site (before <code></body></code>). It updates as you edit the fields above.</p>
|
||||||
|
<div class="snippet-row">
|
||||||
|
<button class="copy" id="copy">Copy</button>
|
||||||
|
<pre id="snippet"><script data-title="LangBot" src="http://localhost:5300/api/v1/embed/<bot-uuid>/widget.js"></script></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3><span class="num">3</span> How it works</h3>
|
||||||
|
<ul class="steps">
|
||||||
|
<li>The <code><script></code> tag pulls <code>widget.js</code> from your LangBot instance, pre-configured for that bot UUID.</li>
|
||||||
|
<li>A floating chat bubble appears in the bottom-right corner of the page.</li>
|
||||||
|
<li>Messages travel over a WebSocket to the bot's bound pipeline; replies stream back into the bubble.</li>
|
||||||
|
<li>Title, bubble icon, language and optional Cloudflare Turnstile protection are all set in the bot's config — no page changes needed.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var $ = function (s) { return document.querySelector(s); };
|
||||||
|
var baseEl = $("#base"), uuidEl = $("#uuid"), titleEl = $("#title"),
|
||||||
|
statusEl = $("#status"), snippetEl = $("#snippet");
|
||||||
|
var WIDGET_ID = "langbot-embed-demo-script";
|
||||||
|
|
||||||
|
function clean(v) { return (v || "").trim().replace(/\/+$/, ""); }
|
||||||
|
|
||||||
|
function buildSrc() {
|
||||||
|
var base = clean(baseEl.value) || "http://localhost:5300";
|
||||||
|
var uuid = uuidEl.value.trim() || "<bot-uuid>";
|
||||||
|
return base + "/api/v1/embed/" + uuid + "/widget.js";
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshSnippet() {
|
||||||
|
var title = titleEl.value.trim() || "LangBot";
|
||||||
|
var src = buildSrc();
|
||||||
|
snippetEl.textContent =
|
||||||
|
'<script data-title="' + title + '" src="' + src + '"><\/script>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(html) { statusEl.innerHTML = html; }
|
||||||
|
|
||||||
|
function removeWidget() {
|
||||||
|
var old = document.getElementById(WIDGET_ID);
|
||||||
|
if (old) old.remove();
|
||||||
|
// The widget injects its own DOM (bubble + panel). Clear the common containers it creates.
|
||||||
|
document.querySelectorAll('[id^="langbot-"]').forEach(function (n) {
|
||||||
|
if (n.id !== WIDGET_ID) n.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWidget() {
|
||||||
|
var uuid = uuidEl.value.trim();
|
||||||
|
var uuidRe = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
||||||
|
if (!uuidRe.test(uuid)) {
|
||||||
|
setStatus('<span class="bad">Enter a valid bot UUID first.</span>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeWidget();
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.id = WIDGET_ID;
|
||||||
|
s.setAttribute("data-title", titleEl.value.trim() || "LangBot");
|
||||||
|
s.src = buildSrc();
|
||||||
|
s.onload = function () {
|
||||||
|
setStatus('<span class="ok">Widget loaded — look bottom-right.</span>');
|
||||||
|
};
|
||||||
|
s.onerror = function () {
|
||||||
|
setStatus('<span class="bad">Failed to load widget.js — check the base URL and that the bot is enabled.</span>');
|
||||||
|
};
|
||||||
|
document.body.appendChild(s);
|
||||||
|
setStatus("Loading…");
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#load").onclick = loadWidget;
|
||||||
|
$("#unload").onclick = function () {
|
||||||
|
removeWidget();
|
||||||
|
setStatus("Widget removed.");
|
||||||
|
};
|
||||||
|
$("#copy").onclick = function () {
|
||||||
|
navigator.clipboard.writeText(snippetEl.textContent).then(function () {
|
||||||
|
var b = $("#copy"); b.textContent = "Copied"; setTimeout(function () { b.textContent = "Copy"; }, 1200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
[baseEl, uuidEl, titleEl].forEach(function (el) { el.addEventListener("input", refreshSnippet); });
|
||||||
|
refreshSnippet();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from v1 import client # type: ignore
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class TestDifyClient:
|
|
||||||
async def test_chat_messages(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
async for chunk in cln.chat_messages(inputs={}, query='调用工具查看现在几点?', user='test'):
|
|
||||||
print(json.dumps(chunk, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_upload_file(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
file_bytes = open('img.png', 'rb').read()
|
|
||||||
|
|
||||||
print(type(file_bytes))
|
|
||||||
|
|
||||||
file = ('img2.png', file_bytes, 'image/png')
|
|
||||||
|
|
||||||
resp = await cln.upload_file(file=file, user='test')
|
|
||||||
print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
async def test_workflow_run(self):
|
|
||||||
cln = client.AsyncDifyServiceClient(api_key=os.getenv('DIFY_API_KEY'), base_url=os.getenv('DIFY_BASE_URL'))
|
|
||||||
|
|
||||||
# resp = await cln.workflow_run(inputs={}, user="test")
|
|
||||||
# # print(json.dumps(resp, ensure_ascii=False, indent=4))
|
|
||||||
# print(resp)
|
|
||||||
chunks = []
|
|
||||||
|
|
||||||
ignored_events = ['text_chunk']
|
|
||||||
async for chunk in cln.workflow_run(inputs={}, user='test'):
|
|
||||||
if chunk['event'] in ignored_events:
|
|
||||||
continue
|
|
||||||
chunks.append(chunk)
|
|
||||||
print(json.dumps(chunks, ensure_ascii=False, indent=4))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.run(TestDifyClient().test_chat_messages())
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import time
|
|
||||||
from quart import request
|
|
||||||
import httpx
|
|
||||||
from quart import Quart
|
|
||||||
from typing import Callable, Dict, Any
|
|
||||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
|
||||||
from .qqofficialevent import QQOfficialEvent
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
||||||
|
|
||||||
|
|
||||||
def handle_validation(body: dict, bot_secret: str):
|
|
||||||
# bot正确的secert是32位的,此处仅为了适配演示demo
|
|
||||||
while len(bot_secret) < 32:
|
|
||||||
bot_secret = bot_secret * 2
|
|
||||||
bot_secret = bot_secret[:32]
|
|
||||||
# 实际使用场景中以上三行内容可清除
|
|
||||||
|
|
||||||
seed_bytes = bot_secret.encode()
|
|
||||||
|
|
||||||
signing_key = ed25519.Ed25519PrivateKey.from_private_bytes(seed_bytes)
|
|
||||||
|
|
||||||
msg = body['d']['event_ts'] + body['d']['plain_token']
|
|
||||||
msg_bytes = msg.encode()
|
|
||||||
|
|
||||||
signature = signing_key.sign(msg_bytes)
|
|
||||||
|
|
||||||
signature_hex = signature.hex()
|
|
||||||
|
|
||||||
response = {'plain_token': body['d']['plain_token'], 'signature': signature_hex}
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialClient:
|
|
||||||
def __init__(self, secret: str, token: str, app_id: str, logger: None):
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['GET', 'POST'],
|
|
||||||
)
|
|
||||||
self.secret = secret
|
|
||||||
self.token = token
|
|
||||||
self.app_id = app_id
|
|
||||||
self._message_handlers = {}
|
|
||||||
self.base_url = 'https://api.sgroup.qq.com'
|
|
||||||
self.access_token = ''
|
|
||||||
self.access_token_expiry_time = None
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
async def check_access_token(self):
|
|
||||||
"""检查access_token是否存在"""
|
|
||||||
if not self.access_token or await self.is_token_expired():
|
|
||||||
return False
|
|
||||||
return bool(self.access_token and self.access_token.strip())
|
|
||||||
|
|
||||||
async def get_access_token(self):
|
|
||||||
"""获取access_token"""
|
|
||||||
url = 'https://bots.qq.com/app/getAppAccessToken'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
params = {
|
|
||||||
'appId': self.app_id,
|
|
||||||
'clientSecret': self.secret,
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
response = await client.post(url, json=params, headers=headers)
|
|
||||||
if response.status_code == 200:
|
|
||||||
response_data = response.json()
|
|
||||||
access_token = response_data.get('access_token')
|
|
||||||
expires_in = int(response_data.get('expires_in', 7200))
|
|
||||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
|
||||||
if access_token:
|
|
||||||
self.access_token = access_token
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'获取access_token失败: {response_data}')
|
|
||||||
raise Exception(f'获取access_token失败: {e}')
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
"""处理回调请求"""
|
|
||||||
try:
|
|
||||||
# 读取请求数据
|
|
||||||
body = await request.get_data()
|
|
||||||
payload = json.loads(body)
|
|
||||||
|
|
||||||
# 验证是否为回调验证请求
|
|
||||||
if payload.get('op') == 13:
|
|
||||||
# 生成签名
|
|
||||||
response = handle_validation(payload, self.secret)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
if payload.get('op') == 0:
|
|
||||||
message_data = await self.get_message(payload)
|
|
||||||
if message_data:
|
|
||||||
event = QQOfficialEvent.from_payload(message_data)
|
|
||||||
await self._handle_message(event)
|
|
||||||
|
|
||||||
return {'code': 0, 'message': 'success'}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
|
||||||
return {'error': str(e)}, 400
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""启动 Quart 应用"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
"""注册消息类型处理器"""
|
|
||||||
|
|
||||||
def decorator(func: Callable[[platform_events.Event], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def _handle_message(self, event: QQOfficialEvent):
|
|
||||||
"""处理消息事件"""
|
|
||||||
msg_type = event.t
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
|
|
||||||
async def get_message(self, msg: dict) -> Dict[str, Any]:
|
|
||||||
"""获取消息"""
|
|
||||||
message_data = {
|
|
||||||
't': msg.get('t', {}),
|
|
||||||
'user_openid': msg.get('d', {}).get('author', {}).get('user_openid', {}),
|
|
||||||
'timestamp': msg.get('d', {}).get('timestamp', {}),
|
|
||||||
'd_author_id': msg.get('d', {}).get('author', {}).get('id', {}),
|
|
||||||
'content': msg.get('d', {}).get('content', {}),
|
|
||||||
'd_id': msg.get('d', {}).get('id', {}),
|
|
||||||
'id': msg.get('id', {}),
|
|
||||||
'channel_id': msg.get('d', {}).get('channel_id', {}),
|
|
||||||
'username': msg.get('d', {}).get('author', {}).get('username', {}),
|
|
||||||
'guild_id': msg.get('d', {}).get('guild_id', {}),
|
|
||||||
'member_openid': msg.get('d', {}).get('author', {}).get('openid', {}),
|
|
||||||
'group_openid': msg.get('d', {}).get('group_openid', {}),
|
|
||||||
}
|
|
||||||
attachments = msg.get('d', {}).get('attachments', [])
|
|
||||||
image_attachments = [attachment['url'] for attachment in attachments if await self.is_image(attachment)]
|
|
||||||
image_attachments_type = [
|
|
||||||
attachment['content_type'] for attachment in attachments if await self.is_image(attachment)
|
|
||||||
]
|
|
||||||
if image_attachments:
|
|
||||||
message_data['image_attachments'] = image_attachments[0]
|
|
||||||
message_data['content_type'] = image_attachments_type[0]
|
|
||||||
else:
|
|
||||||
message_data['image_attachments'] = None
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def is_image(self, attachment: dict) -> bool:
|
|
||||||
"""判断是否为图片附件"""
|
|
||||||
content_type = attachment.get('content_type', '')
|
|
||||||
return content_type.startswith('image/')
|
|
||||||
|
|
||||||
async def send_private_text_msg(self, user_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/users/' + user_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
response_data = response.json()
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送私聊消息失败: {response_data}')
|
|
||||||
raise ValueError(response)
|
|
||||||
|
|
||||||
async def send_group_text_msg(self, group_openid: str, content: str, msg_id: str):
|
|
||||||
"""发送群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/v2/groups/' + group_openid + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
data = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送群聊消息失败:{response.json()}')
|
|
||||||
raise Exception(response.read().decode())
|
|
||||||
|
|
||||||
async def send_channle_group_text_msg(self, channel_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道群聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/channels/' + channel_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道群聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def send_channle_private_text_msg(self, guild_id: str, content: str, msg_id: str):
|
|
||||||
"""发送频道私聊消息"""
|
|
||||||
if not await self.check_access_token():
|
|
||||||
await self.get_access_token()
|
|
||||||
|
|
||||||
url = self.base_url + '/dms/' + guild_id + '/messages'
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'QQBot {self.access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'content': content,
|
|
||||||
'msg_type': 0,
|
|
||||||
'msg_id': msg_id,
|
|
||||||
}
|
|
||||||
response = await client.post(url, headers=headers, json=params)
|
|
||||||
if response.status_code == 200:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
await self.logger.error(f'发送频道私聊消息失败: {response.json()}')
|
|
||||||
raise Exception(response)
|
|
||||||
|
|
||||||
async def is_token_expired(self):
|
|
||||||
"""检查token是否过期"""
|
|
||||||
if self.access_token_expiry_time is None:
|
|
||||||
return True
|
|
||||||
return time.time() > self.access_token_expiry_time
|
|
||||||
@@ -1,588 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
import uuid
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any, Callable, Optional
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from quart import Quart, request, Response, jsonify
|
|
||||||
|
|
||||||
from libs.wecom_ai_bot_api import wecombotevent
|
|
||||||
from libs.wecom_ai_bot_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
|
||||||
from pkg.platform.logger import EventLogger
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StreamChunk:
|
|
||||||
"""描述单次推送给企业微信的流式片段。"""
|
|
||||||
|
|
||||||
# 需要返回给企业微信的文本内容
|
|
||||||
content: str
|
|
||||||
|
|
||||||
# 标记是否为最终片段,对应企业微信协议里的 finish 字段
|
|
||||||
is_final: bool = False
|
|
||||||
|
|
||||||
# 预留额外元信息,未来支持多模态扩展时可使用
|
|
||||||
meta: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StreamSession:
|
|
||||||
"""维护一次企业微信流式会话的上下文。"""
|
|
||||||
|
|
||||||
# 企业微信要求的 stream_id,用于标识后续刷新请求
|
|
||||||
stream_id: str
|
|
||||||
|
|
||||||
# 原始消息的 msgid,便于与流水线消息对应
|
|
||||||
msg_id: str
|
|
||||||
|
|
||||||
# 群聊会话标识(单聊时为空)
|
|
||||||
chat_id: Optional[str]
|
|
||||||
|
|
||||||
# 触发消息的发送者
|
|
||||||
user_id: Optional[str]
|
|
||||||
|
|
||||||
# 会话创建时间
|
|
||||||
created_at: float = field(default_factory=time.time)
|
|
||||||
|
|
||||||
# 最近一次被访问的时间,cleanup 依据该值判断过期
|
|
||||||
last_access: float = field(default_factory=time.time)
|
|
||||||
|
|
||||||
# 将流水线增量结果缓存到队列,刷新请求逐条消费
|
|
||||||
queue: asyncio.Queue = field(default_factory=asyncio.Queue)
|
|
||||||
|
|
||||||
# 是否已经完成(收到最终片段)
|
|
||||||
finished: bool = False
|
|
||||||
|
|
||||||
# 缓存最近一次片段,处理重试或超时兜底
|
|
||||||
last_chunk: Optional[StreamChunk] = None
|
|
||||||
|
|
||||||
|
|
||||||
class StreamSessionManager:
|
|
||||||
"""管理 stream 会话的生命周期,并负责队列的生产消费。"""
|
|
||||||
|
|
||||||
def __init__(self, logger: EventLogger, ttl: int = 60) -> None:
|
|
||||||
self.logger = logger
|
|
||||||
|
|
||||||
self.ttl = ttl # 超时时间(秒),超过该时间未被访问的会话会被清理由 cleanup
|
|
||||||
self._sessions: dict[str, StreamSession] = {} # stream_id -> StreamSession 映射
|
|
||||||
self._msg_index: dict[str, str] = {} # msgid -> stream_id 映射,便于流水线根据消息 ID 找到会话
|
|
||||||
|
|
||||||
def get_stream_id_by_msg(self, msg_id: str) -> Optional[str]:
|
|
||||||
if not msg_id:
|
|
||||||
return None
|
|
||||||
return self._msg_index.get(msg_id)
|
|
||||||
|
|
||||||
def get_session(self, stream_id: str) -> Optional[StreamSession]:
|
|
||||||
return self._sessions.get(stream_id)
|
|
||||||
|
|
||||||
def create_or_get(self, msg_json: dict[str, Any]) -> tuple[StreamSession, bool]:
|
|
||||||
"""根据企业微信回调创建或获取会话。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_json: 企业微信解密后的回调 JSON。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[StreamSession, bool]: `StreamSession` 为会话实例,`bool` 指示是否为新建会话。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在首次回调中调用,得到 `is_new=True` 后再触发流水线。
|
|
||||||
"""
|
|
||||||
msg_id = msg_json.get('msgid', '')
|
|
||||||
if msg_id and msg_id in self._msg_index:
|
|
||||||
stream_id = self._msg_index[msg_id]
|
|
||||||
session = self._sessions.get(stream_id)
|
|
||||||
if session:
|
|
||||||
session.last_access = time.time()
|
|
||||||
return session, False
|
|
||||||
|
|
||||||
stream_id = str(uuid.uuid4())
|
|
||||||
session = StreamSession(
|
|
||||||
stream_id=stream_id,
|
|
||||||
msg_id=msg_id,
|
|
||||||
chat_id=msg_json.get('chatid'),
|
|
||||||
user_id=msg_json.get('from', {}).get('userid'),
|
|
||||||
)
|
|
||||||
|
|
||||||
if msg_id:
|
|
||||||
self._msg_index[msg_id] = stream_id
|
|
||||||
self._sessions[stream_id] = session
|
|
||||||
return session, True
|
|
||||||
|
|
||||||
async def publish(self, stream_id: str, chunk: StreamChunk) -> bool:
|
|
||||||
"""向 stream 队列写入新的增量片段。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stream_id: 企业微信分配的流式会话 ID。
|
|
||||||
chunk: 待发送的增量片段。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 当流式队列存在并成功入队时返回 True。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在收到模型增量后调用 `await manager.publish('sid', StreamChunk('hello'))`。
|
|
||||||
"""
|
|
||||||
session = self._sessions.get(stream_id)
|
|
||||||
if not session:
|
|
||||||
return False
|
|
||||||
|
|
||||||
session.last_access = time.time()
|
|
||||||
session.last_chunk = chunk
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.queue.put_nowait(chunk)
|
|
||||||
except asyncio.QueueFull:
|
|
||||||
# 默认无界队列,此处兜底防御
|
|
||||||
await session.queue.put(chunk)
|
|
||||||
|
|
||||||
if chunk.is_final:
|
|
||||||
session.finished = True
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def consume(self, stream_id: str, timeout: float = 0.5) -> Optional[StreamChunk]:
|
|
||||||
"""从队列中取出一个片段,若超时返回 None。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stream_id: 企业微信流式会话 ID。
|
|
||||||
timeout: 取片段的最长等待时间(秒)。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[StreamChunk]: 成功时返回片段,超时或会话不存在时返回 None。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
企业微信刷新到达时调用,若队列有数据则立即返回 `StreamChunk`。
|
|
||||||
"""
|
|
||||||
session = self._sessions.get(stream_id)
|
|
||||||
if not session:
|
|
||||||
return None
|
|
||||||
|
|
||||||
session.last_access = time.time()
|
|
||||||
|
|
||||||
try:
|
|
||||||
chunk = await asyncio.wait_for(session.queue.get(), timeout)
|
|
||||||
session.last_access = time.time()
|
|
||||||
if chunk.is_final:
|
|
||||||
session.finished = True
|
|
||||||
return chunk
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
if session.finished and session.last_chunk:
|
|
||||||
return session.last_chunk
|
|
||||||
return None
|
|
||||||
|
|
||||||
def mark_finished(self, stream_id: str) -> None:
|
|
||||||
session = self._sessions.get(stream_id)
|
|
||||||
if session:
|
|
||||||
session.finished = True
|
|
||||||
session.last_access = time.time()
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
|
||||||
"""定期清理过期会话,防止队列与映射无上限累积。"""
|
|
||||||
now = time.time()
|
|
||||||
expired: list[str] = []
|
|
||||||
for stream_id, session in self._sessions.items():
|
|
||||||
if now - session.last_access > self.ttl:
|
|
||||||
expired.append(stream_id)
|
|
||||||
|
|
||||||
for stream_id in expired:
|
|
||||||
session = self._sessions.pop(stream_id, None)
|
|
||||||
if not session:
|
|
||||||
continue
|
|
||||||
msg_id = session.msg_id
|
|
||||||
if msg_id and self._msg_index.get(msg_id) == stream_id:
|
|
||||||
self._msg_index.pop(msg_id, None)
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotClient:
|
|
||||||
def __init__(self, Token: str, EnCodingAESKey: str, Corpid: str, logger: EventLogger):
|
|
||||||
"""企业微信智能机器人客户端。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
Token: 企业微信回调验证使用的 token。
|
|
||||||
EnCodingAESKey: 企业微信消息加解密密钥。
|
|
||||||
Corpid: 企业 ID。
|
|
||||||
logger: 日志记录器。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> client = WecomBotClient(Token='token', EnCodingAESKey='aeskey', Corpid='corp', logger=logger)
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.Token = Token
|
|
||||||
self.EnCodingAESKey = EnCodingAESKey
|
|
||||||
self.Corpid = Corpid
|
|
||||||
self.ReceiveId = ''
|
|
||||||
self.app = Quart(__name__)
|
|
||||||
self.app.add_url_rule(
|
|
||||||
'/callback/command',
|
|
||||||
'handle_callback',
|
|
||||||
self.handle_callback_request,
|
|
||||||
methods=['POST', 'GET']
|
|
||||||
)
|
|
||||||
self._message_handlers = {
|
|
||||||
'example': [],
|
|
||||||
}
|
|
||||||
self.logger = logger
|
|
||||||
self.generated_content: dict[str, str] = {}
|
|
||||||
self.msg_id_map: dict[str, int] = {}
|
|
||||||
self.stream_sessions = StreamSessionManager(logger=logger)
|
|
||||||
self.stream_poll_timeout = 0.5
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_stream_payload(stream_id: str, content: str, finish: bool) -> dict[str, Any]:
|
|
||||||
"""按照企业微信协议拼装返回报文。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stream_id: 企业微信会话 ID。
|
|
||||||
content: 推送的文本内容。
|
|
||||||
finish: 是否为最终片段。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict[str, Any]: 可直接加密返回的 payload。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
组装 `{'msgtype': 'stream', 'stream': {'id': 'sid', ...}}` 结构。
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'msgtype': 'stream',
|
|
||||||
'stream': {
|
|
||||||
'id': stream_id,
|
|
||||||
'finish': finish,
|
|
||||||
'content': content,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
async def _encrypt_and_reply(self, payload: dict[str, Any], nonce: str) -> tuple[Response, int]:
|
|
||||||
"""对响应进行加密封装并返回给企业微信。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
payload: 待加密的响应内容。
|
|
||||||
nonce: 企业微信回调参数中的 nonce。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[Response, int]: Quart Response 对象及状态码。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在首包或刷新场景中调用以生成加密响应。
|
|
||||||
"""
|
|
||||||
reply_plain_str = json.dumps(payload, ensure_ascii=False)
|
|
||||||
reply_timestamp = str(int(time.time()))
|
|
||||||
ret, encrypt_text = self.wxcpt.EncryptMsg(reply_plain_str, nonce, reply_timestamp)
|
|
||||||
if ret != 0:
|
|
||||||
await self.logger.error(f'加密失败: {ret}')
|
|
||||||
return jsonify({'error': 'encrypt_failed'}), 500
|
|
||||||
|
|
||||||
root = ET.fromstring(encrypt_text)
|
|
||||||
encrypt = root.find('Encrypt').text
|
|
||||||
resp = {
|
|
||||||
'encrypt': encrypt,
|
|
||||||
}
|
|
||||||
return jsonify(resp), 200
|
|
||||||
|
|
||||||
async def _dispatch_event(self, event: wecombotevent.WecomBotEvent) -> None:
|
|
||||||
"""异步触发流水线处理,避免阻塞首包响应。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event: 由企业微信消息转换的内部事件对象。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await self._handle_message(event)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
async def _handle_post_initial_response(self, msg_json: dict[str, Any], nonce: str) -> tuple[Response, int]:
|
|
||||||
"""处理企业微信首次推送的消息,返回 stream_id 并开启流水线。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_json: 解密后的企业微信消息 JSON。
|
|
||||||
nonce: 企业微信回调参数 nonce。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[Response, int]: Quart Response 及状态码。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
首次回调时调用,立即返回带 `stream_id` 的响应。
|
|
||||||
"""
|
|
||||||
session, is_new = self.stream_sessions.create_or_get(msg_json)
|
|
||||||
|
|
||||||
message_data = await self.get_message(msg_json)
|
|
||||||
if message_data:
|
|
||||||
message_data['stream_id'] = session.stream_id
|
|
||||||
try:
|
|
||||||
event = wecombotevent.WecomBotEvent(message_data)
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
else:
|
|
||||||
if is_new:
|
|
||||||
asyncio.create_task(self._dispatch_event(event))
|
|
||||||
|
|
||||||
payload = self._build_stream_payload(session.stream_id, '', False)
|
|
||||||
return await self._encrypt_and_reply(payload, nonce)
|
|
||||||
|
|
||||||
async def _handle_post_followup_response(self, msg_json: dict[str, Any], nonce: str) -> tuple[Response, int]:
|
|
||||||
"""处理企业微信的流式刷新请求,按需返回增量片段。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_json: 解密后的企业微信刷新请求。
|
|
||||||
nonce: 企业微信回调参数 nonce。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[Response, int]: Quart Response 及状态码。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在刷新请求中调用,按需返回增量片段。
|
|
||||||
"""
|
|
||||||
stream_info = msg_json.get('stream', {})
|
|
||||||
stream_id = stream_info.get('id', '')
|
|
||||||
if not stream_id:
|
|
||||||
await self.logger.error('刷新请求缺少 stream.id')
|
|
||||||
return await self._encrypt_and_reply(self._build_stream_payload('', '', True), nonce)
|
|
||||||
|
|
||||||
session = self.stream_sessions.get_session(stream_id)
|
|
||||||
chunk = await self.stream_sessions.consume(stream_id, timeout=self.stream_poll_timeout)
|
|
||||||
|
|
||||||
if not chunk:
|
|
||||||
cached_content = None
|
|
||||||
if session and session.msg_id:
|
|
||||||
cached_content = self.generated_content.pop(session.msg_id, None)
|
|
||||||
if cached_content is not None:
|
|
||||||
chunk = StreamChunk(content=cached_content, is_final=True)
|
|
||||||
else:
|
|
||||||
payload = self._build_stream_payload(stream_id, '', False)
|
|
||||||
return await self._encrypt_and_reply(payload, nonce)
|
|
||||||
|
|
||||||
payload = self._build_stream_payload(stream_id, chunk.content, chunk.is_final)
|
|
||||||
if chunk.is_final:
|
|
||||||
self.stream_sessions.mark_finished(stream_id)
|
|
||||||
return await self._encrypt_and_reply(payload, nonce)
|
|
||||||
|
|
||||||
async def handle_callback_request(self):
|
|
||||||
"""企业微信回调入口。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Quart Response: 根据请求类型返回验证、首包或刷新结果。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
作为 Quart 路由处理函数直接注册并使用。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.wxcpt = WXBizMsgCrypt(self.Token, self.EnCodingAESKey, '')
|
|
||||||
await self.logger.info(f'{request.method} {request.url} {str(request.args)}')
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return await self._handle_get_callback()
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
return await self._handle_post_callback()
|
|
||||||
|
|
||||||
return Response('', status=405)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
await self.logger.error(traceback.format_exc())
|
|
||||||
return Response('Internal Server Error', status=500)
|
|
||||||
|
|
||||||
async def _handle_get_callback(self) -> tuple[Response, int] | Response:
|
|
||||||
"""处理企业微信的 GET 验证请求。"""
|
|
||||||
|
|
||||||
msg_signature = unquote(request.args.get('msg_signature', ''))
|
|
||||||
timestamp = unquote(request.args.get('timestamp', ''))
|
|
||||||
nonce = unquote(request.args.get('nonce', ''))
|
|
||||||
echostr = unquote(request.args.get('echostr', ''))
|
|
||||||
|
|
||||||
if not all([msg_signature, timestamp, nonce, echostr]):
|
|
||||||
await self.logger.error('请求参数缺失')
|
|
||||||
return Response('缺少参数', status=400)
|
|
||||||
|
|
||||||
ret, decrypted_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
|
||||||
if ret != 0:
|
|
||||||
await self.logger.error('验证URL失败')
|
|
||||||
return Response('验证失败', status=403)
|
|
||||||
|
|
||||||
return Response(decrypted_str, mimetype='text/plain')
|
|
||||||
|
|
||||||
async def _handle_post_callback(self) -> tuple[Response, int] | Response:
|
|
||||||
"""处理企业微信的 POST 回调请求。"""
|
|
||||||
|
|
||||||
self.stream_sessions.cleanup()
|
|
||||||
|
|
||||||
msg_signature = unquote(request.args.get('msg_signature', ''))
|
|
||||||
timestamp = unquote(request.args.get('timestamp', ''))
|
|
||||||
nonce = unquote(request.args.get('nonce', ''))
|
|
||||||
|
|
||||||
encrypted_json = await request.get_json()
|
|
||||||
encrypted_msg = (encrypted_json or {}).get('encrypt', '')
|
|
||||||
if not encrypted_msg:
|
|
||||||
await self.logger.error("请求体中缺少 'encrypt' 字段")
|
|
||||||
return Response('Bad Request', status=400)
|
|
||||||
|
|
||||||
xml_post_data = f"<xml><Encrypt><![CDATA[{encrypted_msg}]]></Encrypt></xml>"
|
|
||||||
ret, decrypted_xml = self.wxcpt.DecryptMsg(xml_post_data, msg_signature, timestamp, nonce)
|
|
||||||
if ret != 0:
|
|
||||||
await self.logger.error('解密失败')
|
|
||||||
return Response('解密失败', status=400)
|
|
||||||
|
|
||||||
msg_json = json.loads(decrypted_xml)
|
|
||||||
|
|
||||||
if msg_json.get('msgtype') == 'stream':
|
|
||||||
return await self._handle_post_followup_response(msg_json, nonce)
|
|
||||||
|
|
||||||
return await self._handle_post_initial_response(msg_json, nonce)
|
|
||||||
|
|
||||||
async def get_message(self, msg_json):
|
|
||||||
message_data = {}
|
|
||||||
|
|
||||||
if msg_json.get('chattype', '') == 'single':
|
|
||||||
message_data['type'] = 'single'
|
|
||||||
elif msg_json.get('chattype', '') == 'group':
|
|
||||||
message_data['type'] = 'group'
|
|
||||||
|
|
||||||
if msg_json.get('msgtype') == 'text':
|
|
||||||
message_data['content'] = msg_json.get('text', {}).get('content')
|
|
||||||
elif msg_json.get('msgtype') == 'image':
|
|
||||||
picurl = msg_json.get('image', {}).get('url', '')
|
|
||||||
base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64
|
|
||||||
elif msg_json.get('msgtype') == 'mixed':
|
|
||||||
items = msg_json.get('mixed', {}).get('msg_item', [])
|
|
||||||
texts = []
|
|
||||||
picurl = None
|
|
||||||
for item in items:
|
|
||||||
if item.get('msgtype') == 'text':
|
|
||||||
texts.append(item.get('text', {}).get('content', ''))
|
|
||||||
elif item.get('msgtype') == 'image' and picurl is None:
|
|
||||||
picurl = item.get('image', {}).get('url')
|
|
||||||
|
|
||||||
if texts:
|
|
||||||
message_data['content'] = "".join(texts) # 拼接所有 text
|
|
||||||
if picurl:
|
|
||||||
base64 = await self.download_url_to_base64(picurl, self.EnCodingAESKey)
|
|
||||||
message_data['picurl'] = base64 # 只保留第一个 image
|
|
||||||
|
|
||||||
# Extract user information
|
|
||||||
from_info = msg_json.get('from', {})
|
|
||||||
message_data['userid'] = from_info.get('userid', '')
|
|
||||||
message_data['username'] = from_info.get('alias', '') or from_info.get('name', '') or from_info.get('userid', '')
|
|
||||||
|
|
||||||
# Extract chat/group information
|
|
||||||
if msg_json.get('chattype', '') == 'group':
|
|
||||||
message_data['chatid'] = msg_json.get('chatid', '')
|
|
||||||
# Try to get group name if available
|
|
||||||
message_data['chatname'] = msg_json.get('chatname', '') or msg_json.get('chatid', '')
|
|
||||||
|
|
||||||
message_data['msgid'] = msg_json.get('msgid', '')
|
|
||||||
|
|
||||||
if msg_json.get('aibotid'):
|
|
||||||
message_data['aibotid'] = msg_json.get('aibotid', '')
|
|
||||||
|
|
||||||
return message_data
|
|
||||||
|
|
||||||
async def _handle_message(self, event: wecombotevent.WecomBotEvent):
|
|
||||||
"""
|
|
||||||
处理消息事件。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
message_id = event.message_id
|
|
||||||
if message_id in self.msg_id_map.keys():
|
|
||||||
self.msg_id_map[message_id] += 1
|
|
||||||
return
|
|
||||||
self.msg_id_map[message_id] = 1
|
|
||||||
msg_type = event.type
|
|
||||||
if msg_type in self._message_handlers:
|
|
||||||
for handler in self._message_handlers[msg_type]:
|
|
||||||
await handler(event)
|
|
||||||
except Exception:
|
|
||||||
print(traceback.format_exc())
|
|
||||||
|
|
||||||
async def push_stream_chunk(self, msg_id: str, content: str, is_final: bool = False) -> bool:
|
|
||||||
"""将流水线片段推送到 stream 会话。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_id: 原始企业微信消息 ID。
|
|
||||||
content: 模型产生的片段内容。
|
|
||||||
is_final: 是否为最终片段。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 当成功写入流式队列时返回 True。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在流水线 `reply_message_chunk` 中调用,将增量推送至企业微信。
|
|
||||||
"""
|
|
||||||
# 根据 msg_id 找到对应 stream 会话,如果不存在说明当前消息非流式
|
|
||||||
stream_id = self.stream_sessions.get_stream_id_by_msg(msg_id)
|
|
||||||
if not stream_id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
chunk = StreamChunk(content=content, is_final=is_final)
|
|
||||||
await self.stream_sessions.publish(stream_id, chunk)
|
|
||||||
if is_final:
|
|
||||||
self.stream_sessions.mark_finished(stream_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def set_message(self, msg_id: str, content: str):
|
|
||||||
"""兼容旧逻辑:若无法流式返回则缓存最终结果。
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg_id: 企业微信消息 ID。
|
|
||||||
content: 最终回复的文本内容。
|
|
||||||
|
|
||||||
Example:
|
|
||||||
在非流式场景下缓存最终结果以备刷新时返回。
|
|
||||||
"""
|
|
||||||
handled = await self.push_stream_chunk(msg_id, content, is_final=True)
|
|
||||||
if not handled:
|
|
||||||
self.generated_content[msg_id] = content
|
|
||||||
|
|
||||||
def on_message(self, msg_type: str):
|
|
||||||
def decorator(func: Callable[[wecombotevent.WecomBotEvent], None]):
|
|
||||||
if msg_type not in self._message_handlers:
|
|
||||||
self._message_handlers[msg_type] = []
|
|
||||||
self._message_handlers[msg_type].append(func)
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def download_url_to_base64(self, download_url, encoding_aes_key):
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(download_url)
|
|
||||||
if response.status_code != 200:
|
|
||||||
await self.logger.error(f'failed to get file: {response.text}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
encrypted_bytes = response.content
|
|
||||||
|
|
||||||
aes_key = base64.b64decode(encoding_aes_key + "=") # base64 补齐
|
|
||||||
iv = aes_key[:16]
|
|
||||||
|
|
||||||
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
|
|
||||||
decrypted = cipher.decrypt(encrypted_bytes)
|
|
||||||
|
|
||||||
pad_len = decrypted[-1]
|
|
||||||
decrypted = decrypted[:-pad_len]
|
|
||||||
|
|
||||||
if decrypted.startswith(b"\xff\xd8"): # JPEG
|
|
||||||
mime_type = "image/jpeg"
|
|
||||||
elif decrypted.startswith(b"\x89PNG"): # PNG
|
|
||||||
mime_type = "image/png"
|
|
||||||
elif decrypted.startswith((b"GIF87a", b"GIF89a")): # GIF
|
|
||||||
mime_type = "image/gif"
|
|
||||||
elif decrypted.startswith(b"BM"): # BMP
|
|
||||||
mime_type = "image/bmp"
|
|
||||||
elif decrypted.startswith(b"II*\x00") or decrypted.startswith(b"MM\x00*"): # TIFF
|
|
||||||
mime_type = "image/tiff"
|
|
||||||
else:
|
|
||||||
mime_type = "application/octet-stream"
|
|
||||||
|
|
||||||
# 转 base64
|
|
||||||
base64_str = base64.b64encode(decrypted).decode("utf-8")
|
|
||||||
return f"data:{mime_type};base64,{base64_str}"
|
|
||||||
|
|
||||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
启动 Quart 应用。
|
|
||||||
"""
|
|
||||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
from typing import Dict, Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class WecomBotEvent(dict):
|
|
||||||
@staticmethod
|
|
||||||
def from_payload(payload: Dict[str, Any]) -> Optional['WecomBotEvent']:
|
|
||||||
try:
|
|
||||||
event = WecomBotEvent(payload)
|
|
||||||
return event
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self) -> str:
|
|
||||||
"""
|
|
||||||
事件类型
|
|
||||||
"""
|
|
||||||
return self.get('type', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def userid(self) -> str:
|
|
||||||
"""
|
|
||||||
用户id
|
|
||||||
"""
|
|
||||||
return self.get('from', {}).get('userid', '') or self.get('userid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def username(self) -> str:
|
|
||||||
"""
|
|
||||||
用户名称
|
|
||||||
"""
|
|
||||||
return self.get('username', '') or self.get('from', {}).get('alias', '') or self.get('from', {}).get('name', '') or self.userid
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chatname(self) -> str:
|
|
||||||
"""
|
|
||||||
群组名称
|
|
||||||
"""
|
|
||||||
return self.get('chatname', '') or str(self.chatid)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content(self) -> str:
|
|
||||||
"""
|
|
||||||
内容
|
|
||||||
"""
|
|
||||||
return self.get('content', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def picurl(self) -> str:
|
|
||||||
"""
|
|
||||||
图片url
|
|
||||||
"""
|
|
||||||
return self.get('picurl', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chatid(self) -> str:
|
|
||||||
"""
|
|
||||||
群组id
|
|
||||||
"""
|
|
||||||
return self.get('chatid', {})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def message_id(self) -> str:
|
|
||||||
"""
|
|
||||||
消息id
|
|
||||||
"""
|
|
||||||
return self.get('msgid', '')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ai_bot_id(self) -> str:
|
|
||||||
"""
|
|
||||||
AI Bot ID
|
|
||||||
"""
|
|
||||||
return self.get('aibotid', '')
|
|
||||||
@@ -1,117 +1,3 @@
|
|||||||
import asyncio
|
import langbot.__main__
|
||||||
import argparse
|
|
||||||
# LangBot 终端启动入口
|
|
||||||
# 在此层级解决依赖项检查。
|
|
||||||
# LangBot/main.py
|
|
||||||
|
|
||||||
asciiart = r"""
|
langbot.__main__.main()
|
||||||
_ ___ _
|
|
||||||
| | __ _ _ _ __ _| _ ) ___| |_
|
|
||||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
|
||||||
|____\__,_|_||_\__, |___/\___/\__|
|
|
||||||
|___/
|
|
||||||
|
|
||||||
⭐️ Open Source 开源地址: https://github.com/langbot-app/LangBot
|
|
||||||
📖 Documentation 文档地址: https://docs.langbot.app
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
|
||||||
parser = argparse.ArgumentParser(description='LangBot')
|
|
||||||
parser.add_argument(
|
|
||||||
'--standalone-runtime',
|
|
||||||
action='store_true',
|
|
||||||
help='Use standalone plugin runtime / 使用独立插件运行时',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
parser.add_argument('--debug', action='store_true', help='Debug mode / 调试模式', default=False)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.standalone_runtime:
|
|
||||||
from pkg.utils import platform
|
|
||||||
|
|
||||||
platform.standalone_runtime = True
|
|
||||||
|
|
||||||
if args.debug:
|
|
||||||
from pkg.utils import constants
|
|
||||||
|
|
||||||
constants.debug_mode = True
|
|
||||||
|
|
||||||
print(asciiart)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 检查依赖
|
|
||||||
|
|
||||||
from pkg.core.bootutils import deps
|
|
||||||
|
|
||||||
missing_deps = await deps.check_deps()
|
|
||||||
|
|
||||||
if missing_deps:
|
|
||||||
print('以下依赖包未安装,将自动安装,请完成后重启程序:')
|
|
||||||
print(
|
|
||||||
'These dependencies are missing, they will be installed automatically, please restart the program after completion:'
|
|
||||||
)
|
|
||||||
for dep in missing_deps:
|
|
||||||
print('-', dep)
|
|
||||||
await deps.install_deps(missing_deps)
|
|
||||||
print('已自动安装缺失的依赖包,请重启程序。')
|
|
||||||
print('The missing dependencies have been installed automatically, please restart the program.')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# # 检查pydantic版本,如果没有 pydantic.v1,则把 pydantic 映射为 v1
|
|
||||||
# import pydantic.version
|
|
||||||
|
|
||||||
# if pydantic.version.VERSION < '2.0':
|
|
||||||
# import pydantic
|
|
||||||
|
|
||||||
# sys.modules['pydantic.v1'] = pydantic
|
|
||||||
|
|
||||||
# 检查配置文件
|
|
||||||
|
|
||||||
from pkg.core.bootutils import files
|
|
||||||
|
|
||||||
generated_files = await files.generate_files()
|
|
||||||
|
|
||||||
if generated_files:
|
|
||||||
print('以下文件不存在,已自动生成:')
|
|
||||||
print('Following files do not exist and have been automatically generated:')
|
|
||||||
for file in generated_files:
|
|
||||||
print('-', file)
|
|
||||||
|
|
||||||
from pkg.core import boot
|
|
||||||
|
|
||||||
await boot.main(loop)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 必须大于 3.10.1
|
|
||||||
if sys.version_info < (3, 10, 1):
|
|
||||||
print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version)
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Your Python version is not supported. Please exit the program by pressing any key.')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# Check if the current directory is the LangBot project root directory
|
|
||||||
invalid_pwd = False
|
|
||||||
|
|
||||||
if not os.path.exists('main.py'):
|
|
||||||
invalid_pwd = True
|
|
||||||
else:
|
|
||||||
with open('main.py', 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
if 'LangBot/main.py' not in content:
|
|
||||||
invalid_pwd = True
|
|
||||||
if invalid_pwd:
|
|
||||||
print('请在 LangBot 项目根目录下以命令形式运行此程序。')
|
|
||||||
input('按任意键退出...')
|
|
||||||
print('Please run this program in the LangBot project root directory in command form.')
|
|
||||||
print('Press any key to exit...')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
|
|
||||||
loop.run_until_complete(main_entry(loop))
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,109 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('webchat', '/api/v1/pipelines/<pipeline_uuid>/chat')
|
|
||||||
class WebChatDebugRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/send', methods=['POST'])
|
|
||||||
async def send_message(pipeline_uuid: str) -> str:
|
|
||||||
"""Send a message to the pipeline for debugging"""
|
|
||||||
|
|
||||||
async def stream_generator(generator):
|
|
||||||
yield 'data: {"type": "start"}\n\n'
|
|
||||||
async for message in generator:
|
|
||||||
yield f'data: {json.dumps({"message": message})}\n\n'
|
|
||||||
yield 'data: {"type": "end"}\n\n'
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = await quart.request.get_json()
|
|
||||||
session_type = data.get('session_type', 'person')
|
|
||||||
message_chain_obj = data.get('message', [])
|
|
||||||
is_stream = data.get('is_stream', False)
|
|
||||||
|
|
||||||
if not message_chain_obj:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
if is_stream:
|
|
||||||
generator = webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj, is_stream
|
|
||||||
)
|
|
||||||
# 设置正确的响应头
|
|
||||||
headers = {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Transfer-Encoding': 'chunked',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
}
|
|
||||||
return quart.Response(stream_generator(generator), mimetype='text/event-stream', headers=headers)
|
|
||||||
|
|
||||||
else: # non-stream
|
|
||||||
result = None
|
|
||||||
async for message in webchat_adapter.send_webchat_message(
|
|
||||||
pipeline_uuid, session_type, message_chain_obj
|
|
||||||
):
|
|
||||||
result = message
|
|
||||||
if result is not None:
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'message': result,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return self.http_status(400, -1, 'message is required')
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/messages/<session_type>', methods=['GET'])
|
|
||||||
async def get_messages(pipeline_uuid: str, session_type: str) -> str:
|
|
||||||
"""Get the message history of the pipeline for debugging"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = self.ap.platform_mgr.webchat_proxy_bot.adapter
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
messages = webchat_adapter.get_webchat_messages(pipeline_uuid, session_type)
|
|
||||||
|
|
||||||
return self.success(data={'messages': messages})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/reset/<session_type>', methods=['POST'])
|
|
||||||
async def reset_session(session_type: str) -> str:
|
|
||||||
"""Reset the debug session"""
|
|
||||||
try:
|
|
||||||
if session_type not in ['person', 'group']:
|
|
||||||
return self.http_status(400, -1, 'session_type must be person or group')
|
|
||||||
|
|
||||||
webchat_adapter = None
|
|
||||||
for bot in self.ap.platform_mgr.bots:
|
|
||||||
if hasattr(bot.adapter, '__class__') and bot.adapter.__class__.__name__ == 'WebChatAdapter':
|
|
||||||
webchat_adapter = bot.adapter
|
|
||||||
break
|
|
||||||
|
|
||||||
if not webchat_adapter:
|
|
||||||
return self.http_status(404, -1, 'WebChat adapter not found')
|
|
||||||
|
|
||||||
webchat_adapter.reset_debug_session(session_type)
|
|
||||||
|
|
||||||
return self.success(data={'message': 'Session reset successfully'})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Internal server error: {str(e)}')
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('adapters', '/api/v1/platform/adapters')
|
|
||||||
class AdaptersRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'])
|
|
||||||
async def _() -> str:
|
|
||||||
return self.success(data={'adapters': self.ap.platform_mgr.get_available_adapters_info()})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>', methods=['GET'])
|
|
||||||
async def _(adapter_name: str) -> str:
|
|
||||||
adapter_info = self.ap.platform_mgr.get_available_adapter_info_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_info is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
return self.success(data={'adapter': adapter_info})
|
|
||||||
|
|
||||||
@self.route('/<adapter_name>/icon', methods=['GET'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _(adapter_name: str) -> quart.Response:
|
|
||||||
adapter_manifest = self.ap.platform_mgr.get_available_adapter_manifest_by_name(adapter_name)
|
|
||||||
|
|
||||||
if adapter_manifest is None:
|
|
||||||
return self.http_status(404, -1, 'adapter not found')
|
|
||||||
|
|
||||||
icon_path = adapter_manifest.icon_rel_path
|
|
||||||
|
|
||||||
if icon_path is None:
|
|
||||||
return self.http_status(404, -1, 'icon not found')
|
|
||||||
|
|
||||||
return await quart.send_file(icon_path)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import quart
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('bots', '/api/v1/platform/bots')
|
|
||||||
class BotsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
|
||||||
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'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
|
||||||
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'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
|
|
||||||
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,309 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import quart
|
|
||||||
import re
|
|
||||||
import httpx
|
|
||||||
import uuid
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .....core import taskmgr
|
|
||||||
from .. import group
|
|
||||||
from langbot_plugin.runtime.plugin.mgr import PluginInstallSource
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('plugins', '/api/v1/plugins')
|
|
||||||
class PluginsRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
plugins = await self.ap.plugin_connector.list_plugins()
|
|
||||||
|
|
||||||
return self.success(data={'plugins': plugins})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/upgrade',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.upgrade_plugin(author, plugin_name, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-upgrade-{plugin_name}',
|
|
||||||
label=f'Upgrading plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>',
|
|
||||||
methods=['GET', 'DELETE'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
return self.success(data={'plugin': plugin})
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
delete_data = quart.request.args.get('delete_data', 'false').lower() == 'true'
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.delete_plugin(
|
|
||||||
author, plugin_name, delete_data=delete_data, task_context=ctx
|
|
||||||
),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name=f'plugin-remove-{plugin_name}',
|
|
||||||
label=f'Removing plugin {plugin_name}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/config',
|
|
||||||
methods=['GET', 'PUT'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
plugin = await self.ap.plugin_connector.get_plugin_info(author, plugin_name)
|
|
||||||
if plugin is None:
|
|
||||||
return self.http_status(404, -1, 'plugin not found')
|
|
||||||
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'config': plugin['plugin_config']})
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
await self.ap.plugin_connector.set_plugin_config(author, plugin_name, data)
|
|
||||||
|
|
||||||
return self.success(data={})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/<author>/<plugin_name>/icon',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.NONE,
|
|
||||||
)
|
|
||||||
async def _(author: str, plugin_name: str) -> quart.Response:
|
|
||||||
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
|
|
||||||
icon_base64 = icon_data['plugin_icon_base64']
|
|
||||||
mime_type = icon_data['mime_type']
|
|
||||||
|
|
||||||
icon_data = base64.b64decode(icon_base64)
|
|
||||||
|
|
||||||
return quart.Response(icon_data, mimetype=mime_type)
|
|
||||||
|
|
||||||
@self.route('/github/releases', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
"""Get releases from a GitHub repository URL"""
|
|
||||||
data = await quart.request.json
|
|
||||||
repo_url = data.get('repo_url', '')
|
|
||||||
|
|
||||||
# Parse GitHub repository URL to extract owner and repo
|
|
||||||
# Supports: https://github.com/owner/repo or github.com/owner/repo
|
|
||||||
pattern = r'github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/.*)?$'
|
|
||||||
match = re.search(pattern, repo_url)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
return self.http_status(400, -1, 'Invalid GitHub repository URL')
|
|
||||||
|
|
||||||
owner, repo = match.groups()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Fetch releases from GitHub API
|
|
||||||
url = f'https://api.github.com/repos/{owner}/{repo}/releases'
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
trust_env=True,
|
|
||||||
follow_redirects=True,
|
|
||||||
timeout=10,
|
|
||||||
) as client:
|
|
||||||
response = await client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
releases = response.json()
|
|
||||||
|
|
||||||
# Format releases data for frontend
|
|
||||||
formatted_releases = []
|
|
||||||
for release in releases:
|
|
||||||
formatted_releases.append(
|
|
||||||
{
|
|
||||||
'id': release['id'],
|
|
||||||
'tag_name': release['tag_name'],
|
|
||||||
'name': release['name'],
|
|
||||||
'published_at': release['published_at'],
|
|
||||||
'prerelease': release['prerelease'],
|
|
||||||
'draft': release['draft'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'releases': formatted_releases, 'owner': owner, 'repo': repo})
|
|
||||||
except httpx.RequestError as e:
|
|
||||||
return self.http_status(500, -1, f'Failed to fetch releases: {str(e)}')
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/github/release-assets',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
"""Get assets from a specific GitHub release"""
|
|
||||||
data = await quart.request.json
|
|
||||||
owner = data.get('owner', '')
|
|
||||||
repo = data.get('repo', '')
|
|
||||||
release_id = data.get('release_id', '')
|
|
||||||
|
|
||||||
if not all([owner, repo, release_id]):
|
|
||||||
return self.http_status(400, -1, 'Missing required parameters')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Fetch release assets from GitHub API
|
|
||||||
url = f'https://api.github.com/repos/{owner}/{repo}/releases/{release_id}'
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
trust_env=True,
|
|
||||||
follow_redirects=True,
|
|
||||||
timeout=10,
|
|
||||||
) as client:
|
|
||||||
response = await client.get(
|
|
||||||
url,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
release = response.json()
|
|
||||||
|
|
||||||
# Format assets data for frontend
|
|
||||||
formatted_assets = []
|
|
||||||
for asset in release.get('assets', []):
|
|
||||||
formatted_assets.append(
|
|
||||||
{
|
|
||||||
'id': asset['id'],
|
|
||||||
'name': asset['name'],
|
|
||||||
'size': asset['size'],
|
|
||||||
'download_url': asset['browser_download_url'],
|
|
||||||
'content_type': asset['content_type'],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# add zipball as a downloadable asset
|
|
||||||
# formatted_assets.append(
|
|
||||||
# {
|
|
||||||
# "id": 0,
|
|
||||||
# "name": "Source code (zip)",
|
|
||||||
# "size": -1,
|
|
||||||
# "download_url": release["zipball_url"],
|
|
||||||
# "content_type": "application/zip",
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
|
|
||||||
return self.success(data={'assets': formatted_assets})
|
|
||||||
except httpx.RequestError as e:
|
|
||||||
return self.http_status(500, -1, f'Failed to fetch release assets: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/install/github', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
"""Install plugin from GitHub release asset"""
|
|
||||||
data = await quart.request.json
|
|
||||||
asset_url = data.get('asset_url', '')
|
|
||||||
owner = data.get('owner', '')
|
|
||||||
repo = data.get('repo', '')
|
|
||||||
release_tag = data.get('release_tag', '')
|
|
||||||
|
|
||||||
if not asset_url:
|
|
||||||
return self.http_status(400, -1, 'Missing asset_url parameter')
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
install_info = {
|
|
||||||
'asset_url': asset_url,
|
|
||||||
'owner': owner,
|
|
||||||
'repo': repo,
|
|
||||||
'release_tag': release_tag,
|
|
||||||
'github_url': f'https://github.com/{owner}/{repo}',
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.GITHUB, install_info, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-github',
|
|
||||||
label=f'Installing plugin from GitHub {owner}/{repo}@{release_tag}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/install/marketplace',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.MARKETPLACE, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-marketplace',
|
|
||||||
label=f'Installing plugin from marketplace ...{data}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/install/local', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
file = (await quart.request.files).get('file')
|
|
||||||
if file is None:
|
|
||||||
return self.http_status(400, -1, 'file is required')
|
|
||||||
|
|
||||||
file_bytes = file.read()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'plugin_file': file_bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = taskmgr.TaskContext.new()
|
|
||||||
wrapper = self.ap.task_mgr.create_user_task(
|
|
||||||
self.ap.plugin_connector.install_plugin(PluginInstallSource.LOCAL, data, task_context=ctx),
|
|
||||||
kind='plugin-operation',
|
|
||||||
name='plugin-install-local',
|
|
||||||
label=f'Installing plugin from local ...{file.filename}',
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data={'task_id': wrapper.id})
|
|
||||||
|
|
||||||
@self.route('/config-files', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
"""Upload a file for plugin configuration"""
|
|
||||||
file = (await quart.request.files).get('file')
|
|
||||||
if file is None:
|
|
||||||
return self.http_status(400, -1, 'file is required')
|
|
||||||
|
|
||||||
# Check file size (10MB limit)
|
|
||||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
||||||
file_bytes = file.read()
|
|
||||||
if len(file_bytes) > MAX_FILE_SIZE:
|
|
||||||
return self.http_status(400, -1, 'file size exceeds 10MB limit')
|
|
||||||
|
|
||||||
# Generate unique file key with original extension
|
|
||||||
original_filename = file.filename
|
|
||||||
_, ext = os.path.splitext(original_filename)
|
|
||||||
file_key = f'plugin_config_{uuid.uuid4().hex}{ext}'
|
|
||||||
|
|
||||||
# Save file using storage manager
|
|
||||||
await self.ap.storage_mgr.storage_provider.save(file_key, file_bytes)
|
|
||||||
|
|
||||||
return self.success(data={'file_key': file_key})
|
|
||||||
|
|
||||||
@self.route('/config-files/<file_key>', methods=['DELETE'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(file_key: str) -> str:
|
|
||||||
"""Delete a plugin configuration file"""
|
|
||||||
# Only allow deletion of files with plugin_config_ prefix for security
|
|
||||||
if not file_key.startswith('plugin_config_'):
|
|
||||||
return self.http_status(400, -1, 'invalid file key')
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.storage_mgr.storage_provider.delete(file_key)
|
|
||||||
return self.success(data={'deleted': True})
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'failed to delete file: {str(e)}')
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import quart
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
from ... import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('mcp', '/api/v1/mcp')
|
|
||||||
class MCPRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/servers', methods=['GET', 'POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
"""获取MCP服务器列表"""
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
servers = await self.ap.mcp_service.get_mcp_servers(contain_runtime_info=True)
|
|
||||||
|
|
||||||
return self.success(data={'servers': servers})
|
|
||||||
|
|
||||||
elif quart.request.method == 'POST':
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
try:
|
|
||||||
uuid = await self.ap.mcp_service.create_mcp_server(data)
|
|
||||||
return self.success(data={'uuid': uuid})
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
return self.http_status(500, -1, f'Failed to create MCP server: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/servers/<server_name>', methods=['GET', 'PUT', 'DELETE'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(server_name: str) -> str:
|
|
||||||
"""获取、更新或删除MCP服务器配置"""
|
|
||||||
|
|
||||||
server_data = await self.ap.mcp_service.get_mcp_server_by_name(server_name)
|
|
||||||
if server_data is None:
|
|
||||||
return self.http_status(404, -1, 'Server not found')
|
|
||||||
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'server': server_data})
|
|
||||||
|
|
||||||
elif quart.request.method == 'PUT':
|
|
||||||
data = await quart.request.json
|
|
||||||
try:
|
|
||||||
await self.ap.mcp_service.update_mcp_server(server_data['uuid'], data)
|
|
||||||
return self.success()
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Failed to update MCP server: {str(e)}')
|
|
||||||
|
|
||||||
elif quart.request.method == 'DELETE':
|
|
||||||
try:
|
|
||||||
await self.ap.mcp_service.delete_mcp_server(server_data['uuid'])
|
|
||||||
return self.success()
|
|
||||||
except Exception as e:
|
|
||||||
return self.http_status(500, -1, f'Failed to delete MCP server: {str(e)}')
|
|
||||||
|
|
||||||
@self.route('/servers/<server_name>/test', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(server_name: str) -> str:
|
|
||||||
"""测试MCP服务器连接"""
|
|
||||||
server_data = await quart.request.json
|
|
||||||
task_id = await self.ap.mcp_service.test_mcp_server(server_name=server_name, server_data=server_data)
|
|
||||||
return self.success(data={'task_id': task_id})
|
|
||||||
@@ -1,115 +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,
|
|
||||||
'enable_marketplace': self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'enable_marketplace', True
|
|
||||||
),
|
|
||||||
'cloud_service_url': (
|
|
||||||
self.ap.instance_config.data.get('plugin', {}).get(
|
|
||||||
'cloud_service_url', 'https://space.langbot.app'
|
|
||||||
)
|
|
||||||
if 'cloud_service_url' in self.ap.instance_config.data.get('plugin', {})
|
|
||||||
else 'https://space.langbot.app'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route('/tasks', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
task_type = quart.request.args.get('type')
|
|
||||||
|
|
||||||
if task_type == '':
|
|
||||||
task_type = None
|
|
||||||
|
|
||||||
return self.success(data=self.ap.task_mgr.get_tasks_dict(task_type))
|
|
||||||
|
|
||||||
@self.route('/tasks/<task_id>', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(task_id: str) -> str:
|
|
||||||
task = self.ap.task_mgr.get_task_by_id(int(task_id))
|
|
||||||
|
|
||||||
if task is None:
|
|
||||||
return self.http_status(404, 404, 'Task not found')
|
|
||||||
|
|
||||||
return self.success(data=task.to_dict())
|
|
||||||
|
|
||||||
@self.route('/debug/exec', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
py_code = await quart.request.data
|
|
||||||
|
|
||||||
ap = self.ap
|
|
||||||
|
|
||||||
return self.success(data=exec(py_code, {'ap': ap}))
|
|
||||||
|
|
||||||
@self.route('/debug/tools/call', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data=await self.ap.tool_mgr.execute_func_call(data['tool_name'], data['tool_parameters'])
|
|
||||||
)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/debug/plugin/action',
|
|
||||||
methods=['POST'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
if not constants.debug_mode:
|
|
||||||
return self.http_status(403, 403, 'Forbidden')
|
|
||||||
|
|
||||||
data = await quart.request.json
|
|
||||||
|
|
||||||
class AnoymousAction:
|
|
||||||
value = 'anonymous_action'
|
|
||||||
|
|
||||||
def __init__(self, value: str):
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
resp = await self.ap.plugin_connector.handler.call_action(
|
|
||||||
AnoymousAction(data['action']),
|
|
||||||
data['data'],
|
|
||||||
timeout=data.get('timeout', 10),
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(data=resp)
|
|
||||||
|
|
||||||
@self.route(
|
|
||||||
'/status/plugin-system',
|
|
||||||
methods=['GET'],
|
|
||||||
auth_type=group.AuthType.USER_TOKEN,
|
|
||||||
)
|
|
||||||
async def _() -> str:
|
|
||||||
plugin_connector_error = 'ok'
|
|
||||||
is_connected = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.plugin_connector.ping_plugin_runtime()
|
|
||||||
except Exception as e:
|
|
||||||
plugin_connector_error = str(e)
|
|
||||||
is_connected = False
|
|
||||||
|
|
||||||
return self.success(
|
|
||||||
data={
|
|
||||||
'is_enable': self.ap.plugin_connector.is_enable_plugin,
|
|
||||||
'is_connected': is_connected,
|
|
||||||
'plugin_connector_error': plugin_connector_error,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import quart
|
|
||||||
import argon2
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from .. import group
|
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('user', '/api/v1/user')
|
|
||||||
class UserRouterGroup(group.RouterGroup):
|
|
||||||
async def initialize(self) -> None:
|
|
||||||
@self.route('/init', methods=['GET', 'POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
if quart.request.method == 'GET':
|
|
||||||
return self.success(data={'initialized': await self.ap.user_service.is_initialized()})
|
|
||||||
|
|
||||||
if await self.ap.user_service.is_initialized():
|
|
||||||
return self.fail(1, 'System already initialized')
|
|
||||||
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
password = json_data['password']
|
|
||||||
|
|
||||||
await self.ap.user_service.create_user(user_email, password)
|
|
||||||
|
|
||||||
return self.success()
|
|
||||||
|
|
||||||
@self.route('/auth', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
try:
|
|
||||||
token = await self.ap.user_service.authenticate(json_data['user'], json_data['password'])
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.fail(1, 'Invalid username or password')
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/check-token', methods=['GET'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
token = await self.ap.user_service.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
return self.success(data={'token': token})
|
|
||||||
|
|
||||||
@self.route('/reset-password', methods=['POST'], auth_type=group.AuthType.NONE)
|
|
||||||
async def _() -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
user_email = json_data['user']
|
|
||||||
recovery_key = json_data['recovery_key']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
# hard sleep 3s for security
|
|
||||||
await asyncio.sleep(3)
|
|
||||||
|
|
||||||
if not await self.ap.user_service.is_initialized():
|
|
||||||
return self.http_status(400, -1, 'System not initialized')
|
|
||||||
|
|
||||||
user_obj = await self.ap.user_service.get_user_by_email(user_email)
|
|
||||||
|
|
||||||
if user_obj is None:
|
|
||||||
return self.http_status(400, -1, 'User not found')
|
|
||||||
|
|
||||||
if recovery_key != self.ap.instance_config.data['system']['recovery_key']:
|
|
||||||
return self.http_status(403, -1, 'Invalid recovery key')
|
|
||||||
|
|
||||||
await self.ap.user_service.reset_password(user_email, new_password)
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
|
|
||||||
@self.route('/change-password', methods=['POST'], auth_type=group.AuthType.USER_TOKEN)
|
|
||||||
async def _(user_email: str) -> str:
|
|
||||||
json_data = await quart.request.json
|
|
||||||
|
|
||||||
current_password = json_data['current_password']
|
|
||||||
new_password = json_data['new_password']
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.ap.user_service.change_password(user_email, current_password, new_password)
|
|
||||||
except argon2.exceptions.VerifyMismatchError:
|
|
||||||
return self.http_status(400, -1, 'Current password is incorrect')
|
|
||||||
except ValueError as e:
|
|
||||||
return self.http_status(400, -1, str(e))
|
|
||||||
|
|
||||||
return self.success(data={'user': user_email})
|
|
||||||
@@ -1,141 +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:
|
|
||||||
"""Bot service"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_bots(self, include_secret: bool = True) -> list[dict]:
|
|
||||||
"""获取所有机器人"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_bot.Bot))
|
|
||||||
|
|
||||||
bots = result.all()
|
|
||||||
|
|
||||||
masked_columns = []
|
|
||||||
if not include_secret:
|
|
||||||
masked_columns = ['adapter_config']
|
|
||||||
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns) for bot in bots]
|
|
||||||
|
|
||||||
async def get_bot(self, bot_uuid: str, include_secret: bool = True) -> 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
|
|
||||||
|
|
||||||
masked_columns = []
|
|
||||||
if not include_secret:
|
|
||||||
masked_columns = ['adapter_config']
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_bot.Bot, bot, masked_columns)
|
|
||||||
|
|
||||||
async def get_runtime_bot_info(self, bot_uuid: str, include_secret: bool = True) -> dict:
|
|
||||||
"""获取机器人运行时信息"""
|
|
||||||
persistence_bot = await self.get_bot(bot_uuid, include_secret)
|
|
||||||
if persistence_bot is None:
|
|
||||||
raise Exception('Bot not found')
|
|
||||||
|
|
||||||
adapter_runtime_values = {}
|
|
||||||
|
|
||||||
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
|
|
||||||
if runtime_bot is not None:
|
|
||||||
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
|
|
||||||
|
|
||||||
persistence_bot['adapter_runtime_values'] = adapter_runtime_values
|
|
||||||
|
|
||||||
return persistence_bot
|
|
||||||
|
|
||||||
async def create_bot(self, bot_data: dict) -> str:
|
|
||||||
"""Create bot"""
|
|
||||||
# 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:
|
|
||||||
"""Update bot"""
|
|
||||||
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()
|
|
||||||
|
|
||||||
# update all conversation that use this bot
|
|
||||||
for session in self.ap.sess_mgr.session_list:
|
|
||||||
if session.using_conversation is not None and session.using_conversation.bot_uuid == bot_uuid:
|
|
||||||
session.using_conversation = None
|
|
||||||
|
|
||||||
async def delete_bot(self, bot_uuid: str) -> None:
|
|
||||||
"""Delete bot"""
|
|
||||||
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,120 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
import sqlalchemy
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import rag as persistence_rag
|
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeService:
|
|
||||||
"""知识库服务"""
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_knowledge_bases(self) -> list[dict]:
|
|
||||||
"""获取所有知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
|
||||||
knowledge_bases = result.all()
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
for knowledge_base in knowledge_bases
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_knowledge_base(self, kb_uuid: str) -> dict | None:
|
|
||||||
"""获取知识库"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
knowledge_base = result.first()
|
|
||||||
if knowledge_base is None:
|
|
||||||
return None
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_rag.KnowledgeBase, knowledge_base)
|
|
||||||
|
|
||||||
async def create_knowledge_base(self, kb_data: dict) -> str:
|
|
||||||
"""创建知识库"""
|
|
||||||
kb_data['uuid'] = str(uuid.uuid4())
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.KnowledgeBase).values(kb_data))
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
return kb_data['uuid']
|
|
||||||
|
|
||||||
async def update_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
|
||||||
"""更新知识库"""
|
|
||||||
if 'uuid' in kb_data:
|
|
||||||
del kb_data['uuid']
|
|
||||||
|
|
||||||
if 'embedding_model_uuid' in kb_data:
|
|
||||||
del kb_data['embedding_model_uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_rag.KnowledgeBase)
|
|
||||||
.values(kb_data)
|
|
||||||
.where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
|
|
||||||
|
|
||||||
kb = await self.get_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.rag_mgr.load_knowledge_base(kb)
|
|
||||||
|
|
||||||
async def store_file(self, kb_uuid: str, file_id: str) -> int:
|
|
||||||
"""存储文件"""
|
|
||||||
# await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_rag.File).values(kb_id=kb_uuid, file_id=file_id))
|
|
||||||
# await self.ap.rag_mgr.store_file(file_id)
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return await runtime_kb.store_file(file_id)
|
|
||||||
|
|
||||||
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
|
||||||
"""检索知识库"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
return [
|
|
||||||
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
|
||||||
"""获取知识库文件"""
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
files = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_rag.File, file) for file in files]
|
|
||||||
|
|
||||||
async def delete_file(self, kb_uuid: str, file_id: str) -> None:
|
|
||||||
"""删除文件"""
|
|
||||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
|
||||||
if runtime_kb is None:
|
|
||||||
raise Exception('Knowledge base not found')
|
|
||||||
await runtime_kb.delete_file(file_id)
|
|
||||||
|
|
||||||
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
|
||||||
"""删除知识库"""
|
|
||||||
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.KnowledgeBase).where(persistence_rag.KnowledgeBase.uuid == kb_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
# delete files
|
|
||||||
files = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_rag.File).where(persistence_rag.File.kb_id == kb_uuid)
|
|
||||||
)
|
|
||||||
for file in files:
|
|
||||||
# delete chunks
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.Chunk).where(persistence_rag.Chunk.file_id == file.uuid)
|
|
||||||
)
|
|
||||||
# delete file
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
|
|
||||||
)
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class LLMModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_llm_models(self, include_secret: bool = True) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.LLMModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
|
|
||||||
masked_columns = []
|
|
||||||
if not include_secret:
|
|
||||||
masked_columns = ['api_keys']
|
|
||||||
|
|
||||||
return [
|
|
||||||
self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model, masked_columns)
|
|
||||||
for model in models
|
|
||||||
]
|
|
||||||
|
|
||||||
async def create_llm_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_model.LLMModel).values(**model_data))
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
# check if default pipeline has no model bound
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
|
||||||
persistence_pipeline.LegacyPipeline.is_default == True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
pipeline = result.first()
|
|
||||||
if pipeline is not None and pipeline.config['ai']['local-agent']['model'] == '':
|
|
||||||
pipeline_config = pipeline.config
|
|
||||||
pipeline_config['ai']['local-agent']['model'] = model_data['uuid']
|
|
||||||
pipeline_data = {'config': pipeline_config}
|
|
||||||
await self.ap.pipeline_service.update_pipeline(pipeline.uuid, pipeline_data)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_llm_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, model)
|
|
||||||
|
|
||||||
async def update_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.LLMModel)
|
|
||||||
.where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
llm_model = await self.get_llm_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_llm_model(llm_model)
|
|
||||||
|
|
||||||
async def delete_llm_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.LLMModel).where(persistence_model.LLMModel.uuid == model_uuid)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_llm_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_llm_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_llm_model: model_requester.RuntimeLLMModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.llm_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_llm_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_llm_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_llm_model = await self.ap.model_mgr.init_runtime_llm_model(model_data)
|
|
||||||
|
|
||||||
# Mon Nov 10 2025: Commented for some providers may not support thinking parameter
|
|
||||||
# # 有些模型厂商默认开启了思考功能,测试容易延迟
|
|
||||||
# extra_args = model_data.get('extra_args', {})
|
|
||||||
# if not extra_args or 'thinking' not in extra_args:
|
|
||||||
# extra_args['thinking'] = {'type': 'disabled'}
|
|
||||||
|
|
||||||
await runtime_llm_model.requester.invoke_llm(
|
|
||||||
query=None,
|
|
||||||
model=runtime_llm_model,
|
|
||||||
messages=[provider_message.Message(role='user', content='Hello, world! Please just reply a "Hello".')],
|
|
||||||
funcs=[],
|
|
||||||
# extra_args=extra_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingModelsService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def get_embedding_models(self) -> list[dict]:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_model.EmbeddingModel))
|
|
||||||
|
|
||||||
models = result.all()
|
|
||||||
return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model) for model in models]
|
|
||||||
|
|
||||||
async def create_embedding_model(self, model_data: dict) -> str:
|
|
||||||
model_data['uuid'] = str(uuid.uuid4())
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(persistence_model.EmbeddingModel).values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_data['uuid'])
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
return model_data['uuid']
|
|
||||||
|
|
||||||
async def get_embedding_model(self, model_uuid: str) -> dict | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
model = result.first()
|
|
||||||
|
|
||||||
if model is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, model)
|
|
||||||
|
|
||||||
async def update_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
if 'uuid' in model_data:
|
|
||||||
del model_data['uuid']
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(persistence_model.EmbeddingModel)
|
|
||||||
.where(persistence_model.EmbeddingModel.uuid == model_uuid)
|
|
||||||
.values(**model_data)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
embedding_model = await self.get_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.load_embedding_model(embedding_model)
|
|
||||||
|
|
||||||
async def delete_embedding_model(self, model_uuid: str) -> None:
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.delete(persistence_model.EmbeddingModel).where(
|
|
||||||
persistence_model.EmbeddingModel.uuid == model_uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.ap.model_mgr.remove_embedding_model(model_uuid)
|
|
||||||
|
|
||||||
async def test_embedding_model(self, model_uuid: str, model_data: dict) -> None:
|
|
||||||
runtime_embedding_model: model_requester.RuntimeEmbeddingModel | None = None
|
|
||||||
|
|
||||||
if model_uuid != '_':
|
|
||||||
for model in self.ap.model_mgr.embedding_models:
|
|
||||||
if model.model_entity.uuid == model_uuid:
|
|
||||||
runtime_embedding_model = model
|
|
||||||
break
|
|
||||||
|
|
||||||
if runtime_embedding_model is None:
|
|
||||||
raise Exception('model not found')
|
|
||||||
|
|
||||||
else:
|
|
||||||
runtime_embedding_model = await self.ap.model_mgr.init_runtime_embedding_model(model_data)
|
|
||||||
|
|
||||||
await runtime_embedding_model.requester.invoke_embedding(
|
|
||||||
model=runtime_embedding_model,
|
|
||||||
input_text=['Hello, world!'],
|
|
||||||
extra_args={},
|
|
||||||
)
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sqlalchemy
|
|
||||||
import argon2
|
|
||||||
import jwt
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ....core import app
|
|
||||||
from ....entity.persistence import user
|
|
||||||
from ....utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application) -> None:
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
async def is_initialized(self) -> bool:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(user.User).limit(1))
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list is not None and len(result_list) > 0
|
|
||||||
|
|
||||||
async def create_user(self, user_email: str, password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.insert(user.User).values(user=user_email, password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_user_by_email(self, user_email: str) -> user.User | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
return result_list[0] if result_list is not None and len(result_list) > 0 else None
|
|
||||||
|
|
||||||
async def authenticate(self, user_email: str, password: str) -> str | None:
|
|
||||||
result = await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.select(user.User).where(user.User.user == user_email)
|
|
||||||
)
|
|
||||||
|
|
||||||
result_list = result.all()
|
|
||||||
|
|
||||||
if result_list is None or len(result_list) == 0:
|
|
||||||
raise ValueError('用户不存在')
|
|
||||||
|
|
||||||
user_obj = result_list[0]
|
|
||||||
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, password)
|
|
||||||
|
|
||||||
return await self.generate_jwt_token(user_email)
|
|
||||||
|
|
||||||
async def generate_jwt_token(self, user_email: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
jwt_expire = self.ap.instance_config.data['system']['jwt']['expire']
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
'user': user_email,
|
|
||||||
'iss': 'LangBot-' + constants.edition,
|
|
||||||
'exp': datetime.datetime.now() + datetime.timedelta(seconds=jwt_expire),
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt.encode(payload, jwt_secret, algorithm='HS256')
|
|
||||||
|
|
||||||
async def verify_jwt_token(self, token: str) -> str:
|
|
||||||
jwt_secret = self.ap.instance_config.data['system']['jwt']['secret']
|
|
||||||
|
|
||||||
return jwt.decode(token, jwt_secret, algorithms=['HS256'])['user']
|
|
||||||
|
|
||||||
async def reset_password(self, user_email: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def change_password(self, user_email: str, current_password: str, new_password: str) -> None:
|
|
||||||
ph = argon2.PasswordHasher()
|
|
||||||
|
|
||||||
user_obj = await self.get_user_by_email(user_email)
|
|
||||||
if user_obj is None:
|
|
||||||
raise ValueError('User not found')
|
|
||||||
|
|
||||||
ph.verify(user_obj.password, current_password)
|
|
||||||
|
|
||||||
hashed_password = ph.hash(new_password)
|
|
||||||
|
|
||||||
await self.ap.persistence_mgr.execute_async(
|
|
||||||
sqlalchemy.update(user.User).where(user.User.user == user_email).values(password=hashed_password)
|
|
||||||
)
|
|
||||||
-211
@@ -1,211 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..platform import botmgr as im_mgr
|
|
||||||
from ..platform.webhook_pusher import WebhookPusher
|
|
||||||
from ..provider.session import sessionmgr as llm_session_mgr
|
|
||||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
|
||||||
from ..provider.tools import toolmgr as llm_tool_mgr
|
|
||||||
from ..config import manager as config_mgr
|
|
||||||
from ..command import cmdmgr
|
|
||||||
from ..plugin import connector as plugin_connector
|
|
||||||
from ..pipeline import pool
|
|
||||||
from ..pipeline import controller, pipelinemgr
|
|
||||||
from ..utils import version as version_mgr, proxy as proxy_mgr, announce as announce_mgr
|
|
||||||
from ..persistence import mgr as persistencemgr
|
|
||||||
from ..api.http.controller import main as http_controller
|
|
||||||
from ..api.http.service import user as user_service
|
|
||||||
from ..api.http.service import model as model_service
|
|
||||||
from ..api.http.service import pipeline as pipeline_service
|
|
||||||
from ..api.http.service import bot as bot_service
|
|
||||||
from ..api.http.service import knowledge as knowledge_service
|
|
||||||
from ..api.http.service import mcp as mcp_service
|
|
||||||
from ..api.http.service import apikey as apikey_service
|
|
||||||
from ..api.http.service import webhook as webhook_service
|
|
||||||
from ..discover import engine as discover_engine
|
|
||||||
from ..storage import mgr as storagemgr
|
|
||||||
from ..utils import logcache
|
|
||||||
from . import taskmgr
|
|
||||||
from . import entities as core_entities
|
|
||||||
from ..rag.knowledge import kbmgr as rag_mgr
|
|
||||||
from ..vector import mgr as vectordb_mgr
|
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
|
||||||
"""Runtime application object and context"""
|
|
||||||
|
|
||||||
event_loop: asyncio.AbstractEventLoop = None
|
|
||||||
|
|
||||||
# asyncio_tasks: list[asyncio.Task] = []
|
|
||||||
task_mgr: taskmgr.AsyncTaskManager = None
|
|
||||||
|
|
||||||
discover: discover_engine.ComponentDiscoveryEngine = None
|
|
||||||
|
|
||||||
platform_mgr: im_mgr.PlatformManager = None
|
|
||||||
|
|
||||||
webhook_pusher: WebhookPusher = None
|
|
||||||
|
|
||||||
cmd_mgr: cmdmgr.CommandManager = None
|
|
||||||
|
|
||||||
sess_mgr: llm_session_mgr.SessionManager = None
|
|
||||||
|
|
||||||
model_mgr: llm_model_mgr.ModelManager = None
|
|
||||||
|
|
||||||
rag_mgr: rag_mgr.RAGManager = None
|
|
||||||
|
|
||||||
# TODO move to pipeline
|
|
||||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
|
||||||
|
|
||||||
# ======= Config manager =======
|
|
||||||
|
|
||||||
command_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
pipeline_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
platform_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
provider_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
system_cfg: config_mgr.ConfigManager = None # deprecated
|
|
||||||
|
|
||||||
instance_config: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# ======= Metadata config manager =======
|
|
||||||
|
|
||||||
sensitive_meta: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
pipeline_config_meta_trigger: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_safety: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_ai: config_mgr.ConfigManager = None
|
|
||||||
pipeline_config_meta_output: config_mgr.ConfigManager = None
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
plugin_connector: plugin_connector.PluginRuntimeConnector = None
|
|
||||||
|
|
||||||
query_pool: pool.QueryPool = None
|
|
||||||
|
|
||||||
ctrl: controller.Controller = None
|
|
||||||
|
|
||||||
pipeline_mgr: pipelinemgr.PipelineManager = None
|
|
||||||
|
|
||||||
ver_mgr: version_mgr.VersionManager = None
|
|
||||||
|
|
||||||
ann_mgr: announce_mgr.AnnouncementManager = None
|
|
||||||
|
|
||||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
|
||||||
|
|
||||||
logger: logging.Logger = None
|
|
||||||
|
|
||||||
persistence_mgr: persistencemgr.PersistenceManager = None
|
|
||||||
|
|
||||||
vector_db_mgr: vectordb_mgr.VectorDBManager = None
|
|
||||||
|
|
||||||
http_ctrl: http_controller.HTTPController = None
|
|
||||||
|
|
||||||
log_cache: logcache.LogCache = None
|
|
||||||
|
|
||||||
storage_mgr: storagemgr.StorageMgr = None
|
|
||||||
|
|
||||||
# ========= HTTP Services =========
|
|
||||||
|
|
||||||
user_service: user_service.UserService = None
|
|
||||||
|
|
||||||
llm_model_service: model_service.LLMModelsService = None
|
|
||||||
|
|
||||||
embedding_models_service: model_service.EmbeddingModelsService = None
|
|
||||||
|
|
||||||
pipeline_service: pipeline_service.PipelineService = None
|
|
||||||
|
|
||||||
bot_service: bot_service.BotService = None
|
|
||||||
|
|
||||||
knowledge_service: knowledge_service.KnowledgeService = None
|
|
||||||
|
|
||||||
mcp_service: mcp_service.MCPService = None
|
|
||||||
|
|
||||||
apikey_service: apikey_service.ApiKeyService = None
|
|
||||||
|
|
||||||
webhook_service: webhook_service.WebhookService = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
try:
|
|
||||||
await self.plugin_connector.initialize_plugins()
|
|
||||||
|
|
||||||
# 后续可能会允许动态重启其他任务
|
|
||||||
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
|
|
||||||
async def never_ending():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.platform_mgr.run(),
|
|
||||||
name='platform-manager',
|
|
||||||
scopes=[
|
|
||||||
core_entities.LifecycleControlScope.APPLICATION,
|
|
||||||
core_entities.LifecycleControlScope.PLATFORM,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.ctrl.run(),
|
|
||||||
name='query-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
self.http_ctrl.run(),
|
|
||||||
name='http-api-controller',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.task_mgr.create_task(
|
|
||||||
never_ending(),
|
|
||||||
name='never-ending-task',
|
|
||||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.print_web_access_info()
|
|
||||||
await self.task_mgr.wait_all()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f'Application runtime fatal exception: {e}')
|
|
||||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
|
||||||
|
|
||||||
def dispose(self):
|
|
||||||
self.plugin_connector.dispose()
|
|
||||||
|
|
||||||
async def print_web_access_info(self):
|
|
||||||
"""Print access webui tips"""
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.join('.', 'web/out')):
|
|
||||||
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh')
|
|
||||||
self.logger.warning(
|
|
||||||
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
host_ip = '127.0.0.1'
|
|
||||||
|
|
||||||
port = self.instance_config.data['api']['port']
|
|
||||||
|
|
||||||
tips = f"""
|
|
||||||
=======================================
|
|
||||||
✨ Access WebUI / 访问管理面板
|
|
||||||
|
|
||||||
🏠 Local Address: http://{host_ip}:{port}/
|
|
||||||
🌐 Public Address: http://<Your Public IP>:{port}/
|
|
||||||
|
|
||||||
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
|
||||||
=======================================
|
|
||||||
""".strip()
|
|
||||||
for line in tips.split('\n'):
|
|
||||||
self.logger.info(line)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import logging
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import colorlog
|
|
||||||
|
|
||||||
from ...utils import constants
|
|
||||||
|
|
||||||
|
|
||||||
log_colors_config = {
|
|
||||||
'DEBUG': 'green', # cyan white
|
|
||||||
'INFO': 'white',
|
|
||||||
'WARNING': 'yellow',
|
|
||||||
'ERROR': 'red',
|
|
||||||
'CRITICAL': 'cyan',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
|
|
||||||
# Remove all existing loggers
|
|
||||||
for handler in logging.root.handlers[:]:
|
|
||||||
logging.root.removeHandler(handler)
|
|
||||||
|
|
||||||
level = logging.INFO
|
|
||||||
|
|
||||||
if constants.debug_mode:
|
|
||||||
level = logging.DEBUG
|
|
||||||
|
|
||||||
log_file_name = 'data/logs/langbot-%s.log' % time.strftime('%Y-%m-%d', time.localtime())
|
|
||||||
|
|
||||||
qcg_logger = logging.getLogger('langbot')
|
|
||||||
|
|
||||||
qcg_logger.setLevel(level)
|
|
||||||
|
|
||||||
color_formatter = colorlog.ColoredFormatter(
|
|
||||||
fmt='%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s',
|
|
||||||
datefmt='%m-%d %H:%M:%S',
|
|
||||||
log_colors=log_colors_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
stream_handler = logging.StreamHandler(sys.stdout)
|
|
||||||
# stream_handler.setLevel(level)
|
|
||||||
# stream_handler.setFormatter(color_formatter)
|
|
||||||
stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
|
|
||||||
|
|
||||||
log_handlers: list[logging.Handler] = [
|
|
||||||
stream_handler,
|
|
||||||
logging.FileHandler(log_file_name, encoding='utf-8'),
|
|
||||||
]
|
|
||||||
log_handlers += extra_handlers if extra_handlers is not None else []
|
|
||||||
|
|
||||||
for handler in log_handlers:
|
|
||||||
handler.setLevel(level)
|
|
||||||
handler.setFormatter(color_formatter)
|
|
||||||
qcg_logger.addHandler(handler)
|
|
||||||
|
|
||||||
qcg_logger.debug('Logging initialized, log level: %s' % level)
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.CRITICAL, # Set log output format
|
|
||||||
format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s',
|
|
||||||
# Log output format
|
|
||||||
# -8 is a placeholder, left-align the output, and output length is 8
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S', # Time output format
|
|
||||||
handlers=[logging.NullHandler()],
|
|
||||||
)
|
|
||||||
|
|
||||||
return qcg_logger
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from . import app
|
|
||||||
|
|
||||||
|
|
||||||
preregistered_migrations: list[typing.Type[Migration]] = []
|
|
||||||
"""Currently not supported for extension"""
|
|
||||||
|
|
||||||
|
|
||||||
def migration_class(name: str, number: int):
|
|
||||||
"""Register a migration"""
|
|
||||||
|
|
||||||
def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]:
|
|
||||||
cls.name = name
|
|
||||||
cls.number = number
|
|
||||||
preregistered_migrations.append(cls)
|
|
||||||
return cls
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(abc.ABC):
|
|
||||||
"""A version migration"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
number: int
|
|
||||||
|
|
||||||
ap: app.Application
|
|
||||||
|
|
||||||
def __init__(self, ap: app.Application):
|
|
||||||
self.ap = ap
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""Determine if the current environment needs to run this migration"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def run(self):
|
|
||||||
"""Run migration"""
|
|
||||||
pass
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('sensitive-word-migration', 1)
|
|
||||||
class SensitiveWordMigration(migration.Migration):
|
|
||||||
"""敏感词迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return os.path.exists('data/config/sensitive-words.json') and not os.path.exists(
|
|
||||||
'data/metadata/sensitive-words.json'
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
# 移动文件
|
|
||||||
os.rename('data/config/sensitive-words.json', 'data/metadata/sensitive-words.json')
|
|
||||||
|
|
||||||
# 重新加载配置
|
|
||||||
await self.ap.sensitive_meta.load_config()
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('openai-config-migration', 2)
|
|
||||||
class OpenAIConfigMigration(migration.Migration):
|
|
||||||
"""OpenAI配置迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'openai-config' in self.ap.provider_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
old_openai_config = self.ap.provider_cfg.data['openai-config'].copy()
|
|
||||||
|
|
||||||
if 'keys' not in self.ap.provider_cfg.data:
|
|
||||||
self.ap.provider_cfg.data['keys'] = {}
|
|
||||||
|
|
||||||
if 'openai' not in self.ap.provider_cfg.data['keys']:
|
|
||||||
self.ap.provider_cfg.data['keys']['openai'] = []
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['keys']['openai'] = old_openai_config['api-keys']
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['model'] = old_openai_config['chat-completions-params']['model']
|
|
||||||
|
|
||||||
del old_openai_config['chat-completions-params']['model']
|
|
||||||
|
|
||||||
if 'requester' not in self.ap.provider_cfg.data:
|
|
||||||
self.ap.provider_cfg.data['requester'] = {}
|
|
||||||
|
|
||||||
if 'openai-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
|
||||||
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {}
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {
|
|
||||||
'base-url': old_openai_config['base_url'],
|
|
||||||
'args': old_openai_config['chat-completions-params'],
|
|
||||||
'timeout': old_openai_config['request-timeout'],
|
|
||||||
}
|
|
||||||
|
|
||||||
del self.ap.provider_cfg.data['openai-config']
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('anthropic-requester-config-completion', 3)
|
|
||||||
class AnthropicRequesterConfigCompletionMigration(migration.Migration):
|
|
||||||
"""OpenAI配置迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return (
|
|
||||||
'anthropic-messages' not in self.ap.provider_cfg.data['requester']
|
|
||||||
or 'anthropic' not in self.ap.provider_cfg.data['keys']
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
if 'anthropic-messages' not in self.ap.provider_cfg.data['requester']:
|
|
||||||
self.ap.provider_cfg.data['requester']['anthropic-messages'] = {
|
|
||||||
'base-url': 'https://api.anthropic.com',
|
|
||||||
'args': {'max_tokens': 1024},
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
if 'anthropic' not in self.ap.provider_cfg.data['keys']:
|
|
||||||
self.ap.provider_cfg.data['keys']['anthropic'] = []
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('moonshot-config-completion', 4)
|
|
||||||
class MoonshotConfigCompletionMigration(migration.Migration):
|
|
||||||
"""OpenAI配置迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return (
|
|
||||||
'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']
|
|
||||||
or 'moonshot' not in self.ap.provider_cfg.data['keys']
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
if 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
|
||||||
self.ap.provider_cfg.data['requester']['moonshot-chat-completions'] = {
|
|
||||||
'base-url': 'https://api.moonshot.cn/v1',
|
|
||||||
'args': {},
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
if 'moonshot' not in self.ap.provider_cfg.data['keys']:
|
|
||||||
self.ap.provider_cfg.data['keys']['moonshot'] = []
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('deepseek-config-completion', 5)
|
|
||||||
class DeepseekConfigCompletionMigration(migration.Migration):
|
|
||||||
"""OpenAI配置迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return (
|
|
||||||
'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']
|
|
||||||
or 'deepseek' not in self.ap.provider_cfg.data['keys']
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
if 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
|
||||||
self.ap.provider_cfg.data['requester']['deepseek-chat-completions'] = {
|
|
||||||
'base-url': 'https://api.deepseek.com',
|
|
||||||
'args': {},
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
if 'deepseek' not in self.ap.provider_cfg.data['keys']:
|
|
||||||
self.ap.provider_cfg.data['keys']['deepseek'] = []
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('vision-config', 6)
|
|
||||||
class VisionConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'enable-vision' not in self.ap.provider_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
if 'enable-vision' not in self.ap.provider_cfg.data:
|
|
||||||
self.ap.provider_cfg.data['enable-vision'] = False
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('qcg-center-url-config', 7)
|
|
||||||
class QCGCenterURLConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'qcg-center-url' not in self.ap.system_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
if 'qcg-center-url' not in self.ap.system_cfg.data:
|
|
||||||
self.ap.system_cfg.data['qcg-center-url'] = 'https://api.qchatgpt.rockchin.top/api/v2'
|
|
||||||
|
|
||||||
await self.ap.system_cfg.dump_config()
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('ad-fixwin-cfg-migration', 8)
|
|
||||||
class AdFixwinConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return isinstance(self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default'], int)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
for session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']:
|
|
||||||
temp_dict = {
|
|
||||||
'window-size': 60,
|
|
||||||
'limit': self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name],
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name] = temp_dict
|
|
||||||
|
|
||||||
await self.ap.pipeline_cfg.dump_config()
|
|
||||||
@@ -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,23 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('ollama-requester-config', 10)
|
|
||||||
class MsgTruncatorConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'ollama-chat' not in self.ap.provider_cfg.data['requester']
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['requester']['ollama-chat'] = {
|
|
||||||
'base-url': 'http://127.0.0.1:11434',
|
|
||||||
'args': {},
|
|
||||||
'timeout': 600,
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('command-prefix-config', 11)
|
|
||||||
class CommandPrefixConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'command-prefix' not in self.ap.command_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.command_cfg.data['command-prefix'] = ['!', '!']
|
|
||||||
|
|
||||||
await self.ap.command_cfg.dump_config()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('runner-config', 12)
|
|
||||||
class RunnerConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'runner' not in self.ap.provider_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['runner'] = 'local-agent'
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('http-api-config', 13)
|
|
||||||
class HttpApiConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'http-api' not in self.ap.system_cfg.data or 'persistence' not in self.ap.system_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.system_cfg.data['http-api'] = {
|
|
||||||
'enable': True,
|
|
||||||
'host': '0.0.0.0',
|
|
||||||
'port': 5300,
|
|
||||||
'jwt-expire': 604800,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ap.system_cfg.data['persistence'] = {
|
|
||||||
'sqlite': {'path': 'data/persistence.db'},
|
|
||||||
'use': 'sqlite',
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.system_cfg.dump_config()
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('force-delay-config', 14)
|
|
||||||
class ForceDelayConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return isinstance(self.ap.platform_cfg.data['force-delay'], list)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
|
|
||||||
self.ap.platform_cfg.data['force-delay'] = {
|
|
||||||
'min': self.ap.platform_cfg.data['force-delay'][0],
|
|
||||||
'max': self.ap.platform_cfg.data['force-delay'][1],
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.platform_cfg.dump_config()
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('gitee-ai-config', 15)
|
|
||||||
class GiteeAIConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return (
|
|
||||||
'gitee-ai-chat-completions' not in self.ap.provider_cfg.data['requester']
|
|
||||||
or 'gitee-ai' not in self.ap.provider_cfg.data['keys']
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
self.ap.provider_cfg.data['requester']['gitee-ai-chat-completions'] = {
|
|
||||||
'base-url': 'https://ai.gitee.com/v1',
|
|
||||||
'args': {},
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.ap.provider_cfg.data['keys']['gitee-ai'] = ['XXXXX']
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('dify-service-api-config', 16)
|
|
||||||
class DifyServiceAPICfgMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'dify-service-api' not in self.ap.provider_cfg.data
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
self.ap.provider_cfg.data['dify-service-api'] = {
|
|
||||||
'base-url': 'https://api.dify.ai/v1',
|
|
||||||
'app-type': 'chat',
|
|
||||||
'chat': {'api-key': 'app-1234567890'},
|
|
||||||
'workflow': {'api-key': 'app-1234567890', 'output-key': 'summary'},
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('dify-api-timeout-params', 17)
|
|
||||||
class DifyAPITimeoutParamsMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return (
|
|
||||||
'timeout' not in self.ap.provider_cfg.data['dify-service-api']['chat']
|
|
||||||
or 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['workflow']
|
|
||||||
or 'agent' not in self.ap.provider_cfg.data['dify-service-api']
|
|
||||||
)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
self.ap.provider_cfg.data['dify-service-api']['chat']['timeout'] = 120
|
|
||||||
self.ap.provider_cfg.data['dify-service-api']['workflow']['timeout'] = 120
|
|
||||||
self.ap.provider_cfg.data['dify-service-api']['agent'] = {
|
|
||||||
'api-key': 'app-1234567890',
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from .. import migration
|
|
||||||
|
|
||||||
|
|
||||||
@migration.migration_class('xai-config', 18)
|
|
||||||
class XaiConfigMigration(migration.Migration):
|
|
||||||
"""迁移"""
|
|
||||||
|
|
||||||
async def need_migrate(self) -> bool:
|
|
||||||
"""判断当前环境是否需要运行此迁移"""
|
|
||||||
return 'xai-chat-completions' not in self.ap.provider_cfg.data['requester']
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""执行迁移"""
|
|
||||||
self.ap.provider_cfg.data['requester']['xai-chat-completions'] = {
|
|
||||||
'base-url': 'https://api.x.ai/v1',
|
|
||||||
'args': {},
|
|
||||||
'timeout': 120,
|
|
||||||
}
|
|
||||||
self.ap.provider_cfg.data['keys']['xai'] = ['xai-1234567890']
|
|
||||||
|
|
||||||
await self.ap.provider_cfg.dump_config()
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user