mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
Compare commits
2809 Commits
noReleaseP
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d51293594 | ||
|
|
b78b0e50bb | ||
|
|
6b4c1a7dee | ||
|
|
2e1f16d7b4 | ||
|
|
50c33c5213 | ||
|
|
ace6d62d76 | ||
|
|
b7c4c21796 | ||
|
|
66602da9cb | ||
|
|
31b483509c | ||
|
|
ba7cf69c9d | ||
|
|
37296be67e | ||
|
|
6c03a1dd31 | ||
|
|
b75ec9e989 | ||
|
|
5c8523e4ef | ||
|
|
9802a42a9e | ||
|
|
99e3abec72 | ||
|
|
fc2efdf994 | ||
|
|
6ed672d996 | ||
|
|
2bf593fa6b | ||
|
|
3182214663 | ||
|
|
20614b20b7 | ||
|
|
da323817f7 | ||
|
|
763c1a885c | ||
|
|
dbc09f46f4 | ||
|
|
cf43f09aff | ||
|
|
c3c51b0fbf | ||
|
|
8a42daa63f | ||
|
|
d91d98c9d4 | ||
|
|
2e82f2b2d1 | ||
|
|
f459c7017a | ||
|
|
c27ccb8475 | ||
|
|
abb2f7ae05 | ||
|
|
80606ed32c | ||
|
|
bc7c5fa864 | ||
|
|
ed0ea68037 | ||
|
|
6ac4dbc011 | ||
|
|
e642ffa5b3 | ||
|
|
6a24c951e0 | ||
|
|
58369480e2 | ||
|
|
43553e2c7d | ||
|
|
268ac8855a | ||
|
|
0f10cc62ec | ||
|
|
99f649c6b7 | ||
|
|
f25ac78538 | ||
|
|
cef24d8c4b | ||
|
|
7a10dfdac1 | ||
|
|
02892e57bb | ||
|
|
524c56a12b | ||
|
|
0e0d7cc7b8 | ||
|
|
1f877e2b8e | ||
|
|
8cd50fbdb4 | ||
|
|
42421d171e | ||
|
|
32215e9a3f | ||
|
|
dd1c7ffc39 | ||
|
|
b59bf62da5 | ||
|
|
f4c32f7b30 | ||
|
|
8844a5304d | ||
|
|
922ddd47f4 | ||
|
|
8c8702c6c9 | ||
|
|
70147fcf5e | ||
|
|
b3ee16e876 | ||
|
|
8d7976190d | ||
|
|
3edae3e678 | ||
|
|
dd2254203c | ||
|
|
f8658e2d77 | ||
|
|
021c3bbb94 | ||
|
|
0a64a96f65 | ||
|
|
48576dc46d | ||
|
|
12de0343b4 | ||
|
|
fcd34a9ff3 | ||
|
|
0dcf904d81 | ||
|
|
4fe92d8ece | ||
|
|
c893ffc177 | ||
|
|
a076ce5756 | ||
|
|
af82227dff | ||
|
|
8f2b177145 | ||
|
|
9a997fbcb0 | ||
|
|
17070471f7 | ||
|
|
cb48221ed3 | ||
|
|
68eb0290e0 | ||
|
|
61bc6a1dc2 | ||
|
|
4a84bf2355 | ||
|
|
2c2a89d9db | ||
|
|
c91e2f0efe | ||
|
|
411d082d2a | ||
|
|
d4e08a1765 | ||
|
|
b529d07479 | ||
|
|
d44df75e5c | ||
|
|
b74e07b608 | ||
|
|
4a868afecd | ||
|
|
1cb9560663 | ||
|
|
8f878673ae | ||
|
|
74a5e37892 | ||
|
|
76a69ecc7e | ||
|
|
f06e3d3efa | ||
|
|
973e7bae42 | ||
|
|
94aa175c1a | ||
|
|
777b766fff | ||
|
|
1adaa93034 | ||
|
|
9853eccd89 | ||
|
|
7699ba3cae | ||
|
|
9ac8b1a6fd | ||
|
|
f476c4724d | ||
|
|
3d12632c9f | ||
|
|
350e59fa6b | ||
|
|
b3d5b3fc8f | ||
|
|
4a02c531b2 | ||
|
|
2dd2abedde | ||
|
|
0d59c04151 | ||
|
|
08e0ede655 | ||
|
|
bcf89ca434 | ||
|
|
5e2f677d0b | ||
|
|
4df372052d | ||
|
|
2c5a0a00ba | ||
|
|
f3295b0fdd | ||
|
|
431d515c26 | ||
|
|
d9e6198992 | ||
|
|
3951cbf266 | ||
|
|
c47c4994ae | ||
|
|
a6072c2abb | ||
|
|
360422f25e | ||
|
|
f135c946bd | ||
|
|
750cc24900 | ||
|
|
46062bf4b9 | ||
|
|
869b2176a7 | ||
|
|
7138c101e3 | ||
|
|
04e26225cd | ||
|
|
f9f2de570f | ||
|
|
1dd598c7be | ||
|
|
c0f04e4f20 | ||
|
|
d3279b9823 | ||
|
|
2ad1f97e12 | ||
|
|
1046f3c2aa | ||
|
|
1afecf01e4 | ||
|
|
3ee7736361 | ||
|
|
0666778fea | ||
|
|
8df90558ab | ||
|
|
c1c03f11b4 | ||
|
|
da9afcd0ad | ||
|
|
bc1fbfa190 | ||
|
|
f3199dda20 | ||
|
|
4d0a28a1a7 | ||
|
|
76831579ad | ||
|
|
c2d752f9e9 | ||
|
|
4c0917556f | ||
|
|
e17b0cf5c5 | ||
|
|
f2647316a5 | ||
|
|
78cc157657 | ||
|
|
f576f990de | ||
|
|
254feb6a3a | ||
|
|
4c5139e9ff | ||
|
|
a055e37d3a | ||
|
|
bef5d6627b | ||
|
|
69767ebdb4 | ||
|
|
53ecd0933e | ||
|
|
d32f783392 | ||
|
|
4d3610cdf7 | ||
|
|
166eebabff | ||
|
|
9f2f1cd577 | ||
|
|
d86b884cab | ||
|
|
8345edd9f7 | ||
|
|
e3821b3f09 | ||
|
|
72ca62eae4 | ||
|
|
075091ed06 | ||
|
|
d0a3dee083 | ||
|
|
6ba9b6973d | ||
|
|
345eccf04c | ||
|
|
127a38b15c | ||
|
|
760db38c11 | ||
|
|
e4729337c8 | ||
|
|
7be226d3fa | ||
|
|
68372a4b7a | ||
|
|
d65f862c36 | ||
|
|
5fa75330cf | ||
|
|
547e3d098e | ||
|
|
0f39a31648 | ||
|
|
f1ddddfe00 | ||
|
|
4e61302156 | ||
|
|
9e3cf418ba | ||
|
|
3e29ec7892 | ||
|
|
f452742cd2 | ||
|
|
b560432b0b | ||
|
|
99e5478ced | ||
|
|
09dba91a37 | ||
|
|
18ec4adac9 | ||
|
|
8bedaa468a | ||
|
|
0ab366fcac | ||
|
|
d664039e54 | ||
|
|
6535ba4f72 | ||
|
|
3b181cff93 | ||
|
|
d1274366a0 | ||
|
|
35a4b0f55f | ||
|
|
399ebd36d7 | ||
|
|
a3552893aa | ||
|
|
b6cdf18c1a | ||
|
|
bd4c7f634d | ||
|
|
160ca540ab | ||
|
|
74c3a77ed1 | ||
|
|
0b527868bc | ||
|
|
0f35458cf7 | ||
|
|
70ad92ca16 | ||
|
|
c0d56aa905 | ||
|
|
ed869f7e81 | ||
|
|
ea42579374 | ||
|
|
72d701df3e | ||
|
|
1191b34fd4 | ||
|
|
ca3d3b2a66 | ||
|
|
2891708060 | ||
|
|
3f59bfac5c | ||
|
|
ee24582dd3 | ||
|
|
0ffb4d5792 | ||
|
|
5a6206f148 | ||
|
|
b1014313d6 | ||
|
|
fcc2f6a195 | ||
|
|
c8ffc79077 | ||
|
|
1a13a41168 | ||
|
|
bf279049c0 | ||
|
|
05cc58f2d7 | ||
|
|
d887881ea0 | ||
|
|
8bb2f3e745 | ||
|
|
e7e6eeda61 | ||
|
|
b6ff2be4df | ||
|
|
a2ea185602 | ||
|
|
5d60dbf3f9 | ||
|
|
66e252a59f | ||
|
|
8050ea1ffb | ||
|
|
04ab48de8e | ||
|
|
521a941792 | ||
|
|
6741850081 | ||
|
|
32f6d8b253 | ||
|
|
80a6b421e8 | ||
|
|
dc454b24ec | ||
|
|
0dce884519 | ||
|
|
d70196e799 | ||
|
|
2c6f127f47 | ||
|
|
72ec4b77d6 | ||
|
|
8b935175bd | ||
|
|
eae9980f5e | ||
|
|
6a7e88ffd6 | ||
|
|
e2071d9486 | ||
|
|
0b0a0c07a0 | ||
|
|
d7b354b9b4 | ||
|
|
78d36af96b | ||
|
|
6355140cd8 | ||
|
|
c224c32d03 | ||
|
|
826ceab5b8 | ||
|
|
a327182cb2 | ||
|
|
a9beb66aef | ||
|
|
ab6cf6c938 | ||
|
|
fc1e85ff16 | ||
|
|
6f98feaaf1 | ||
|
|
345c8b113f | ||
|
|
a95c422de9 | ||
|
|
93319ec2a8 | ||
|
|
e0d5469ae2 | ||
|
|
1f9f330cef | ||
|
|
f74502c711 | ||
|
|
11acd99c10 | ||
|
|
589f61931a | ||
|
|
caab1c2831 | ||
|
|
e701ceeeba | ||
|
|
2194b2975c | ||
|
|
89b25b8985 | ||
|
|
40f1af4434 | ||
|
|
91959527a4 | ||
|
|
46b4482a7d | ||
|
|
d7fc5283f7 | ||
|
|
4bdd8a021c | ||
|
|
c0ccdaf91a | ||
|
|
d9fa1cbb06 | ||
|
|
8858f432b5 | ||
|
|
e7fe41810e | ||
|
|
8f5ec48522 | ||
|
|
56183867a7 | ||
|
|
ea6ce2f552 | ||
|
|
55df728471 | ||
|
|
8a370a260e | ||
|
|
64764c412b | ||
|
|
f2d5c21712 | ||
|
|
6113c42014 | ||
|
|
fd9d1c4acc | ||
|
|
118ebddae6 | ||
|
|
2742144e12 | ||
|
|
83ff64698b | ||
|
|
b5e22c6db8 | ||
|
|
d3a147bbdd | ||
|
|
8eb1b8759b | ||
|
|
0155d3b0b9 | ||
|
|
e47a5b4e0d | ||
|
|
87ecb4e519 | ||
|
|
df524b8a7a | ||
|
|
8a7df423ab | ||
|
|
cafd623c92 | ||
|
|
4df11ef064 | ||
|
|
4012310d99 | ||
|
|
9e9bc88473 | ||
|
|
aa7c08ee00 | ||
|
|
b98de29b07 | ||
|
|
53ade384eb | ||
|
|
c7c2eb4518 | ||
|
|
37fa318258 | ||
|
|
ff7bebb782 | ||
|
|
30bb26f898 | ||
|
|
9c1f4e1690 | ||
|
|
865ee2ca01 | ||
|
|
c2264080bd | ||
|
|
67b622d5a6 | ||
|
|
a534c02d75 | ||
|
|
da890d3074 | ||
|
|
3049aa7a96 | ||
|
|
8b2480ad3b | ||
|
|
b176959836 | ||
|
|
a0c42a5f6e | ||
|
|
e66f674968 | ||
|
|
dd0e0abdc4 | ||
|
|
13f6396eb4 | ||
|
|
7bbaa4fcad | ||
|
|
e931d5eb88 | ||
|
|
4bbfa2f1d7 | ||
|
|
17d997c88e | ||
|
|
dd30d08c68 | ||
|
|
0ea7609ff1 | ||
|
|
28d4b1dd61 | ||
|
|
5179b3e53a | ||
|
|
8ccda10045 | ||
|
|
46fbfbefea | ||
|
|
288b294148 | ||
|
|
b464d238c5 | ||
|
|
e1a78e8ff9 | ||
|
|
2b8eb5f01c | ||
|
|
8f863cf530 | ||
|
|
2351193c51 | ||
|
|
bf2bc70794 | ||
|
|
ebe0b68e8f | ||
|
|
8c87a47f5a | ||
|
|
b8b9a37825 | ||
|
|
13dd6fcee3 | ||
|
|
39c50d3c12 | ||
|
|
29f0075bd8 | ||
|
|
8a96ffbcc0 | ||
|
|
67f68d8101 | ||
|
|
ad59d92cef | ||
|
|
85f97860c5 | ||
|
|
8fd21e76f2 | ||
|
|
cc83ddbe21 | ||
|
|
99fcde1586 | ||
|
|
eab08dfbf3 | ||
|
|
dbf0200cca | ||
|
|
ac44f35299 | ||
|
|
d6a5fdd911 | ||
|
|
4668db716a | ||
|
|
f7cd6b76f2 | ||
|
|
b6d47187f5 | ||
|
|
051fffd41e | ||
|
|
c5480078b3 | ||
|
|
e744e9c4ef | ||
|
|
9f22b8b585 | ||
|
|
27cee0a4e1 | ||
|
|
6d35fc408c | ||
|
|
0607a0fa5c | ||
|
|
ed57d2fafa | ||
|
|
39ef92676b | ||
|
|
7301476228 | ||
|
|
457cc3eecd | ||
|
|
a381069bcc | ||
|
|
146c38e64c | ||
|
|
763c41729e | ||
|
|
0021efebd7 | ||
|
|
5f18a1b13a | ||
|
|
0124448479 | ||
|
|
621f1301b3 | ||
|
|
e76bc80e51 | ||
|
|
a27560e804 | ||
|
|
46452de7b5 | ||
|
|
2aef139577 | ||
|
|
03b11481ed | ||
|
|
8c5cb71812 | ||
|
|
7c59bc1ce5 | ||
|
|
0b60ef0d06 | ||
|
|
eede354d3b | ||
|
|
eb7b5dcc25 | ||
|
|
47e9ce96fc | ||
|
|
4e95bc542c | ||
|
|
e4f321ea7a | ||
|
|
246eb71b75 | ||
|
|
261f50b8ec | ||
|
|
9736d0708a | ||
|
|
02dbe80d2f | ||
|
|
0f239ace17 | ||
|
|
3a82ae8da5 | ||
|
|
c33c9eaab0 | ||
|
|
87f626f3cc | ||
|
|
e88302f1b4 | ||
|
|
5597dffaeb | ||
|
|
7f25d61531 | ||
|
|
15e524c6e6 | ||
|
|
4a1d033ee9 | ||
|
|
8adc88a8c0 | ||
|
|
a62b38eda7 | ||
|
|
fcef784180 | ||
|
|
c3ed4ef6a1 | ||
|
|
b9f768af25 | ||
|
|
47ff883fc7 | ||
|
|
68906c43ff | ||
|
|
c6deed4e6e | ||
|
|
b45cc59322 | ||
|
|
c33a96823b | ||
|
|
d3ab16761d | ||
|
|
70f23f24b0 | ||
|
|
00a8410c94 | ||
|
|
2a17e89a99 | ||
|
|
8fe0992c15 | ||
|
|
a9776b7b53 | ||
|
|
074d359c8e | ||
|
|
7728b4262b | ||
|
|
4905b5a738 | ||
|
|
43a259a1ae | ||
|
|
cffe493db0 | ||
|
|
0042629bf0 | ||
|
|
a7d638cc9a | ||
|
|
f84a79bf74 | ||
|
|
f5a0cb9175 | ||
|
|
f9a5507029 | ||
|
|
5ce32d2f04 | ||
|
|
4908996cac | ||
|
|
ee545a163f | ||
|
|
6e0e5802cc | ||
|
|
0d53843230 | ||
|
|
b65670cd1a | ||
|
|
ba4b5255a2 | ||
|
|
d60af2b451 | ||
|
|
44ac8b2b63 | ||
|
|
b70001c579 | ||
|
|
4a8f5516f6 | ||
|
|
48d11540ae | ||
|
|
84129e3339 | ||
|
|
377d455ec1 | ||
|
|
41650b585a | ||
|
|
52280d7a05 | ||
|
|
0ce81a2df2 | ||
|
|
d9a2bb9a06 | ||
|
|
cb88da7f02 | ||
|
|
5560a4f52d | ||
|
|
e4d951b174 | ||
|
|
6e08bf71c9 | ||
|
|
daaf4b54ef | ||
|
|
3291266f5d | ||
|
|
307f6acd8c | ||
|
|
f1ac9c77e6 | ||
|
|
b434a4e3d7 | ||
|
|
2f209cd59f | ||
|
|
0f585fd5ef | ||
|
|
a152dece9a | ||
|
|
d3b31f7027 | ||
|
|
c00f05fca4 | ||
|
|
92c3a86356 | ||
|
|
341fdc409d | ||
|
|
ebd542f592 | ||
|
|
194b2d9814 | ||
|
|
7aed5cf1ed | ||
|
|
abc88c4979 | ||
|
|
3fa38f71f1 | ||
|
|
d651d956d6 | ||
|
|
6754666845 | ||
|
|
08e6f46b19 | ||
|
|
8f8c8ff367 | ||
|
|
63ec2a8c34 | ||
|
|
f58c8497c3 | ||
|
|
1497fdae56 | ||
|
|
10a3cb40e1 | ||
|
|
dd1ec15a39 | ||
|
|
ea51cec57e | ||
|
|
28ce986a8c | ||
|
|
489b145606 | ||
|
|
5e92bffaa6 | ||
|
|
277d1b0e30 | ||
|
|
13f4ed8d2c | ||
|
|
91cb5ca36c | ||
|
|
c34d54a6cb | ||
|
|
2d1737da1f | ||
|
|
adb0bf2473 | ||
|
|
a1b8b9d47b | ||
|
|
8df14bf9d9 | ||
|
|
c98d265a1e | ||
|
|
4e6782a6b7 | ||
|
|
5541e9e6d0 | ||
|
|
878ab0ef6b | ||
|
|
b61bd36b14 | ||
|
|
bb672d8f46 | ||
|
|
ba1a26543b | ||
|
|
cb868ee7b2 | ||
|
|
5dd5cb12ad | ||
|
|
2dfa83ff22 | ||
|
|
27bb4e1253 | ||
|
|
45afdbdfbb | ||
|
|
11e52a3ade | ||
|
|
4cbbe9e000 | ||
|
|
e986a0acaf | ||
|
|
f5b893cfe0 | ||
|
|
333ec346ef | ||
|
|
2f2db4d445 | ||
|
|
e31883547d | ||
|
|
88c0066b06 | ||
|
|
fdc79b8d77 | ||
|
|
f244795e57 | ||
|
|
5a2aa19d0f | ||
|
|
f731115805 | ||
|
|
67bc065ccd | ||
|
|
d15df3338f | ||
|
|
c74cf38e9f | ||
|
|
81eb92646f | ||
|
|
019a9317e9 | ||
|
|
0e68a922bd | ||
|
|
4e1d81c9f8 | ||
|
|
199164fc4b | ||
|
|
c9c26213df | ||
|
|
b7c57104c4 | ||
|
|
0be08d8882 | ||
|
|
e0abd19636 | ||
|
|
4380041c7f | ||
|
|
65814a4644 | ||
|
|
7237294008 | ||
|
|
214bc8ada9 | ||
|
|
6a1de889b4 | ||
|
|
4a319b2b20 | ||
|
|
9f269d1614 | ||
|
|
4b57771eb1 | ||
|
|
5922be7e15 | ||
|
|
858cfd8d5a | ||
|
|
cbe297dc59 | ||
|
|
de76fed25a | ||
|
|
301509b1db | ||
|
|
a10e61735d | ||
|
|
1ef0193028 | ||
|
|
1e85d02ae4 | ||
|
|
d78a329aa9 | ||
|
|
bfdf238db5 | ||
|
|
234b61e2f8 | ||
|
|
9f43097361 | ||
|
|
f395cac893 | ||
|
|
fe122281fd | ||
|
|
6d788cadbc | ||
|
|
a79a22a74d | ||
|
|
2ed3b68790 | ||
|
|
bd9331ce62 | ||
|
|
14c161b733 | ||
|
|
815cdf8b4a | ||
|
|
7d5503dab2 | ||
|
|
9ba1ad5bd3 | ||
|
|
367d04d0f0 | ||
|
|
75c3ddde19 | ||
|
|
c6e77e42be | ||
|
|
4d0a39eb65 | ||
|
|
10a44c70b6 | ||
|
|
ac03a2dceb | ||
|
|
56248c350f | ||
|
|
244aaf6e20 | ||
|
|
5b044a1917 | ||
|
|
cd25340826 | ||
|
|
ebd8e014c6 | ||
|
|
a0b7d759ac | ||
|
|
09884d3152 | ||
|
|
bef0d73e83 | ||
|
|
8d28ace252 | ||
|
|
39c062f73e | ||
|
|
0e5c9e19e1 | ||
|
|
01f2ef5694 | ||
|
|
c5b62b6ba3 | ||
|
|
bbf583ddb5 | ||
|
|
22ef1a399e | ||
|
|
0733f8878f | ||
|
|
f36a61dbb2 | ||
|
|
6d8936bd74 | ||
|
|
d2b93b3296 | ||
|
|
552fee9bac | ||
|
|
34fe8b324d | ||
|
|
c4671fbf1c | ||
|
|
4bcc06c955 | ||
|
|
348f6d9eaa | ||
|
|
157ffdc34c | ||
|
|
c81d5a1a49 | ||
|
|
a01706d163 | ||
|
|
a8d03c98dc | ||
|
|
68cdd163d3 | ||
|
|
4005a8a3e2 | ||
|
|
3f0153ea4d | ||
|
|
60b50a35f1 | ||
|
|
abd02f04af | ||
|
|
a60aa6f644 | ||
|
|
542409d48d | ||
|
|
1a10b40b17 | ||
|
|
e2124054bf | ||
|
|
3c6e858c35 | ||
|
|
ee3da8aa17 | ||
|
|
8670ae82a3 | ||
|
|
14411a8af6 | ||
|
|
c246470b37 | ||
|
|
48c9d66ab8 | ||
|
|
f474e42b79 | ||
|
|
5553a86ac8 | ||
|
|
01613b2f0d | ||
|
|
a177786063 | ||
|
|
62b2884011 | ||
|
|
6b782f8761 | ||
|
|
0c2560cafb | ||
|
|
c5eeab2fd0 | ||
|
|
6f2fd72af6 | ||
|
|
2d06f1cadb | ||
|
|
af493c117c | ||
|
|
896fef8cce | ||
|
|
89c1972abe | ||
|
|
1627d04958 | ||
|
|
c959c99e45 | ||
|
|
0eac9135c0 | ||
|
|
0203faa8c1 | ||
|
|
35f76cb7ae | ||
|
|
c34232a26c | ||
|
|
b43dd95dc6 | ||
|
|
5331ba83d7 | ||
|
|
a2038b86f1 | ||
|
|
eb066f3485 | ||
|
|
bf98b82cf2 | ||
|
|
edd70b943d | ||
|
|
3cbc823085 | ||
|
|
48becf2c51 | ||
|
|
56c686cd5a | ||
|
|
208273c0dd | ||
|
|
2ff7ca3025 | ||
|
|
61a2361730 | ||
|
|
f80f997a89 | ||
|
|
18529a42c1 | ||
|
|
3e707b4b6e | ||
|
|
62f0a938a8 | ||
|
|
ad3a163d82 | ||
|
|
f5a4503610 | ||
|
|
ec012cf5ed | ||
|
|
d70eceb72c | ||
|
|
f271608114 | ||
|
|
793f0a9c10 | ||
|
|
4f2ec195fc | ||
|
|
e6bc009414 | ||
|
|
20dc8fb5ab | ||
|
|
9a71edfeb0 | ||
|
|
fe3fd664af | ||
|
|
6402755ac6 | ||
|
|
ac8fe049de | ||
|
|
955b391253 | ||
|
|
08c6672841 | ||
|
|
8917050fae | ||
|
|
21daef46f7 | ||
|
|
8ad60b5b64 | ||
|
|
7e17c96c30 | ||
|
|
f17b06767e | ||
|
|
70a29fc623 | ||
|
|
239223be3f | ||
|
|
b112cb320c | ||
|
|
5aaf2ba3ef | ||
|
|
f1e9f46af1 | ||
|
|
8dfef1d118 | ||
|
|
919a621bf8 | ||
|
|
3ac96f464d | ||
|
|
f9f03b81d1 | ||
|
|
42171a9c07 | ||
|
|
f1f00115c9 | ||
|
|
59bff61409 | ||
|
|
778693a804 | ||
|
|
e5b2da225c | ||
|
|
4a988b89a2 | ||
|
|
e5e8807312 | ||
|
|
1376530c2e | ||
|
|
7d34a2154b | ||
|
|
ff335130ae | ||
|
|
0afef0ac0f | ||
|
|
6447f270ea | ||
|
|
81be62e1a4 | ||
|
|
409909ccb1 | ||
|
|
b821b69dbb | ||
|
|
7e2448655e | ||
|
|
a7d2a68639 | ||
|
|
aba51409a7 | ||
|
|
5e5d37cbf1 | ||
|
|
e5a99a0fe4 | ||
|
|
a594cc07f6 | ||
|
|
0a9714fbe7 | ||
|
|
1992934dce | ||
|
|
bb930aec14 | ||
|
|
1d7f2ab701 | ||
|
|
347da6142e | ||
|
|
a9f4dc517a | ||
|
|
9d45f3f3a7 | ||
|
|
256d24718b | ||
|
|
1272b8ef16 | ||
|
|
696162ee52 | ||
|
|
533f993e3a | ||
|
|
738b0af5fb | ||
|
|
5d9bac5e7b | ||
|
|
f376c9703a | ||
|
|
20a62fcf69 | ||
|
|
248d4beed1 | ||
|
|
0e52aff363 | ||
|
|
4ed854d7b8 | ||
|
|
c6ff33c6ab | ||
|
|
6c10cb7dca | ||
|
|
130495f519 | ||
|
|
219d328342 | ||
|
|
c835555a59 | ||
|
|
6652b57a0d | ||
|
|
bf51afedf6 | ||
|
|
39f9400de7 | ||
|
|
ac1d39580b | ||
|
|
9362b34858 | ||
|
|
c6f6c715bd | ||
|
|
6a8106d9ac | ||
|
|
5abbcb62a2 | ||
|
|
2bf94539bd | ||
|
|
91cd8cf380 | ||
|
|
c3de3fa275 | ||
|
|
039752419b | ||
|
|
18c708da58 | ||
|
|
8c08b8ee8a | ||
|
|
015be6008d | ||
|
|
da86384e58 | ||
|
|
86ff6f5eb6 | ||
|
|
ae6979151f | ||
|
|
fd1b5d494e | ||
|
|
cd68760c75 | ||
|
|
13d36412dd | ||
|
|
f2e1ae432c | ||
|
|
0f30f1dcbd | ||
|
|
d070737ef7 | ||
|
|
7e2b180ea5 | ||
|
|
52b62a49c8 | ||
|
|
ab6820c3df | ||
|
|
686002bf3a | ||
|
|
8da45b1ed8 | ||
|
|
b7bf0a6172 | ||
|
|
d562728d56 | ||
|
|
f4f5e88710 | ||
|
|
cc2d8588c4 | ||
|
|
37343bde66 | ||
|
|
ce185e8e8e | ||
|
|
cc20435ca5 | ||
|
|
dd3654c1a7 | ||
|
|
0c89dbce8d | ||
|
|
d01858125c | ||
|
|
e467c2b5fc | ||
|
|
a596056ff8 | ||
|
|
77a1af6b35 | ||
|
|
66050febb6 | ||
|
|
11d94ae8c3 | ||
|
|
055b389353 | ||
|
|
b30016ed08 | ||
|
|
247b41bdb2 | ||
|
|
f0cfd9f921 | ||
|
|
d917b3f00c | ||
|
|
c52236e8a9 | ||
|
|
7b284591bd | ||
|
|
425681ea09 | ||
|
|
d1f7b93d77 | ||
|
|
3a6b9b0287 | ||
|
|
e914d93c25 | ||
|
|
90b479b9d2 | ||
|
|
138ddf122a | ||
|
|
fd7c386c12 | ||
|
|
2fd6659129 | ||
|
|
98eafd704b | ||
|
|
be46997fe2 | ||
|
|
dbdb942156 | ||
|
|
d4cf6f650d | ||
|
|
101931a258 | ||
|
|
15e2535791 | ||
|
|
7763f11f5d | ||
|
|
55087e54d0 | ||
|
|
f8b877fde0 | ||
|
|
7a8102430f | ||
|
|
4031ff2835 | ||
|
|
df700ec7c2 | ||
|
|
337090e7cb | ||
|
|
7753881c01 | ||
|
|
0eca24dcce | ||
|
|
cf6076f504 | ||
|
|
b966f47acb | ||
|
|
0db6a4e524 | ||
|
|
95c6caff5a | ||
|
|
5371431be6 | ||
|
|
da1f7050a6 | ||
|
|
7c15f3ba12 | ||
|
|
a5f3331c24 | ||
|
|
6935ac33ac | ||
|
|
29f3cb9d5c | ||
|
|
dafbed91e7 | ||
|
|
83d64528bb | ||
|
|
6632d365c5 | ||
|
|
9cb4f58dd0 | ||
|
|
6af837bafc | ||
|
|
eb42516f88 | ||
|
|
4b2ffcda12 | ||
|
|
6c6f4ff076 | ||
|
|
245d7601cd | ||
|
|
e265f267e1 | ||
|
|
f58d5f184f | ||
|
|
7886702ef2 | ||
|
|
8007084f8c | ||
|
|
17762d9bd8 | ||
|
|
72947fe20e | ||
|
|
f544fd13c3 | ||
|
|
a6ab19187b | ||
|
|
5b8e78726d | ||
|
|
ec515adc67 | ||
|
|
2d156b09f6 | ||
|
|
50b973a0c3 | ||
|
|
364fa0cbc0 | ||
|
|
a0056eb14c | ||
|
|
f6d3619bbe | ||
|
|
e74de068ea | ||
|
|
ef6be4dfd9 | ||
|
|
436b45c05c | ||
|
|
2893c30f5c | ||
|
|
4604f70a57 | ||
|
|
9e24e240d8 | ||
|
|
9c3f5920da | ||
|
|
0d21faa9d3 | ||
|
|
124e1215e8 | ||
|
|
d2fb0dd749 | ||
|
|
f5cee8b6b5 | ||
|
|
4a41a4cf95 | ||
|
|
bcba5162b7 | ||
|
|
7414b288dc | ||
|
|
3c39ffca72 | ||
|
|
324f1c324d | ||
|
|
646687b8da | ||
|
|
7382186bc4 | ||
|
|
2a6ca9cb97 | ||
|
|
460e065eed | ||
|
|
d4af2d4326 | ||
|
|
7538973b33 | ||
|
|
b65ce87a39 | ||
|
|
209f16af76 | ||
|
|
09e70d70e9 | ||
|
|
f1beb10893 | ||
|
|
5c162009ee | ||
|
|
db547fb378 | ||
|
|
44b005ffdd | ||
|
|
d42b29d673 | ||
|
|
9d724dbb8d | ||
|
|
3554702054 | ||
|
|
96183eb3e0 | ||
|
|
4b5ac6ad03 | ||
|
|
ea1a24fd1e | ||
|
|
9d6a56b496 | ||
|
|
a18bf6aa2f | ||
|
|
8eca2cba58 | ||
|
|
23321ce8e6 | ||
|
|
1949ebb304 | ||
|
|
2eaac168dc | ||
|
|
5c74bb41c9 | ||
|
|
32f138bff5 | ||
|
|
a6836c723a | ||
|
|
9850a0c2bf | ||
|
|
778065f7fb | ||
|
|
3d31ace50b | ||
|
|
2a030622a9 | ||
|
|
3950fc39bc | ||
|
|
8d37447146 | ||
|
|
5562148327 | ||
|
|
1765fd5ff2 | ||
|
|
aa6fd6c70b | ||
|
|
3a4890778f | ||
|
|
7bfe8b3f5b | ||
|
|
af8f07218a | ||
|
|
deb9e24c42 | ||
|
|
7d904afd39 | ||
|
|
ef207f9435 | ||
|
|
18152fe04b | ||
|
|
2b09591524 | ||
|
|
a623f79d97 | ||
|
|
90a3f17a8f | ||
|
|
1175cf9bbf | ||
|
|
b85f798364 | ||
|
|
3003f39e34 | ||
|
|
b57186e894 | ||
|
|
43d73bc493 | ||
|
|
5672bdb406 | ||
|
|
9c6f2ce088 | ||
|
|
ca183d2eb7 | ||
|
|
cf2e1a473e | ||
|
|
59e4c85be5 | ||
|
|
4db15fcac7 | ||
|
|
e03e12539a | ||
|
|
2d64447c08 | ||
|
|
43c5411265 | ||
|
|
db8cc65e08 | ||
|
|
b81eb9be0c | ||
|
|
b1c7bf5b58 | ||
|
|
453237aef8 | ||
|
|
8511432dee | ||
|
|
ac500266f3 | ||
|
|
efed9f3348 | ||
|
|
f1ed79fa4e | ||
|
|
cb7f7b80df | ||
|
|
112f99d6d9 | ||
|
|
00cafb1188 | ||
|
|
8af401eea4 | ||
|
|
446546b69f | ||
|
|
5c26ce215b | ||
|
|
8ca714853a | ||
|
|
577dc0d175 | ||
|
|
4417b61fd1 | ||
|
|
8a6d9d76da | ||
|
|
92acaf6c27 | ||
|
|
4d53b3cb06 | ||
|
|
7cad4ffa37 | ||
|
|
b6f312325f | ||
|
|
43a6492cab | ||
|
|
92e3546e8a | ||
|
|
8a9000cc67 | ||
|
|
6e3514c0b2 | ||
|
|
deb22739b7 | ||
|
|
bc3b24d2f1 | ||
|
|
8caa6e86a1 | ||
|
|
a2efb3ee15 | ||
|
|
08e0cd232d | ||
|
|
2782c8cebe | ||
|
|
5abe9b8a16 | ||
|
|
7801db0331 | ||
|
|
694ba4e32d | ||
|
|
e5c0e41336 | ||
|
|
69435c04cc | ||
|
|
13e29a9966 | ||
|
|
601b0a8964 | ||
|
|
7c2ceb0aca | ||
|
|
42fabd5133 | ||
|
|
2fdb53efc9 | ||
|
|
9e9825a125 | ||
|
|
d012c1e33d | ||
|
|
c8f331675c | ||
|
|
edc7f81486 | ||
|
|
210a8856e2 | ||
|
|
854effc43e | ||
|
|
c531cb11af | ||
|
|
633d3b5af2 | ||
|
|
d6e655fcba | ||
|
|
b64e1c609f | ||
|
|
41e9dba040 | ||
|
|
80cf5c738f | ||
|
|
e5bcb1d179 | ||
|
|
fc23fc7aed | ||
|
|
ebd091a9e0 | ||
|
|
11342e75de | ||
|
|
07e073f526 | ||
|
|
c5457374a8 | ||
|
|
2e1fb21ff9 | ||
|
|
5198349591 | ||
|
|
8a4967525a | ||
|
|
30b068c6e2 | ||
|
|
ea3fff59ac | ||
|
|
5347094466 | ||
|
|
4059e7fb6c | ||
|
|
7f66efcdd5 | ||
|
|
472d472bc1 | ||
|
|
fb18278bdc | ||
|
|
b09ce8296f | ||
|
|
f9d07779a9 | ||
|
|
913e43d84c | ||
|
|
51634c1caf | ||
|
|
0e00da6617 | ||
|
|
4e7b9aaf59 | ||
|
|
5ee6baeaaa | ||
|
|
f11a036c60 | ||
|
|
0ac02ff4ce | ||
|
|
99cc50b5cb | ||
|
|
1d8fb02989 | ||
|
|
122cb1188c | ||
|
|
ca36ade288 | ||
|
|
0877046db7 | ||
|
|
ce9615a00e | ||
|
|
dbe5a41395 | ||
|
|
4a4ca54c6e | ||
|
|
47acb63feb | ||
|
|
038c5d41e2 | ||
|
|
011a795895 | ||
|
|
873a0339d8 | ||
|
|
715da548c8 | ||
|
|
5378c6ba35 | ||
|
|
8799f86ea4 | ||
|
|
686be4acbc | ||
|
|
5744eca37a | ||
|
|
70f8ddb1ba | ||
|
|
be1328cee9 | ||
|
|
c0dbf6fd13 | ||
|
|
ffe9c3e0f8 | ||
|
|
e20b79b0ed | ||
|
|
e04d46db2c | ||
|
|
7341435127 | ||
|
|
8b56f94667 | ||
|
|
f5e98d4ebb | ||
|
|
23a0dba470 | ||
|
|
512371cc25 | ||
|
|
9f15ab5000 | ||
|
|
cd4a06b692 | ||
|
|
d01eadc70f | ||
|
|
629ebae0e9 | ||
|
|
394d4b3c1b | ||
|
|
5ff59f1b07 | ||
|
|
f8127eb585 | ||
|
|
7cd03b0243 | ||
|
|
5379e4cf27 | ||
|
|
5be17c55d2 | ||
|
|
6c1ee922de | ||
|
|
d8c730341a | ||
|
|
432440d6bf | ||
|
|
9c4ea2d09b | ||
|
|
2c50ab0255 | ||
|
|
b85615cece | ||
|
|
349ce6908e | ||
|
|
4275459d45 | ||
|
|
81481c9050 | ||
|
|
3124cc0fef | ||
|
|
5c584ee60d | ||
|
|
c7c7e36c86 | ||
|
|
47d8358272 | ||
|
|
a89a20a374 | ||
|
|
b9d46d9972 | ||
|
|
c1f4de425a | ||
|
|
a0fd152d19 | ||
|
|
1a62e08bab | ||
|
|
edbc59c117 | ||
|
|
cfdd0f8cb2 | ||
|
|
808f30675d | ||
|
|
46072abb41 | ||
|
|
71ffbb9eb5 | ||
|
|
27bbb2297a | ||
|
|
0d235aaef8 | ||
|
|
e22c804deb | ||
|
|
c136e790ef | ||
|
|
3697afd9d6 | ||
|
|
c597c6482a | ||
|
|
dda8c637d8 | ||
|
|
e6d7aaa440 | ||
|
|
028458b33c | ||
|
|
9c7d8099cb | ||
|
|
5640dc332d | ||
|
|
40275c3ef1 | ||
|
|
ebe0b2f335 | ||
|
|
97603e8441 | ||
|
|
72cd444861 | ||
|
|
955b859f2c | ||
|
|
dea5cc9c0c | ||
|
|
d13ab1703e | ||
|
|
61ab6a009b | ||
|
|
a9ae36d362 | ||
|
|
f518395ce5 | ||
|
|
20b17fe378 | ||
|
|
572182180c | ||
|
|
de261099aa | ||
|
|
50f0122955 | ||
|
|
fe9eff923e | ||
|
|
dd36278032 | ||
|
|
a079821976 | ||
|
|
fa233e0a24 | ||
|
|
22306cb4ea | ||
|
|
f2d45a3668 | ||
|
|
db91ff12f7 | ||
|
|
eb841fb73e | ||
|
|
bd0438df76 | ||
|
|
9ca1fc59ef | ||
|
|
84a80a5ec8 | ||
|
|
4b2e248646 | ||
|
|
b90e45590a | ||
|
|
ff93d563a8 | ||
|
|
53228498ed | ||
|
|
8ece82e43a | ||
|
|
8b4684675e | ||
|
|
8cca12fff2 | ||
|
|
a74111612e | ||
|
|
c7799a65c4 | ||
|
|
aabb01c50f | ||
|
|
95e2ada965 | ||
|
|
3fe7d53c76 | ||
|
|
e8634bb1ab | ||
|
|
dbe46b5770 | ||
|
|
6d9fba30b1 | ||
|
|
6a866bf871 | ||
|
|
3c961e4652 | ||
|
|
7abd999420 | ||
|
|
fca8fbb135 | ||
|
|
c67caf18df | ||
|
|
fe956fe4a5 | ||
|
|
0e52f679a2 | ||
|
|
b9500283ec | ||
|
|
441b69b528 | ||
|
|
898bcdc96b | ||
|
|
02bc1fc45e | ||
|
|
5585981dc3 | ||
|
|
a4777f194b | ||
|
|
41aeda8dc0 | ||
|
|
2ed522667e | ||
|
|
1932444666 | ||
|
|
b49b7e963d | ||
|
|
435c11ff27 | ||
|
|
2e93600437 | ||
|
|
faecb70d0f | ||
|
|
92e1ac5c3a | ||
|
|
8963a2117b | ||
|
|
aa300258ab | ||
|
|
48841daff5 | ||
|
|
8878f1ed87 | ||
|
|
f6205d79c0 | ||
|
|
d6d5dac6b3 | ||
|
|
05b979e68a | ||
|
|
9f7d9e4c0d | ||
|
|
98a9fed726 | ||
|
|
720a218259 | ||
|
|
60c0adc6f9 | ||
|
|
bc8c346e68 | ||
|
|
a198b6da0b | ||
|
|
0f3dc35df4 | ||
|
|
7b6e6b046a | ||
|
|
9e503191d6 | ||
|
|
1fd23a0d8d | ||
|
|
3811700a78 | ||
|
|
8762ba3d9c | ||
|
|
c42b5aab5a | ||
|
|
d724899ec0 | ||
|
|
81aacdd76e | ||
|
|
0aa072b4e8 | ||
|
|
6335e9dd8b | ||
|
|
a785289ac9 | ||
|
|
f8bace040c | ||
|
|
d62d597695 | ||
|
|
d938129884 | ||
|
|
327f448321 | ||
|
|
19af3740c1 | ||
|
|
11b1110eed | ||
|
|
682b897e21 | ||
|
|
998ad7623c | ||
|
|
4f1db33abc | ||
|
|
ca6cb60bdd | ||
|
|
133e48a5a9 | ||
|
|
d659d01b1e | ||
|
|
34f73fd84b | ||
|
|
54b87ff79d | ||
|
|
6c2843e7c1 | ||
|
|
6761a31982 | ||
|
|
9401a79b2b | ||
|
|
7a4905d943 | ||
|
|
4db1d2b3a3 | ||
|
|
2ffe2967d6 | ||
|
|
0875c0f266 | ||
|
|
68c7de5199 | ||
|
|
4dfb8597ae | ||
|
|
e21a27ff23 | ||
|
|
91ad7944de | ||
|
|
c86602ebaf | ||
|
|
f75ac292db | ||
|
|
2742c249bf | ||
|
|
36f04849ab | ||
|
|
a60c896e89 | ||
|
|
c442320c7f | ||
|
|
6aeae7e9f5 | ||
|
|
cae79aac48 | ||
|
|
0623f4009a | ||
|
|
06adeb72c4 | ||
|
|
ef044f4fc7 | ||
|
|
7cd4e904ca | ||
|
|
c724494ee7 | ||
|
|
cdb2db348e | ||
|
|
5873d4696f | ||
|
|
613787f49c | ||
|
|
f620874251 | ||
|
|
1f08082a58 | ||
|
|
8f5da1677b | ||
|
|
5439a3a31f | ||
|
|
d92ee23764 | ||
|
|
71ecfc2566 | ||
|
|
c0787e0bb6 | ||
|
|
357da2d236 | ||
|
|
6071241872 | ||
|
|
ab93c67081 | ||
|
|
7af6b833df | ||
|
|
3e4b85aeb5 | ||
|
|
2b6be04c5d | ||
|
|
b2d1c82196 | ||
|
|
a19da7b923 | ||
|
|
4a9a78d07b | ||
|
|
300dbd076f | ||
|
|
ddf52524a8 | ||
|
|
7dcc44b4fc | ||
|
|
75af631c17 | ||
|
|
04dd4fce68 | ||
|
|
2776a95a40 | ||
|
|
dc93b37fd6 | ||
|
|
6502a64cab | ||
|
|
5311e78776 | ||
|
|
35721c1340 | ||
|
|
a76df22cab | ||
|
|
a90f996b24 | ||
|
|
c96d4456ea | ||
|
|
d1df6d993f | ||
|
|
191f8866ae | ||
|
|
e17da4e2ee | ||
|
|
2c3fdb4fdc | ||
|
|
e89c6b68c9 | ||
|
|
51cca31f04 | ||
|
|
e51950aa75 | ||
|
|
4c344e0636 | ||
|
|
90261d1f55 | ||
|
|
fabf93f741 | ||
|
|
ab8ef01c76 | ||
|
|
e463d3a8fe | ||
|
|
a6bc617a3b | ||
|
|
1b1ccdd733 | ||
|
|
8d00e710d5 | ||
|
|
de9e3bdbd5 | ||
|
|
b6e054a73f | ||
|
|
a078b2cf12 | ||
|
|
6f32bf9621 | ||
|
|
ac628b26d9 | ||
|
|
7ba655902b | ||
|
|
05c1fdaa9e | ||
|
|
d7687913a9 | ||
|
|
9e718a2e8a | ||
|
|
cbec2f6d02 | ||
|
|
52eb37d13d | ||
|
|
8e9f43885a | ||
|
|
9eefbcb6f2 | ||
|
|
4d8ebc8c38 | ||
|
|
21cfb6ee6f | ||
|
|
c72ad2b242 | ||
|
|
e83b0a7825 | ||
|
|
a8f2438288 | ||
|
|
d0ceaff6ed | ||
|
|
dbe6272bd8 | ||
|
|
eceaf85807 | ||
|
|
d0606b79b0 | ||
|
|
412f290606 | ||
|
|
21e1acc4f5 | ||
|
|
326aad3c00 | ||
|
|
493c2e9a16 | ||
|
|
51a87e28e2 | ||
|
|
be2ff20f4b | ||
|
|
19c6b2fc32 | ||
|
|
5d249f441b | ||
|
|
852254eaef | ||
|
|
43ea64befa | ||
|
|
0f2cb58897 | ||
|
|
dbece6af7f | ||
|
|
b1e68182bd | ||
|
|
45a64bea78 | ||
|
|
aec8735388 | ||
|
|
1d91faaa49 | ||
|
|
e1e21c0063 | ||
|
|
e775499080 | ||
|
|
735aad5a91 | ||
|
|
fb4e106f69 | ||
|
|
e5659db535 | ||
|
|
5381e09a6c | ||
|
|
21f16ecd68 | ||
|
|
12fc76b326 | ||
|
|
d7f87dd269 | ||
|
|
56227f3713 | ||
|
|
f492fee486 | ||
|
|
41a7814615 | ||
|
|
8644f2c166 | ||
|
|
e4a9365caf | ||
|
|
9fc7af1295 | ||
|
|
d0eeb2b304 | ||
|
|
e4518ebcf1 | ||
|
|
7604cefd0f | ||
|
|
71729d4784 | ||
|
|
1d16bc4968 | ||
|
|
de2bf79004 | ||
|
|
83ed7a9f38 | ||
|
|
c326e72758 | ||
|
|
ac9cef82cc | ||
|
|
ea254d57d2 | ||
|
|
a661f24ae0 | ||
|
|
afabf9256b | ||
|
|
74a8f9c9e2 | ||
|
|
1d11e448f9 | ||
|
|
e3e23cbccb | ||
|
|
79132aa11d | ||
|
|
7bb9e6e951 | ||
|
|
37dc5b4135 | ||
|
|
d588faf470 | ||
|
|
8b51a81158 | ||
|
|
9f125974bf | ||
|
|
d0aed48ca9 | ||
|
|
bf548df6ae | ||
|
|
a3fe105f8e | ||
|
|
5add1d71bc | ||
|
|
7a01cff0c8 | ||
|
|
e8602f7134 | ||
|
|
e9aad2c8d7 | ||
|
|
60d4f3d77c | ||
|
|
9b8c5a3499 | ||
|
|
53dde0607d | ||
|
|
7f034b4ffa | ||
|
|
599ab83100 | ||
|
|
f4a3508ec2 | ||
|
|
44b92909eb | ||
|
|
8ed07b8d1a | ||
|
|
2ff9ced15e | ||
|
|
641b8d71ed | ||
|
|
a31b450f54 | ||
|
|
97bb24c5b9 | ||
|
|
5e5a3639d1 | ||
|
|
0a68a77e28 | ||
|
|
11a0c4142e | ||
|
|
d214d80579 | ||
|
|
ed719fd44e | ||
|
|
5dc6bed0d1 | ||
|
|
b1244a4d4e | ||
|
|
6aa325a4b1 | ||
|
|
88a11561f9 | ||
|
|
fd30022065 | ||
|
|
9486312737 | ||
|
|
e37070a985 | ||
|
|
ffb98ecca2 | ||
|
|
29bd69ef97 | ||
|
|
e46c9530cc | ||
|
|
7ddd303e2d | ||
|
|
66798a1d0f | ||
|
|
bd05afdf14 | ||
|
|
136e48f7ee | ||
|
|
facb5f177a | ||
|
|
10ce31cc46 | ||
|
|
3b4f3c516b | ||
|
|
a1e3981ce4 | ||
|
|
89f26781fe | ||
|
|
914292a80b | ||
|
|
8227e3299b | ||
|
|
07ca48d652 | ||
|
|
243f45c7db | ||
|
|
12cfce3622 | ||
|
|
535c4a8a11 | ||
|
|
6606c671b2 | ||
|
|
242f24840d | ||
|
|
486f636b2d | ||
|
|
b293d7a7cd | ||
|
|
f4fa0b42a6 | ||
|
|
209e89712d | ||
|
|
3314a7a9e9 | ||
|
|
793d64303e | ||
|
|
6642498f00 | ||
|
|
32b400dcb1 | ||
|
|
0dcd2d8179 | ||
|
|
736f8b613c | ||
|
|
9e7d9a937d | ||
|
|
4767983279 | ||
|
|
e37f35d95a | ||
|
|
ad1e609fb9 | ||
|
|
f9bc4a5acd | ||
|
|
2b79185f6a | ||
|
|
840f638472 | ||
|
|
908169a55e | ||
|
|
dbf9f2398e | ||
|
|
2ea3ff0b5c | ||
|
|
91bf72c710 | ||
|
|
baabb70622 | ||
|
|
94ea64a6a9 | ||
|
|
f97896b2c7 | ||
|
|
9027db8587 | ||
|
|
cd46e1c131 | ||
|
|
59211191a4 | ||
|
|
a3ca7e82c7 | ||
|
|
0094056def | ||
|
|
a9f305a1c6 | ||
|
|
e8cc048901 | ||
|
|
05da43f606 | ||
|
|
a81faa7d8e | ||
|
|
18ba7d1da7 | ||
|
|
875adfcbaa | ||
|
|
6e9c213893 | ||
|
|
753066ccb9 | ||
|
|
8b36782c25 | ||
|
|
da9dde6bd2 | ||
|
|
07f6e69b93 | ||
|
|
31a7503df3 | ||
|
|
11db8d8d17 | ||
|
|
93ee8d51bc | ||
|
|
83e80f324e | ||
|
|
c51eac717e | ||
|
|
db7d5dcce3 | ||
|
|
0d25578e22 | ||
|
|
1a457be823 | ||
|
|
20e3edba8f | ||
|
|
036c2182a5 | ||
|
|
6238f430e8 | ||
|
|
9fc891ec01 | ||
|
|
491d977d9e | ||
|
|
9a4bcda9bc | ||
|
|
2c2374a763 | ||
|
|
a76e0b287e | ||
|
|
1d6f1e3c7c | ||
|
|
896fd982a1 | ||
|
|
c031ab20da | ||
|
|
318b6e6bf1 | ||
|
|
ca3999d251 | ||
|
|
658eb278c4 | ||
|
|
bb219889e5 | ||
|
|
3239c9ec3f | ||
|
|
16153dc573 | ||
|
|
e0d9a295ab | ||
|
|
eabdda5eb1 | ||
|
|
43f45f9184 | ||
|
|
7c19785a17 | ||
|
|
78005f8b4e | ||
|
|
0d4784d098 | ||
|
|
805454e037 | ||
|
|
bf383bbf9c | ||
|
|
73ffd67792 | ||
|
|
54bbfc8eda | ||
|
|
a3e234c979 | ||
|
|
9336abff8b | ||
|
|
0fe161cd7f | ||
|
|
7cc55eab3e | ||
|
|
15482e398b | ||
|
|
601fa0ac7f | ||
|
|
2819da5f2f | ||
|
|
3cb3562477 | ||
|
|
cee205994f | ||
|
|
e44df0a3dd | ||
|
|
84a51cb26d | ||
|
|
db02d9c126 | ||
|
|
709b86b724 | ||
|
|
68184b0e47 | ||
|
|
6d2a4c038d | ||
|
|
2f05f5b456 | ||
|
|
d5e3120350 | ||
|
|
a4589327a6 | ||
|
|
c151665419 | ||
|
|
947790e8d1 | ||
|
|
26770439bb | ||
|
|
7da9171dde | ||
|
|
16b386eaf7 | ||
|
|
c330aab48b | ||
|
|
5f998a0852 | ||
|
|
c3dfbb64a6 | ||
|
|
3db52282b8 | ||
|
|
a313ae5f97 | ||
|
|
18cce189a4 | ||
|
|
fb308d576b | ||
|
|
8c976303a4 | ||
|
|
12f1f3609d | ||
|
|
661fdeb6a1 | ||
|
|
d52f9b9543 | ||
|
|
7174742886 | ||
|
|
cd0a8fb24b | ||
|
|
1fbc92bc6d | ||
|
|
231dca956d | ||
|
|
0dd74c825b | ||
|
|
9703fc0366 | ||
|
|
7c3557e943 | ||
|
|
21f153e5c3 | ||
|
|
ea6a0af5a7 | ||
|
|
c53ffaca6c | ||
|
|
3469515e04 | ||
|
|
e8da26cb8a | ||
|
|
1235fc1339 | ||
|
|
47e308b99d | ||
|
|
fdba470e9a | ||
|
|
a1ccceefd2 | ||
|
|
1c4a700d92 | ||
|
|
81c2c3c0e5 | ||
|
|
3c2db5097a | ||
|
|
ce56f79687 | ||
|
|
ee0d6dcdae | ||
|
|
bcf1d92f73 | ||
|
|
ffdec16ce6 | ||
|
|
b2f6e84adc | ||
|
|
f76c457e1f | ||
|
|
80bd0a20df | ||
|
|
efeaf73339 | ||
|
|
91b5100a24 | ||
|
|
d1a06f4730 | ||
|
|
b0b186e951 | ||
|
|
4c8fedef6e | ||
|
|
718c221d01 | ||
|
|
077e77eee5 | ||
|
|
b51ca06c7c | ||
|
|
2f092f4a87 | ||
|
|
f1ff9c05c4 | ||
|
|
c9c8603ccc | ||
|
|
47e281fb61 | ||
|
|
dc625647eb | ||
|
|
66cf1b05be | ||
|
|
622cc89414 | ||
|
|
78d98c40b1 | ||
|
|
1c5f06d9a9 | ||
|
|
998fe5a980 | ||
|
|
8cad4089a7 | ||
|
|
48cc3656bd | ||
|
|
68ddb3a6e1 | ||
|
|
70583f5ba0 | ||
|
|
5bebe01dd0 | ||
|
|
4dd976c9c5 | ||
|
|
221b310485 | ||
|
|
dd1cec70c0 | ||
|
|
7656443b28 | ||
|
|
9d91c13b12 | ||
|
|
7c06141ce2 | ||
|
|
3dc413638b | ||
|
|
bdb8baeddd | ||
|
|
21966bfb69 | ||
|
|
e78c82e999 | ||
|
|
2bdc3468d1 | ||
|
|
987b3dc4ef | ||
|
|
45a10b4ac7 | ||
|
|
b5d33ef629 | ||
|
|
d3629916bf | ||
|
|
c5cb26d295 | ||
|
|
4b2785c5eb | ||
|
|
7ed190e6d2 | ||
|
|
eac041cdd2 | ||
|
|
05527cfc01 | ||
|
|
61e2af4a14 | ||
|
|
79804b6ecd | ||
|
|
76434b2f4e | ||
|
|
ec8bd4922e | ||
|
|
4ffa773fac | ||
|
|
ea8b7bc8aa | ||
|
|
39ce5646f6 | ||
|
|
5092a82739 | ||
|
|
3bba0b6d9a | ||
|
|
7a19dd503d | ||
|
|
9e6a01fefd | ||
|
|
933471b4d9 | ||
|
|
f81808d239 | ||
|
|
96832b6f7d | ||
|
|
e2eb0a84b0 | ||
|
|
c8eb2e3376 | ||
|
|
21fe5822f9 | ||
|
|
d49cc9a7a3 | ||
|
|
910d0bfae1 | ||
|
|
d6761949ca | ||
|
|
6afac1f593 | ||
|
|
4d1a270d22 | ||
|
|
a7888f5536 | ||
|
|
b9049e91cf | ||
|
|
7db56c8e77 | ||
|
|
50563cb957 | ||
|
|
18ae2299a7 | ||
|
|
7463e0aab9 | ||
|
|
c92d47bb95 | ||
|
|
0b1af7df91 | ||
|
|
a9104eb2da | ||
|
|
abbd15d5cc | ||
|
|
aadfa14d59 | ||
|
|
4cd10bbe25 | ||
|
|
1d4a6b71ab | ||
|
|
a7f830dd73 | ||
|
|
bae86ac05c | ||
|
|
a3706bfe21 | ||
|
|
91e23b8c11 | ||
|
|
37ef1c9fab | ||
|
|
6bc6f77af1 | ||
|
|
2c478ccc25 | ||
|
|
404e5492a3 | ||
|
|
d5b5d667a5 | ||
|
|
8807f02f36 | ||
|
|
269e561497 | ||
|
|
527ad81d38 | ||
|
|
972d3c18af | ||
|
|
3cbfc078fc | ||
|
|
fde6822b5c | ||
|
|
930321bcf1 | ||
|
|
c45931363a | ||
|
|
9c6491e5ee | ||
|
|
9bc248f5bc | ||
|
|
becac2fde5 | ||
|
|
1e1a103882 | ||
|
|
e5cffb7c9b | ||
|
|
e2becf7777 | ||
|
|
a6b875a242 | ||
|
|
b5e67f3df8 | ||
|
|
2093fb16a7 | ||
|
|
fc9a9d2386 | ||
|
|
5e69f78f7e | ||
|
|
6919bece77 | ||
|
|
8b003739f1 | ||
|
|
2e9229a6ad | ||
|
|
5a3e7fe8ee | ||
|
|
7b3d7e7bd6 | ||
|
|
fdd7c1864d | ||
|
|
cac5a5adff | ||
|
|
63307633c2 | ||
|
|
387dfa39ff | ||
|
|
1f797f899c | ||
|
|
092bb0a1e2 | ||
|
|
2c3399e237 | ||
|
|
835275b47f | ||
|
|
7b060ce3f9 | ||
|
|
1fb69311b0 | ||
|
|
995d1f61d2 | ||
|
|
80258e9182 | ||
|
|
bd6a32e08e | ||
|
|
5f138de75b | ||
|
|
d0b0f2209a | ||
|
|
0752698c1d | ||
|
|
9855c6b8f5 | ||
|
|
52a7c25540 | ||
|
|
fa823de6b0 | ||
|
|
f53070d8b6 | ||
|
|
7677672691 | ||
|
|
dead8fa168 | ||
|
|
c6347bea45 | ||
|
|
32bd194bfc | ||
|
|
cca48a394d | ||
|
|
a723c8ce37 | ||
|
|
327b2509f6 | ||
|
|
1dae7bd655 | ||
|
|
550a131685 | ||
|
|
0cfb8bb29f | ||
|
|
9c32420a95 | ||
|
|
867093cc88 | ||
|
|
82763f8ec5 | ||
|
|
97449065df | ||
|
|
9489783846 | ||
|
|
f91c9015bc | ||
|
|
302d86056d | ||
|
|
98bebfddaa | ||
|
|
dab20e3187 | ||
|
|
09e72f7c5f | ||
|
|
2028d85f84 | ||
|
|
ed3c0d9014 | ||
|
|
be06150990 | ||
|
|
afb3fb4a31 | ||
|
|
d66577e6c3 | ||
|
|
6a4ea5446a | ||
|
|
74e84c744a | ||
|
|
5ad2446cf3 | ||
|
|
63303bb5c0 | ||
|
|
13393b6624 | ||
|
|
b9fa11c0c3 | ||
|
|
8c6ce1f030 | ||
|
|
1d963d0f0c | ||
|
|
0ee383be27 | ||
|
|
53d09129b4 | ||
|
|
a398c6f311 | ||
|
|
4347ddd42a | ||
|
|
22cb8a6a06 | ||
|
|
7f554fd862 | ||
|
|
a82bfa8a56 | ||
|
|
95784debbf | ||
|
|
2471c5bf0f | ||
|
|
2fe6d731b8 | ||
|
|
ce881372ee | ||
|
|
171ea7c375 | ||
|
|
1e9a6f813f | ||
|
|
39a7f3b2b9 | ||
|
|
8d375a02db | ||
|
|
cac8a0a414 | ||
|
|
c89623967e | ||
|
|
92aa9c1711 | ||
|
|
71f2a58acb | ||
|
|
1f07a8a9e3 | ||
|
|
cacd21bde7 | ||
|
|
a060ec66c3 | ||
|
|
fd10db3c75 | ||
|
|
db4c658980 | ||
|
|
0ee88674f8 | ||
|
|
3540759682 | ||
|
|
44cc8f15b4 | ||
|
|
59f821bf0a | ||
|
|
80858672b0 | ||
|
|
3258d5b255 | ||
|
|
e8c8cc0a9c | ||
|
|
570c19f29f | ||
|
|
ee93fd8636 | ||
|
|
1e6c32ffc7 | ||
|
|
3ef2fb958c | ||
|
|
97edfe7cd7 | ||
|
|
1bdc96f8b2 | ||
|
|
4ef285aee9 | ||
|
|
6ccee3b7cf | ||
|
|
082731ba32 | ||
|
|
0bf85fb644 | ||
|
|
5ce1759dd9 | ||
|
|
1e016dfa24 | ||
|
|
7b3bb53f06 | ||
|
|
53d0059848 | ||
|
|
9a85178a29 | ||
|
|
d74681a128 | ||
|
|
06c8773975 | ||
|
|
ae358dd6d0 | ||
|
|
7174cbf41f | ||
|
|
f73d69e814 | ||
|
|
8af174127d | ||
|
|
991a0aa5f6 | ||
|
|
abc19e78b8 | ||
|
|
836df87e18 | ||
|
|
9cad94e961 | ||
|
|
b9568eb558 | ||
|
|
f951625025 | ||
|
|
c2b3b53c12 | ||
|
|
d95e18c202 | ||
|
|
e705e707e5 | ||
|
|
2fa5d7608f | ||
|
|
f9a3e99795 | ||
|
|
d86ad25f86 | ||
|
|
cf583486e3 | ||
|
|
7366ca59c7 | ||
|
|
12820e6c64 | ||
|
|
71b54fd684 | ||
|
|
aeb1912db6 | ||
|
|
84b2867148 | ||
|
|
5880dacad8 | ||
|
|
b5b67ad958 | ||
|
|
2a913ed24c | ||
|
|
aab56294ba | ||
|
|
26912ef976 | ||
|
|
c1fed3410b | ||
|
|
c853bba4ba | ||
|
|
f340a44abf | ||
|
|
0dec10ddf2 | ||
|
|
7026abe56a | ||
|
|
a9d92115f8 | ||
|
|
6f2d7d96d0 | ||
|
|
532a713355 | ||
|
|
976a9de39c | ||
|
|
32162afa65 | ||
|
|
c1c751a9ab | ||
|
|
b749ba587d | ||
|
|
b2741686fd | ||
|
|
94bf7739a0 | ||
|
|
33d600fb6b | ||
|
|
e2de3d0102 | ||
|
|
6b76adc00e | ||
|
|
61f4cb2f65 | ||
|
|
28bd232dda | ||
|
|
e9e458c877 | ||
|
|
437971ded8 | ||
|
|
3945ac95d1 | ||
|
|
13ab647dc0 | ||
|
|
c75b0ce8fb | ||
|
|
6cc4688660 | ||
|
|
b730f17eb6 | ||
|
|
698782c537 | ||
|
|
2b0faea8ec | ||
|
|
d130c376f4 | ||
|
|
238c55a40e | ||
|
|
b5924bb34f | ||
|
|
1368ee22b2 | ||
|
|
2a0cf57303 | ||
|
|
f10af09bd2 | ||
|
|
850a4eeb7c | ||
|
|
411034902a | ||
|
|
1900ddacbb | ||
|
|
8d084427d2 | ||
|
|
a064c24f60 | ||
|
|
b43882aad0 | ||
|
|
f4ead5ec5c | ||
|
|
ea9ae85428 | ||
|
|
a9a798b19d | ||
|
|
f4ae9df3bf | ||
|
|
f3bcff1261 | ||
|
|
b4bd86549e | ||
|
|
a975718a64 | ||
|
|
3d06a18bcb | ||
|
|
a236089785 | ||
|
|
2f877965cf | ||
|
|
ad5ef95e65 | ||
|
|
8d35ecd711 | ||
|
|
e63c6ac723 | ||
|
|
0984c19fd9 | ||
|
|
a10d3213fd | ||
|
|
f52a0eb02f | ||
|
|
1ea8da69a2 | ||
|
|
5bbc38a7a3 | ||
|
|
aa433bd5ab | ||
|
|
2c5933da0b | ||
|
|
77bc6fbf59 | ||
|
|
701cb7be40 | ||
|
|
ab8d77c968 | ||
|
|
6c03fe678a | ||
|
|
41b30238c3 | ||
|
|
aa768459c0 | ||
|
|
28014512f7 | ||
|
|
f9a99eed66 | ||
|
|
461b574e09 | ||
|
|
36c192ff6b | ||
|
|
101625965c | ||
|
|
83177a3416 | ||
|
|
c3904786e1 | ||
|
|
b31c34905a | ||
|
|
41cbe91870 | ||
|
|
872b16b779 | ||
|
|
9f3cc9c293 | ||
|
|
2d148c4970 | ||
|
|
0869b57741 | ||
|
|
af225aa18f | ||
|
|
06f3c5d32b | ||
|
|
4e71a08b57 | ||
|
|
bf5ebc9245 | ||
|
|
fba81582ab | ||
|
|
b4645168f9 | ||
|
|
d00c68e329 | ||
|
|
cb636b96bf | ||
|
|
12468b5b15 | ||
|
|
6a5414b5fd | ||
|
|
db51fd0ad7 | ||
|
|
256bc4dc1e | ||
|
|
d2bd6e23b6 | ||
|
|
bb12b48887 | ||
|
|
a58e55daf3 | ||
|
|
23a05fe5b0 | ||
|
|
3a63630068 | ||
|
|
565066bbcd | ||
|
|
c10f72cf4c | ||
|
|
af8c21f3d4 | ||
|
|
6f6c3af302 | ||
|
|
61a47808c8 | ||
|
|
e02765bf95 | ||
|
|
b69f193a3e | ||
|
|
7c6526d1ea | ||
|
|
b8776fba65 | ||
|
|
38357dd68d | ||
|
|
d1c2453310 | ||
|
|
ebc1ac50c6 | ||
|
|
892610872f | ||
|
|
a990a40850 | ||
|
|
3f29464dbd | ||
|
|
998d07f3b4 | ||
|
|
949bc6268c | ||
|
|
2c03e5a77e | ||
|
|
aad62dfa6f | ||
|
|
08e27d07ea | ||
|
|
1fddd244e5 | ||
|
|
d85b4b1cf0 | ||
|
|
09fca2c292 | ||
|
|
feda3d18fb | ||
|
|
eb6e5d0756 | ||
|
|
7386daad28 | ||
|
|
3f290b2e1a | ||
|
|
43519ffe80 | ||
|
|
c8bb3d612a | ||
|
|
bc48b7e623 | ||
|
|
d59d5797f6 | ||
|
|
11d3c1e650 | ||
|
|
8cfd9e6694 | ||
|
|
d3f401c54d | ||
|
|
a889170d1a | ||
|
|
459e9f9322 | ||
|
|
707afdcdf9 | ||
|
|
ad1cf379c4 | ||
|
|
582277fe2d | ||
|
|
14b9f814c7 | ||
|
|
b11e5d99b0 | ||
|
|
9590718da4 | ||
|
|
8c2b53cffb | ||
|
|
5a85c073a8 | ||
|
|
2d2fbd0a8b | ||
|
|
1b25a05122 | ||
|
|
709cc1140b | ||
|
|
1730962636 | ||
|
|
a1de4f6f7a | ||
|
|
a5ccda5ed6 | ||
|
|
f035e654ba | ||
|
|
151d3e9f66 | ||
|
|
c79207e197 | ||
|
|
f9d461d9a1 | ||
|
|
3e17bbb90f | ||
|
|
549a7eff7f | ||
|
|
db2e366014 | ||
|
|
26e4215054 | ||
|
|
5f07ff8145 | ||
|
|
e396ba4649 | ||
|
|
d1dff6dedd | ||
|
|
419354cb07 | ||
|
|
7708eaa82c | ||
|
|
9fccf84987 | ||
|
|
0f59788184 | ||
|
|
0ad52bcd3f | ||
|
|
d7d710ec07 | ||
|
|
75a9a3e9af | ||
|
|
70503bedb7 | ||
|
|
7890eac3f8 | ||
|
|
e15f3595b3 | ||
|
|
eebd6a6ba3 | ||
|
|
0407f3e4ac | ||
|
|
5abca84437 | ||
|
|
d2776cc1e6 | ||
|
|
9fe0ee2b77 | ||
|
|
b68daac323 | ||
|
|
665de5dc43 | ||
|
|
e3b280758c | ||
|
|
374ae25d9c | ||
|
|
c86529ac99 | ||
|
|
6309f1fb78 | ||
|
|
c246fb6d8e | ||
|
|
ec6c041bcf | ||
|
|
2da5a9f3c7 | ||
|
|
4e0df52d7c | ||
|
|
71b8bf13e4 | ||
|
|
a8b1e6ce91 | ||
|
|
1419d7611d | ||
|
|
89c83ebf20 | ||
|
|
76d7db88ea | ||
|
|
67a208bc90 | ||
|
|
acbd55ded2 | ||
|
|
11a240a6d1 | ||
|
|
97c85abbe7 | ||
|
|
06a0cd2a3d | ||
|
|
572b215df8 | ||
|
|
2c542bf412 | ||
|
|
1576ba7a01 | ||
|
|
45e4096a12 | ||
|
|
8a1d4fe287 | ||
|
|
98f880ebc2 | ||
|
|
2b852853f3 | ||
|
|
c7a9988033 | ||
|
|
c475eebe1c | ||
|
|
0fe7355ae0 | ||
|
|
57de96e3a2 | ||
|
|
70571cef50 | ||
|
|
0b6deb3340 | ||
|
|
dcda85a825 | ||
|
|
9d3bff018b | ||
|
|
051376e0d2 | ||
|
|
a113785211 | ||
|
|
3f4ed4dc3c | ||
|
|
ac80764fae | ||
|
|
e43afd4891 | ||
|
|
f1aea1d495 | ||
|
|
0e2a5db104 | ||
|
|
3a4c9771fa | ||
|
|
f4f8ef9523 | ||
|
|
b9ace69a72 | ||
|
|
aef0b2a26e | ||
|
|
f7712d71ec | ||
|
|
e94b44e3b8 | ||
|
|
524e863c78 | ||
|
|
bbc80ac901 | ||
|
|
f969ddd6ca | ||
|
|
1cc9781333 | ||
|
|
a609801bae | ||
|
|
d8b606d372 | ||
|
|
572a440e65 | ||
|
|
6e4eeae9b7 | ||
|
|
1a73669df8 | ||
|
|
91ebaf1122 | ||
|
|
46703eb906 | ||
|
|
b9dd9d5193 | ||
|
|
884481a4ec | ||
|
|
9040b37a63 | ||
|
|
99d47b2fa2 | ||
|
|
6575359a94 | ||
|
|
a2fc726372 | ||
|
|
3bfce8ab51 | ||
|
|
ff9a9830f2 | ||
|
|
e2b59e8efe | ||
|
|
04dad9757f | ||
|
|
75ea1080ad | ||
|
|
e25b064319 | ||
|
|
5d0dbc40ce | ||
|
|
beae8de5eb | ||
|
|
c4ff30c722 | ||
|
|
6f4ecb101b | ||
|
|
9f9b0ef846 | ||
|
|
de6957062c | ||
|
|
0a9b43e6fa | ||
|
|
5b0edd9937 | ||
|
|
8a400d202a | ||
|
|
5a1e9f7fb2 | ||
|
|
e03af75cf8 | ||
|
|
0da4919255 | ||
|
|
914e566d1f | ||
|
|
6ec2b653fe | ||
|
|
ba0a088b9c | ||
|
|
478e83bcd9 | ||
|
|
386124a3b9 | ||
|
|
ff5e7c16d1 | ||
|
|
7ff7a66012 | ||
|
|
c99dfb8a86 | ||
|
|
10f9d4c6b3 | ||
|
|
d347813411 | ||
|
|
7a93898b3f | ||
|
|
c057ea900f | ||
|
|
512266e74f | ||
|
|
e36aee11c7 | ||
|
|
97421299f5 | ||
|
|
bc41e5aa80 | ||
|
|
2fa30e7def | ||
|
|
1c6a7d9ba5 | ||
|
|
47435c42a5 | ||
|
|
39a1b421e6 | ||
|
|
b5edf2295b | ||
|
|
fb650a3d7a | ||
|
|
521541f311 | ||
|
|
7020abadbf | ||
|
|
d95fb3b5be | ||
|
|
3e524dc790 | ||
|
|
a64940bff8 | ||
|
|
c739290f0b | ||
|
|
af292fe050 | ||
|
|
634c7fb302 | ||
|
|
33efb94013 | ||
|
|
549e4dc02e | ||
|
|
3d40909c02 | ||
|
|
1aef81e38f | ||
|
|
1b0ae8da58 | ||
|
|
7979a8e97f | ||
|
|
080e53d9a9 | ||
|
|
89bb364b16 | ||
|
|
3586cd941f | ||
|
|
054d0839ac | ||
|
|
dd75f98d85 | ||
|
|
ec23bb5268 | ||
|
|
bc99db4fc1 | ||
|
|
c8275fcfbf | ||
|
|
a345043c30 | ||
|
|
382d37d479 | ||
|
|
32c144a75d | ||
|
|
7ca2aa5e39 | ||
|
|
86cc4a23ac | ||
|
|
08d1e138bd | ||
|
|
a9fe86542f | ||
|
|
4e29776fcd | ||
|
|
ee3eae8f4d | ||
|
|
a84575858a | ||
|
|
ac472291c7 | ||
|
|
f304873c6a | ||
|
|
18caf8face | ||
|
|
d21115aaa8 | ||
|
|
a05ecd2e7f | ||
|
|
32a725126d | ||
|
|
0528690622 | ||
|
|
819339142e | ||
|
|
1d0573e7ff | ||
|
|
00623bc431 | ||
|
|
c872264456 | ||
|
|
1336d3cb9a | ||
|
|
d1459578cd | ||
|
|
8a67fcf40f | ||
|
|
7930370aa9 | ||
|
|
0b854bdcf1 | ||
|
|
cba6aab48d | ||
|
|
12a9ca7a77 | ||
|
|
a6cbd226e1 | ||
|
|
3577e62b41 | ||
|
|
f86e69fcd1 | ||
|
|
292e00b078 | ||
|
|
2a91497bcf | ||
|
|
b0cca0a4c2 | ||
|
|
a2bda85a9c | ||
|
|
20677cff86 | ||
|
|
c8af5d8445 | ||
|
|
2dbe984539 | ||
|
|
6b8fa664f1 | ||
|
|
2b9612e933 | ||
|
|
749d0219fb | ||
|
|
a11a152bd7 | ||
|
|
fc803a3742 | ||
|
|
13a1e15f24 | ||
|
|
3f41b94da5 | ||
|
|
0fb5bfda20 | ||
|
|
dc1fd73ebb | ||
|
|
161b694f71 | ||
|
|
45d1c89e45 | ||
|
|
e26664aa51 | ||
|
|
e29691efbd | ||
|
|
6d45327882 | ||
|
|
fbd41eef49 | ||
|
|
0a30c88322 | ||
|
|
4f5af0e8c8 | ||
|
|
df3f0fd159 | ||
|
|
f2493c79dd | ||
|
|
a86a035b6b | ||
|
|
7995793bfd | ||
|
|
a56b340646 | ||
|
|
7473cdfe16 | ||
|
|
24273ac158 | ||
|
|
fe6275000e | ||
|
|
5fbf369f82 | ||
|
|
4400475ffa | ||
|
|
796eb7c95d | ||
|
|
89a01378e7 | ||
|
|
f4735e5e30 | ||
|
|
f1bb3045aa | ||
|
|
96e474a555 | ||
|
|
833d29b101 | ||
|
|
dce6734ba2 | ||
|
|
0481167dc6 | ||
|
|
a002f93f7b | ||
|
|
3c894fe70e | ||
|
|
8c69b8a1d9 | ||
|
|
a9dae05303 | ||
|
|
ae6994e241 | ||
|
|
caa72fa40c | ||
|
|
46cc9220c3 | ||
|
|
ddb56d7a8e | ||
|
|
a0267416d7 | ||
|
|
56e1ef3602 | ||
|
|
b4fc1057d1 | ||
|
|
06037df607 | ||
|
|
dce134d08d | ||
|
|
cca471d068 | ||
|
|
ddb211b74a | ||
|
|
cef70751ff | ||
|
|
2d2219fc6e | ||
|
|
514a6b4192 | ||
|
|
7a552b3434 | ||
|
|
ecebd1b0e0 | ||
|
|
8dc34d2a88 | ||
|
|
d52644ceec | ||
|
|
3052510591 | ||
|
|
777a5617db | ||
|
|
e17c1087e9 | ||
|
|
633695175a | ||
|
|
9e78bf3d21 | ||
|
|
43aa68a55d | ||
|
|
b8308f8c57 | ||
|
|
466bfbddeb | ||
|
|
b6da07b225 | ||
|
|
2f2159239a | ||
|
|
67d1ca8a65 | ||
|
|
497a393e83 | ||
|
|
782c0e22ea | ||
|
|
2932fc6dfd | ||
|
|
0a9eab2113 | ||
|
|
50a673a8ec | ||
|
|
9e25d0f9e4 | ||
|
|
23cd7be711 | ||
|
|
025b9e33f1 | ||
|
|
bab2f64913 | ||
|
|
b00e09aa9c | ||
|
|
0b109fdc7a | ||
|
|
018fea2ddb | ||
|
|
f8a3cc4352 | ||
|
|
6ab853acc1 | ||
|
|
e825dea02f | ||
|
|
cf8740d16e | ||
|
|
9c4809e26f | ||
|
|
0a232fd9ef | ||
|
|
23016a0791 | ||
|
|
cdcc67ff23 | ||
|
|
92274bfc34 | ||
|
|
2fed6f61ba | ||
|
|
59b2cd26d2 | ||
|
|
f7b87e99d2 | ||
|
|
70bc985145 | ||
|
|
070dbe9108 | ||
|
|
a63fa6d955 | ||
|
|
c7703809b0 | ||
|
|
37eb74338f | ||
|
|
77d5585b7c | ||
|
|
6cab3ef029 | ||
|
|
820a7b78fc | ||
|
|
c51dffef3a | ||
|
|
983bc3da3c | ||
|
|
09be956a58 | ||
|
|
5eded50c53 | ||
|
|
6d8eebd314 | ||
|
|
19a0572b5f | ||
|
|
6272e98474 | ||
|
|
45042fe7d4 | ||
|
|
d85e840126 | ||
|
|
804889f1de | ||
|
|
919c996434 | ||
|
|
00823b3d62 | ||
|
|
af54efd24a | ||
|
|
b1c9b121f6 | ||
|
|
7b5649d153 | ||
|
|
52bf716d84 | ||
|
|
c149dd7b66 | ||
|
|
65d5a1ed63 | ||
|
|
5516754bbb | ||
|
|
08082f2ee3 | ||
|
|
8489266080 | ||
|
|
51c7e0b235 | ||
|
|
628b6b0bb4 | ||
|
|
7e024d860d | ||
|
|
c2f6273f70 | ||
|
|
96e401ec7b | ||
|
|
ae8ac65447 | ||
|
|
2d4f59f36e | ||
|
|
0e85467e02 | ||
|
|
eb41cf5481 | ||
|
|
b970a42d07 | ||
|
|
8c9d123e1c | ||
|
|
ab2a95e347 | ||
|
|
2184c558a4 | ||
|
|
83cb8588fd | ||
|
|
007e82c533 | ||
|
|
499f8580a7 | ||
|
|
a7dc3c5dab | ||
|
|
d01d3a3c53 | ||
|
|
580e062dbf | ||
|
|
c8cee8410c | ||
|
|
6bf331c2e3 | ||
|
|
4c4930737c | ||
|
|
9de01e9525 | ||
|
|
c6a16f5974 | ||
|
|
253ef44d17 | ||
|
|
15a1f00b73 | ||
|
|
b5fa2ea8b8 | ||
|
|
449e024771 | ||
|
|
1bee7a146b | ||
|
|
270a632789 | ||
|
|
418bb05b4c | ||
|
|
052b834151 | ||
|
|
58ee204a75 | ||
|
|
0a02ee8c04 | ||
|
|
950ef4a181 | ||
|
|
7b7cdd8adb | ||
|
|
471768e760 | ||
|
|
c7517d31a4 | ||
|
|
7d10d0398e | ||
|
|
a2bc25c08b | ||
|
|
3cb49fe2d8 | ||
|
|
5b96ac122f | ||
|
|
612033f478 | ||
|
|
48ee940d8e | ||
|
|
e74df0b37d | ||
|
|
640afdc49c | ||
|
|
6b39df5b9b | ||
|
|
e7e698765e | ||
|
|
43fea13dab | ||
|
|
bc899e5bd0 | ||
|
|
160086feb9 | ||
|
|
016391c976 | ||
|
|
91746448a3 | ||
|
|
5cb0543237 | ||
|
|
fac29a24a8 | ||
|
|
4d3a2a21d0 | ||
|
|
6d4f88041c | ||
|
|
18587d3690 | ||
|
|
423090dccd | ||
|
|
78e88baab3 | ||
|
|
6a276767b3 | ||
|
|
2cb26c7c70 | ||
|
|
ff66c88060 | ||
|
|
611e82b8f9 | ||
|
|
59bdee7137 | ||
|
|
e8dbd426ae | ||
|
|
40d6e809a0 | ||
|
|
236c540d18 | ||
|
|
d6ca059f6c | ||
|
|
52c06a60ca | ||
|
|
6353644ec3 | ||
|
|
20df9ded3d | ||
|
|
7569b18a4c | ||
|
|
b9da4f4951 | ||
|
|
89b9e29257 | ||
|
|
d605de9de4 | ||
|
|
d46c94d7c3 | ||
|
|
2db9c00530 | ||
|
|
66d8d159f9 | ||
|
|
9fa1446284 | ||
|
|
b3e4cb48c7 | ||
|
|
0bca7b2247 | ||
|
|
7812e03c9d | ||
|
|
7a852ae5af | ||
|
|
706d9e61c1 | ||
|
|
8f0ed4ff4b | ||
|
|
3415b6f121 | ||
|
|
256ba6fb86 | ||
|
|
d30b2b9afe | ||
|
|
be943ca1fc | ||
|
|
1ddab2a97a | ||
|
|
e15fd4695c | ||
|
|
ffa4b1b4a1 | ||
|
|
f8eee3a2a6 | ||
|
|
eeee7a8343 | ||
|
|
8447b73fcb | ||
|
|
2863945d5f | ||
|
|
cb1f8ca6f7 | ||
|
|
1d9964bcb1 | ||
|
|
15cb8016d3 | ||
|
|
895cc0a2c5 | ||
|
|
20bf349e4e | ||
|
|
e297763da1 | ||
|
|
e471970654 | ||
|
|
12faaaced8 | ||
|
|
083cbc55cc | ||
|
|
8aa7a3273d | ||
|
|
255e2c4385 | ||
|
|
9856306870 | ||
|
|
527ab8b8a7 | ||
|
|
f8e19ba9b3 | ||
|
|
7649dbfbbc | ||
|
|
81e734644d | ||
|
|
ae55cf5b1e | ||
|
|
af539546ef | ||
|
|
0031ce57d0 | ||
|
|
2f48a2ce57 | ||
|
|
6068ab7100 | ||
|
|
29a7dccef4 | ||
|
|
e2073da86e | ||
|
|
ae079526f7 | ||
|
|
947bae8e26 | ||
|
|
a68e29dff6 | ||
|
|
a588d7f960 | ||
|
|
66224e5a32 | ||
|
|
07abad6a14 | ||
|
|
83d02aaaac | ||
|
|
5a27ac165e | ||
|
|
bd9a523233 | ||
|
|
43959b158f | ||
|
|
d81b457bba | ||
|
|
b40d639785 | ||
|
|
0a8d8f4f66 | ||
|
|
d16cb25cde | ||
|
|
7aef1758e0 | ||
|
|
9758756fdd | ||
|
|
13ef35f96f | ||
|
|
6b8c1209b7 | ||
|
|
7184f3053a | ||
|
|
b83eac10e6 | ||
|
|
cb42eaef69 | ||
|
|
0dfd636a7e | ||
|
|
21ff0fd258 | ||
|
|
c2eaeb2c72 | ||
|
|
2a414a4bea | ||
|
|
fc0c38c8af | ||
|
|
595e6c8a0c | ||
|
|
ced16fd221 | ||
|
|
0817c3f148 | ||
|
|
fb40af81ac | ||
|
|
1c5ad05e89 | ||
|
|
86bef566c4 | ||
|
|
0983ccb61e | ||
|
|
a1d9f469c0 | ||
|
|
952124f783 | ||
|
|
6be12e8ace | ||
|
|
0799f380e1 | ||
|
|
f65270ee7e | ||
|
|
414910719c | ||
|
|
10a1e8faa6 | ||
|
|
4eea21927e | ||
|
|
48c7f659f9 | ||
|
|
b33333f4aa | ||
|
|
9edb32b081 | ||
|
|
c9b25fe806 | ||
|
|
b6ee3939be | ||
|
|
e5485cddd0 | ||
|
|
ac81597236 | ||
|
|
58d991df0a | ||
|
|
3f8e380da4 | ||
|
|
ae831a2654 | ||
|
|
ae72cf2283 | ||
|
|
8164f4b506 | ||
|
|
9617be0ca4 | ||
|
|
f079d7b9fa | ||
|
|
00afda452f | ||
|
|
70386abadd | ||
|
|
5865ac017c | ||
|
|
4061a92f8e | ||
|
|
d37c31b31c | ||
|
|
973ef0078f | ||
|
|
48dcd257da | ||
|
|
da03911610 | ||
|
|
aba9d945b5 | ||
|
|
b6f7f3b73f | ||
|
|
2050d20ea7 | ||
|
|
ac1fb4a63a | ||
|
|
ced38490e1 | ||
|
|
ad28b69198 | ||
|
|
8c67d3c58f | ||
|
|
7171817de8 | ||
|
|
73f9d674e1 | ||
|
|
5e046399f8 | ||
|
|
4966cd9ac7 | ||
|
|
da936ecfe3 | ||
|
|
89e10d43de | ||
|
|
3bf289af69 | ||
|
|
c7c9a6c5ca | ||
|
|
aee8446a23 | ||
|
|
2bb4f1fbb8 | ||
|
|
6e7b0ee4ff | ||
|
|
204f5b9a54 | ||
|
|
8c41e3506f | ||
|
|
c2c33e45b8 | ||
|
|
1acaf4e58b | ||
|
|
eca80d5a4c | ||
|
|
f538957be9 | ||
|
|
82a839a60a | ||
|
|
df494da9e4 | ||
|
|
1ea53f7f04 | ||
|
|
ac6d695f6d | ||
|
|
73dccb21f5 | ||
|
|
4221102ad5 | ||
|
|
b100f12e7f | ||
|
|
2069ba6836 | ||
|
|
ea57976808 | ||
|
|
4055d3542b | ||
|
|
0b0271a1f4 | ||
|
|
e03585ad4d | ||
|
|
11a385791e | ||
|
|
e228225178 | ||
|
|
1c96d971e1 | ||
|
|
b799de7995 | ||
|
|
b01d246555 | ||
|
|
9363b073cf | ||
|
|
12ca04ac6f | ||
|
|
51737c28bd | ||
|
|
50d5ec224a | ||
|
|
95a7397d14 | ||
|
|
aedac6d22c | ||
|
|
d522975ecc | ||
|
|
68fda8d7f3 | ||
|
|
b0cfec9913 | ||
|
|
ba8eba1581 | ||
|
|
f9eaed41c1 | ||
|
|
1202a62df7 | ||
|
|
8c1f7796f6 | ||
|
|
42aee35789 | ||
|
|
b628849caa | ||
|
|
031f08b0d4 | ||
|
|
fab6f9b93f | ||
|
|
564c5d937d | ||
|
|
2d3bb01487 | ||
|
|
607ea2d293 | ||
|
|
d817b53780 | ||
|
|
e8a2cbe06a | ||
|
|
d2b0577752 | ||
|
|
b4edd5cbad | ||
|
|
348477747e | ||
|
|
bb7ee174ea | ||
|
|
ab5add14ef | ||
|
|
44f4820cee | ||
|
|
8f1609b944 | ||
|
|
66b5b75631 | ||
|
|
17e293afe8 | ||
|
|
1cf35f59fd | ||
|
|
bb4b897934 | ||
|
|
0eaf1af2e3 | ||
|
|
f70c12540b | ||
|
|
479fe73c24 | ||
|
|
f6cad85476 | ||
|
|
888197e6ce | ||
|
|
e634305759 | ||
|
|
fe054211f4 | ||
|
|
f102a29ea0 | ||
|
|
2b8bd45bcd | ||
|
|
7f730c4be0 | ||
|
|
b6e31cac23 | ||
|
|
9fe4f218d5 | ||
|
|
cc38cc2676 | ||
|
|
f56c6876d1 | ||
|
|
196e424c88 | ||
|
|
9270dc2c52 | ||
|
|
14aec251b4 | ||
|
|
d2a7a57245 | ||
|
|
1964fc76c8 | ||
|
|
b8d4b490ce | ||
|
|
76891e4855 | ||
|
|
3d868b3a39 | ||
|
|
7b56bcf7a9 | ||
|
|
f96ae56bce | ||
|
|
d52108f4e1 | ||
|
|
5f07b7ad1f | ||
|
|
cda10cf1a6 | ||
|
|
d226b8ebc5 | ||
|
|
d08794579c | ||
|
|
7450494741 | ||
|
|
36dca7ae2f | ||
|
|
5dae777e79 | ||
|
|
e518d172d7 | ||
|
|
af29277acd | ||
|
|
79bfa0792d | ||
|
|
cf23c5d31c | ||
|
|
84418a296b | ||
|
|
5f83cc6bb7 | ||
|
|
cde168c93c | ||
|
|
fed24c0748 | ||
|
|
b45d11b3c3 | ||
|
|
84d9af69bb | ||
|
|
684d356646 | ||
|
|
975300c9fc | ||
|
|
ca349e33fc | ||
|
|
ccf62fe95c | ||
|
|
d056cb6769 | ||
|
|
b0016eebf9 | ||
|
|
0490ad9207 | ||
|
|
4a20ae236b | ||
|
|
9be1c7fc6f | ||
|
|
5621d32b30 | ||
|
|
b7642fe876 | ||
|
|
c842485d33 | ||
|
|
341444ef1c | ||
|
|
66f5a219d2 | ||
|
|
cf678aa345 | ||
|
|
d1549b3df0 | ||
|
|
002919fffe | ||
|
|
087d097204 | ||
|
|
ca4eeda6f0 | ||
|
|
94543a4708 | ||
|
|
d4738dfb46 | ||
|
|
3bdf6810aa | ||
|
|
f489c2f3b4 | ||
|
|
a724bfe155 | ||
|
|
179a372bfe | ||
|
|
651d765ab0 | ||
|
|
7ddc853f63 | ||
|
|
1bd1bfc725 | ||
|
|
f6ec0fda7a | ||
|
|
7be368ae8c | ||
|
|
f67db2617b | ||
|
|
ed5bf8100f | ||
|
|
0ef8a1c9ae | ||
|
|
32460cbf78 | ||
|
|
6f6c9c222c | ||
|
|
438d0ed1ea | ||
|
|
3ef1c71cad | ||
|
|
aaadf6b8ba | ||
|
|
6af614f319 | ||
|
|
c75dbd67df | ||
|
|
dc3d186e2a | ||
|
|
44550feddd | ||
|
|
a0810d5f63 | ||
|
|
cfc97fb22d | ||
|
|
d67dbe8062 | ||
|
|
e89035e11c | ||
|
|
2ea711e629 | ||
|
|
a716f071be | ||
|
|
3450a91824 | ||
|
|
d2c2b457e5 | ||
|
|
9cd7e49804 | ||
|
|
e9155e836f | ||
|
|
ed248539c7 | ||
|
|
54cc75506f | ||
|
|
4269c7927e | ||
|
|
064ac7f603 | ||
|
|
48ccf15273 | ||
|
|
b920ced6d4 | ||
|
|
69610a674c | ||
|
|
1828e34190 | ||
|
|
d53f4e3917 | ||
|
|
01706d5b4e | ||
|
|
8916b8a450 | ||
|
|
ed33af5638 | ||
|
|
c94a9e1ae6 | ||
|
|
e2e93afd06 | ||
|
|
a810158d5b | ||
|
|
5a5ebb95fc | ||
|
|
61dd9e29c0 | ||
|
|
ac65d81ba1 | ||
|
|
3aca987176 | ||
|
|
7288d3cb15 | ||
|
|
7477c7c67f | ||
|
|
453952859e | ||
|
|
85d46089e3 | ||
|
|
3b55f706de | ||
|
|
f448276423 | ||
|
|
830ee704da | ||
|
|
393369e446 | ||
|
|
2cc6a09905 | ||
|
|
d7d9d88e16 | ||
|
|
357d6aaf75 | ||
|
|
8059c422e3 | ||
|
|
b336e1334d | ||
|
|
12a0942ddb | ||
|
|
7e5a77f77e | ||
|
|
e0caeb5dd2 | ||
|
|
77076f3bdd | ||
|
|
2933d4843f | ||
|
|
c5de978098 | ||
|
|
8b9cfab072 | ||
|
|
ea5f3c222f | ||
|
|
36bcbca15b | ||
|
|
2b2060e71b | ||
|
|
451688f2df | ||
|
|
d993852de7 | ||
|
|
9d73770a4e | ||
|
|
2541acf9d2 | ||
|
|
a1bfbad24e | ||
|
|
8af4918048 | ||
|
|
49f4ab0ec8 | ||
|
|
85c623fb0f | ||
|
|
9e28298250 | ||
|
|
7a04ef0985 | ||
|
|
83005e9ba9 | ||
|
|
f0c78f0529 | ||
|
|
3f638adcf9 | ||
|
|
d9405d8d5d | ||
|
|
606713a418 | ||
|
|
52102f0d0a | ||
|
|
61c29829ed | ||
|
|
df30931aad | ||
|
|
5afcc03e8b | ||
|
|
fbeb4673f4 | ||
|
|
4aba319560 | ||
|
|
74f79e002c | ||
|
|
2668ef2b3f | ||
|
|
74c018e271 | ||
|
|
64776fd601 | ||
|
|
59877bf71d | ||
|
|
d2800ac58b | ||
|
|
ffef944119 | ||
|
|
651b291ef6 | ||
|
|
e4b581f197 | ||
|
|
4f3939e2d9 | ||
|
|
1048ca612d | ||
|
|
b1a2d21ee9 | ||
|
|
dd4e8bdc8b | ||
|
|
e28c9bae0c | ||
|
|
5c10f520fb | ||
|
|
f8abe90674 | ||
|
|
964ad42cb4 | ||
|
|
424b970469 | ||
|
|
792366e221 | ||
|
|
79e970c4c3 | ||
|
|
d12acd5f31 | ||
|
|
13e55e05a4 | ||
|
|
9a7490bc2f | ||
|
|
a610a9d3d3 | ||
|
|
56e906c83f | ||
|
|
101f26e5a3 | ||
|
|
0bba205cf2 | ||
|
|
cc3beb191f | ||
|
|
42f5092bb9 | ||
|
|
bc6728d123 | ||
|
|
754278f80f | ||
|
|
c9c980b6fe | ||
|
|
a457d13d2c | ||
|
|
7440e9e5d2 | ||
|
|
39d901a5cb | ||
|
|
2e1ebff985 | ||
|
|
b8ed9ba321 | ||
|
|
c89a8e1cd1 | ||
|
|
480d201c55 | ||
|
|
a4b7d4a012 | ||
|
|
7fe676712b | ||
|
|
552733129c | ||
|
|
a4d73090f8 | ||
|
|
7d39b72800 | ||
|
|
f1e12563e9 | ||
|
|
0ac5e5b35e | ||
|
|
6b3f74a39a | ||
|
|
3c3e2e86c3 | ||
|
|
204a778db2 | ||
|
|
3594e64bfc | ||
|
|
c23d114094 | ||
|
|
6cb3fdc7c9 | ||
|
|
c57642bd4e | ||
|
|
891ee0fac8 | ||
|
|
1b69f0b668 | ||
|
|
46b310ceb9 | ||
|
|
85fe44ec92 | ||
|
|
fdcec0fbf7 | ||
|
|
2664ea8622 | ||
|
|
862724da74 | ||
|
|
a1c167fb7f | ||
|
|
adc2290fc1 | ||
|
|
8713fd8130 | ||
|
|
77df3d1ae5 | ||
|
|
2234e9db0e | ||
|
|
dd3d403de8 | ||
|
|
5364c36a79 | ||
|
|
118fbe3f7d | ||
|
|
61ec8e96f2 | ||
|
|
19289527ae | ||
|
|
77fdd6ddb8 | ||
|
|
f7830b5e9d | ||
|
|
13e5d76a44 | ||
|
|
7b8ad2e315 | ||
|
|
623f094e5b | ||
|
|
fd25d61b56 | ||
|
|
6f5802551f | ||
|
|
cbab824fd0 | ||
|
|
0c3d911e74 | ||
|
|
e161343d72 | ||
|
|
4984896c95 | ||
|
|
28d1f5ead9 | ||
|
|
5044f757fb | ||
|
|
aa28b5aead | ||
|
|
5ada507c2b | ||
|
|
48be080fe0 | ||
|
|
cc2442e761 | ||
|
|
a5560823d9 | ||
|
|
9cd313c4df | ||
|
|
173f05a8ae | ||
|
|
29819668e3 | ||
|
|
9cbb9734f2 | ||
|
|
cf897410ee | ||
|
|
bf1896f959 | ||
|
|
e7c79a5156 | ||
|
|
eb60c1b0a0 | ||
|
|
6b1b69c741 | ||
|
|
352930694a | ||
|
|
215ed7ab0e | ||
|
|
2da5883d7a | ||
|
|
b6d731cf87 | ||
|
|
bef918749d | ||
|
|
96b7674644 | ||
|
|
d0bcf6940a | ||
|
|
c35fd9c4b7 | ||
|
|
0bb5923257 | ||
|
|
41752aff60 | ||
|
|
e873d81b63 | ||
|
|
7489a11ab3 | ||
|
|
86b7a8482c | ||
|
|
731dedf155 | ||
|
|
131297d859 | ||
|
|
7ce9687702 | ||
|
|
d56163b19b | ||
|
|
e0f8a04f8e | ||
|
|
3c08741cb6 | ||
|
|
c902822723 | ||
|
|
13f31d3fae | ||
|
|
70268c0cbb | ||
|
|
04eaf9f3e9 | ||
|
|
fd57b7df18 | ||
|
|
d72c364962 | ||
|
|
618487947b | ||
|
|
e75140d732 | ||
|
|
e09f6105a1 | ||
|
|
d3a6928e3a | ||
|
|
8b2128b4dc | ||
|
|
0773490c77 | ||
|
|
c2610a32e4 | ||
|
|
aaf72de552 | ||
|
|
65664ae178 | ||
|
|
742600fc4f | ||
|
|
6531aae617 | ||
|
|
842748947f | ||
|
|
b733f8f55b | ||
|
|
b7ae1fa516 | ||
|
|
64c587c17d | ||
|
|
133d8bbeef | ||
|
|
7d3bc4203e | ||
|
|
24a10265f3 | ||
|
|
351039dc3b | ||
|
|
3ed9f1a532 | ||
|
|
99282100a0 | ||
|
|
057d8a05d7 | ||
|
|
71b69a3226 | ||
|
|
d1a5c9a090 | ||
|
|
2b20d946e6 | ||
|
|
3c96e1298c | ||
|
|
4e54c24bf0 | ||
|
|
2894309fa6 | ||
|
|
fbd53dae7c | ||
|
|
ba2c362082 | ||
|
|
680085d16f | ||
|
|
2319c7eae2 | ||
|
|
645099ecf2 | ||
|
|
d51a0a644a | ||
|
|
37abc79551 | ||
|
|
20bdc7de58 | ||
|
|
690e542f37 | ||
|
|
33f80c8d16 | ||
|
|
e01cc09a28 | ||
|
|
120ec98ba7 | ||
|
|
b4938ba1fb | ||
|
|
41d0082cee | ||
|
|
0e786660b4 | ||
|
|
6af55d8a1d | ||
|
|
2c9e7f70f2 | ||
|
|
42819daf0f | ||
|
|
08d86dbd30 | ||
|
|
32e8f08398 | ||
|
|
78c73def8a | ||
|
|
82d845b5c8 | ||
|
|
a6bda0dec7 | ||
|
|
40fd9b0579 | ||
|
|
3eda4382b2 | ||
|
|
fd2812a30b | ||
|
|
fd27a7c999 | ||
|
|
870aba0560 | ||
|
|
37153e7360 | ||
|
|
f06b16437c | ||
|
|
e582780195 | ||
|
|
df9e89deb7 | ||
|
|
b4033b2902 | ||
|
|
023ed21363 | ||
|
|
52d6721ae2 | ||
|
|
fa967c3c89 | ||
|
|
6d81821557 | ||
|
|
56664f9fbc | ||
|
|
eb1564a3dd | ||
|
|
d5c6d43ddf | ||
|
|
da5b1cf3fa | ||
|
|
4232ab6f47 | ||
|
|
78c1ad16ce | ||
|
|
9962a6ebcc | ||
|
|
36def20a07 | ||
|
|
c7689d3c89 | ||
|
|
dfa8621a1a | ||
|
|
f884313d72 | ||
|
|
7afe5f39bf | ||
|
|
01bc529b93 | ||
|
|
77bf1c7d8e | ||
|
|
9d31d8b071 | ||
|
|
5256d3c718 | ||
|
|
c662e2c4e3 | ||
|
|
06264354cf | ||
|
|
2dcbe87986 | ||
|
|
8506cdae8f | ||
|
|
bf7487fafe | ||
|
|
0afc2d5903 | ||
|
|
2d62b5937e | ||
|
|
97ddb10ff5 | ||
|
|
2a74c8e053 | ||
|
|
bd920cedf5 | ||
|
|
ee76929fee | ||
|
|
8bb8a72060 | ||
|
|
dcc5d40a04 | ||
|
|
3bebeb4d99 | ||
|
|
d2922afce2 | ||
|
|
5dfd0a9b50 | ||
|
|
67cfe654b8 | ||
|
|
7ca8dcfb6a | ||
|
|
948b0f4df9 | ||
|
|
584cacba6c | ||
|
|
45ed06be64 | ||
|
|
3dec627d40 |
30
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: 漏洞反馈
|
||||
description: 【供中文用户】报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭。由于自己操作不当/不甚了解所用技术栈引起的网络连接问题恕无法解决,请勿提 issue。容器间网络连接问题,参考文档 https://docs.langbot.app/zh/workshop/network-details.html
|
||||
title: "[Bug]: "
|
||||
labels: ["bug?"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: 运行环境
|
||||
description: LangBot 版本、操作系统、系统架构、**Python版本**、**主机地理位置**
|
||||
placeholder: 例如:v3.3.0、CentOS x64 Python 3.10.3、Docker
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 异常情况
|
||||
description: 完整描述异常情况,什么时候发生的、发生了什么。**请附带日志信息。**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 复现步骤
|
||||
description: 提供越多信息,我们会越快解决问题,建议多提供配置截图;**如果你不认真填写(只一两句话概括),我们会很生气并且立即关闭 issue 或两年后才回复你**
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 启用的插件
|
||||
description: 有些情况可能和插件功能有关,建议提供插件启用情况。
|
||||
validations:
|
||||
required: false
|
||||
30
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug-report_en.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
title: "[Bug]: "
|
||||
labels: ["bug?"]
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Runtime environment
|
||||
description: LangBot version, operating system, system architecture, **Python version**, **host location**
|
||||
placeholder: "For example: v3.3.0, CentOS x64 Python 3.10.3, Docker"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Exception
|
||||
description: Describe the exception in detail, what happened and when it happened. **Please include log information.**
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
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. 【注意】请务必认真填写此部分,若不提供完整信息(如只有一两句话的概括),我们将不会回复!
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Enabled plugins
|
||||
description: Some cases may be related to plugin functionality, so please provide the plugin enablement status.
|
||||
validations:
|
||||
required: false
|
||||
21
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 需求建议
|
||||
title: "[Feature]: "
|
||||
labels: []
|
||||
description: "【供中文用户】新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 这是一个?
|
||||
description: 新功能建议还是现有功能优化
|
||||
options:
|
||||
- 新功能
|
||||
- 现有功能优化
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 详细描述
|
||||
description: 详细描述,越详细越好
|
||||
validations:
|
||||
required: true
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature-request_en.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature-request_en.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Feature request
|
||||
title: "[Feature]: "
|
||||
labels: []
|
||||
description: "New features or existing feature improvements should use this template; issues that do not match will be closed directly"
|
||||
body:
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: This is a?
|
||||
description: New feature request or existing feature improvement
|
||||
options:
|
||||
- New feature
|
||||
- Existing feature improvement
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Detailed description
|
||||
description: Detailed description, the more detailed the better
|
||||
validations:
|
||||
required: true
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: 提交新插件
|
||||
title: "[Plugin]: 请求登记新插件"
|
||||
labels: ["独立插件"]
|
||||
description: "【供中文用户】本模板供且仅供提交新插件使用"
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: 插件名称
|
||||
description: 填写插件的名称
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 插件代码库地址
|
||||
description: 仅支持 Github
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 插件简介
|
||||
description: 插件的简介
|
||||
validations:
|
||||
required: true
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/submit-plugin_en.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/submit-plugin_en.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Submit a new plugin
|
||||
title: "[Plugin]: Request to register a new plugin"
|
||||
labels: ["Independent Plugin"]
|
||||
description: "This template is only for submitting new plugins"
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Plugin name
|
||||
description: Fill in the name of the plugin
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Plugin code repository address
|
||||
description: Only support Github
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Plugin description
|
||||
description: The description of the plugin
|
||||
validations:
|
||||
required: true
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
22
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: 漏洞反馈
|
||||
about: 报错或漏洞请使用这个模板创建
|
||||
title: "[BUG]"
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**运行环境**
|
||||
- 部署方式:
|
||||
手动部署/自动部署/Docker部署
|
||||
- 系统环境:
|
||||
例如: Centos x64
|
||||
- Python环境(仅手动部署填写):
|
||||
例如: Python 3.10.9
|
||||
|
||||
**描述漏洞**
|
||||
什么时候发生的,mirai还是主程序,越详细越好
|
||||
|
||||
**完整报错信息**
|
||||
完整的报错信息
|
||||
10
.github/ISSUE_TEMPLATE/需求建议.md
vendored
10
.github/ISSUE_TEMPLATE/需求建议.md
vendored
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 软件优化建议请使用这个模板创建
|
||||
title: "[ENHANCE]"
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
不是需求建议请勿填写此模板!!!!
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
- dependency-name: "openai"
|
||||
32
.github/pull_request_template.md
vendored
Normal file
32
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
## 概述 / Overview
|
||||
|
||||
> 请在此部分填写你实现/解决/优化的内容:
|
||||
> 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
|
||||
|
||||
### PR 作者完成 / For PR author
|
||||
|
||||
*请在方括号间写`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)?
|
||||
- [ ] 与项目所有者沟通过了吗? / Have you communicated with the project maintainer?
|
||||
- [ ] 我确定已自行测试所作的更改,确保功能符合预期。 / I have tested the changes and ensured they work as expected.
|
||||
|
||||
### 项目维护者完成 / For project maintainer
|
||||
|
||||
- [ ] 相关 issues 链接了吗? / Have you linked the related issues?
|
||||
- [ ] 配置项写好了吗?迁移写好了吗?生效了吗? / Have you written the configuration items? Have you written the migration? Has it taken effect?
|
||||
- [ ] 依赖加到 pyproject.toml 和 core/bootutils/deps.py 了吗 / Have you added the dependencies to pyproject.toml and core/bootutils/deps.py?
|
||||
- [ ] 文档编写了吗? / Have you written the documentation?
|
||||
29
.github/workflows/build-dev-image.yaml
vendored
Normal file
29
.github/workflows/build-dev-image.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Build Dev Image
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-dev-image:
|
||||
runs-on: ubuntu-latest
|
||||
# 如果是tag则跳过
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Generate Tag
|
||||
id: generate_tag
|
||||
run: |
|
||||
# 获取分支名称,把/替换为-
|
||||
echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/\//-/g'
|
||||
echo ::set-output name=tag::$(echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/\//-/g')
|
||||
- name: Login to Registry
|
||||
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker buildx create --name mybuilder --use
|
||||
docker build -t rockchin/langbot:${{ steps.generate_tag.outputs.tag }} . --push
|
||||
48
.github/workflows/build-docker-image.yml
vendored
Normal file
48
.github/workflows/build-docker-image.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Build Docker Image
|
||||
on:
|
||||
## 发布release的时候会自动构建
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
publish-docker-image:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build image
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: judge has env GITHUB_REF # 如果没有GITHUB_REF环境变量,则把github.ref变量赋值给GITHUB_REF
|
||||
run: |
|
||||
if [ -z "$GITHUB_REF" ]; then
|
||||
export GITHUB_REF=${{ github.ref }}
|
||||
echo $GITHUB_REF
|
||||
fi
|
||||
- name: Check version
|
||||
id: check_version
|
||||
run: |
|
||||
echo $GITHUB_REF
|
||||
# 如果是tag,则去掉refs/tags/前缀
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "It's a tag"
|
||||
echo $GITHUB_REF
|
||||
echo $GITHUB_REF | awk -F '/' '{print $3}'
|
||||
echo ::set-output name=version::$(echo $GITHUB_REF | awk -F '/' '{print $3}')
|
||||
else
|
||||
echo "It's not a tag"
|
||||
echo $GITHUB_REF
|
||||
echo ::set-output name=version::${GITHUB_REF}
|
||||
fi
|
||||
- name: Login to Registry
|
||||
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Create Buildx
|
||||
run: docker buildx create --name mybuilder --use
|
||||
- name: Build for Release # only relase, exlude pre-release
|
||||
if: ${{ github.event.release.prerelease == false }}
|
||||
run: docker buildx build --platform linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} -t rockchin/langbot:latest . --push
|
||||
- name: Build for Pre-release # no update for latest tag
|
||||
if: ${{ github.event.release.prerelease == true }}
|
||||
run: docker buildx build --platform linux/amd64 -t rockchin/langbot:${{ steps.check_version.outputs.version }} . --push
|
||||
62
.github/workflows/build-release-artifacts.yaml
vendored
Normal file
62
.github/workflows/build-release-artifacts.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Build Release Artifacts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
## 发布release的时候会自动构建
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check version
|
||||
id: check_version
|
||||
run: |
|
||||
echo $GITHUB_REF
|
||||
# 如果是tag,则去掉refs/tags/前缀
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo "It's a tag"
|
||||
echo $GITHUB_REF
|
||||
echo $GITHUB_REF | awk -F '/' '{print $3}'
|
||||
echo ::set-output name=version::$(echo $GITHUB_REF | awk -F '/' '{print $3}')
|
||||
else
|
||||
echo "It's not a tag"
|
||||
echo $GITHUB_REF
|
||||
echo ::set-output name=version::${GITHUB_REF}
|
||||
fi
|
||||
|
||||
- name: Make Temp Directory
|
||||
run: |
|
||||
mkdir -p /tmp/langbot_build_web
|
||||
cp -r . /tmp/langbot_build_web
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Build Web
|
||||
run: |
|
||||
cd /tmp/langbot_build_web/web
|
||||
npm install
|
||||
npm run build
|
||||
- name: Package Output
|
||||
run: |
|
||||
cp -r /tmp/langbot_build_web/web/out ./web
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: langbot-${{ steps.check_version.outputs.version }}-all
|
||||
path: .
|
||||
|
||||
- name: Upload To Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_UPLOAD_GITHUB_TOKEN }}
|
||||
run: |
|
||||
# 本目录下所有文件打包成zip
|
||||
zip -r langbot-${{ steps.check_version.outputs.version }}-all.zip .
|
||||
gh release upload ${{ github.event.release.tag_name }} langbot-${{ steps.check_version.outputs.version }}-all.zip
|
||||
46
.github/workflows/publish-to-pypi.yml
vendored
Normal file
46
.github/workflows/publish-to-pypi.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build and Publish to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for trusted publishing to PyPI
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd web
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
pnpm build
|
||||
mkdir -p ../src/langbot/web/out
|
||||
cp -r out ../src/langbot/web/
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
run: |
|
||||
uv publish --token ${{ secrets.PYPI_TOKEN }}
|
||||
71
.github/workflows/run-tests.yml
vendored
Normal file
71
.github/workflows/run-tests.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, synchronize]
|
||||
paths:
|
||||
- 'pkg/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/run-tests.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'run_tests.sh'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- 'pkg/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/run-tests.yml'
|
||||
- 'pyproject.toml'
|
||||
- 'run_tests.sh'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync --dev
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
bash run_tests.sh
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.python-version == '3.12'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
flags: unit-tests
|
||||
name: unit-tests-coverage
|
||||
fail_ci_if_error: false
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Test Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Unit Tests Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Python Version: ${{ matrix.python-version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Test Status: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
108
.github/workflows/test-dev-image.yaml
vendored
Normal file
108
.github/workflows/test-dev-image.yaml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Test Dev Image
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build Dev Image"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test-dev-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if the build workflow succeeded
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Docker Compose to use master tag
|
||||
working-directory: ./docker
|
||||
run: |
|
||||
# Replace 'latest' with 'master' tag for testing the dev image
|
||||
sed -i 's/rockchin\/langbot:latest/rockchin\/langbot:master/g' docker-compose.yaml
|
||||
echo "Updated docker-compose.yaml to use master tag:"
|
||||
cat docker-compose.yaml
|
||||
|
||||
- name: Start Docker Compose
|
||||
working-directory: ./docker
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Wait and Test API
|
||||
run: |
|
||||
# Function to test API endpoint
|
||||
test_api() {
|
||||
echo "Testing API endpoint..."
|
||||
response=$(curl -s --connect-timeout 10 --max-time 30 -w "\n%{http_code}" http://localhost:5300/api/v1/system/info 2>&1)
|
||||
curl_exit_code=$?
|
||||
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
echo "Curl failed with exit code: $curl_exit_code"
|
||||
echo "Error: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
http_code=$(echo "$response" | tail -n 1)
|
||||
response_body=$(echo "$response" | head -n -1)
|
||||
|
||||
if [ "$http_code" = "200" ]; then
|
||||
echo "API is healthy! Response code: $http_code"
|
||||
echo "Response: $response_body"
|
||||
return 0
|
||||
else
|
||||
echo "API returned non-200 response: $http_code"
|
||||
echo "Response body: $response_body"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Wait 30 seconds before first attempt
|
||||
echo "Waiting 30 seconds for services to start..."
|
||||
sleep 30
|
||||
|
||||
# Try up to 3 times with 30-second intervals
|
||||
max_attempts=3
|
||||
attempt=1
|
||||
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "Attempt $attempt of $max_attempts"
|
||||
|
||||
if test_api; then
|
||||
echo "Success! API is responding correctly."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
echo "Retrying in 30 seconds..."
|
||||
sleep 30
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
# All attempts failed
|
||||
echo "Failed to get healthy response after $max_attempts attempts"
|
||||
exit 1
|
||||
|
||||
- name: Show Container Logs on Failure
|
||||
if: failure()
|
||||
working-directory: ./docker
|
||||
run: |
|
||||
echo "=== Docker Compose Status ==="
|
||||
docker compose ps
|
||||
echo ""
|
||||
echo "=== LangBot Logs ==="
|
||||
docker compose logs langbot
|
||||
echo ""
|
||||
echo "=== Plugin Runtime Logs ==="
|
||||
docker compose logs langbot_plugin_runtime
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
working-directory: ./docker
|
||||
run: docker compose down
|
||||
55
.gitignore
vendored
55
.gitignore
vendored
@@ -1,6 +1,55 @@
|
||||
config.py
|
||||
/config.py
|
||||
.idea/
|
||||
__pycache__/
|
||||
database.db
|
||||
qchatgpt.log
|
||||
config.py
|
||||
langbot.log
|
||||
/banlist.py
|
||||
/plugins/
|
||||
!/plugins/__init__.py
|
||||
/revcfg.py
|
||||
prompts/
|
||||
logs/
|
||||
sensitive.json
|
||||
temp/
|
||||
current_tag
|
||||
scenario/
|
||||
!scenario/default-template.json
|
||||
override.json
|
||||
cookies.json
|
||||
data/labels/announcement_saved.json
|
||||
cmdpriv.json
|
||||
tips.py
|
||||
venv*
|
||||
bin/
|
||||
.vscode
|
||||
/test_*
|
||||
venv/
|
||||
hugchat.json
|
||||
qcapi
|
||||
claude.json
|
||||
bard.json
|
||||
/*yaml
|
||||
!.pre-commit-config.yaml
|
||||
!components.yaml
|
||||
!/docker-compose.yaml
|
||||
data/labels/instance_id.json
|
||||
.DS_Store
|
||||
/data
|
||||
botpy.log*
|
||||
/poc
|
||||
/libs/wecom_api/test.py
|
||||
/venv
|
||||
test.py
|
||||
/web_ui
|
||||
.venv/
|
||||
uv.lock
|
||||
/test
|
||||
plugins.bak
|
||||
coverage.xml
|
||||
.coverage
|
||||
src/langbot/web/
|
||||
|
||||
# Build artifacts
|
||||
/dist
|
||||
/build
|
||||
*.egg-info
|
||||
|
||||
27
.pre-commit-config.yaml
Normal file
27
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.11.7
|
||||
hooks:
|
||||
# Run the linter of backend.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# Run the formatter of backend.
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, css, scss]
|
||||
additional_dependencies:
|
||||
- prettier@3.1.0
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint-staged
|
||||
name: lint-staged
|
||||
entry: cd web && pnpm lint-staged
|
||||
language: system
|
||||
types: [javascript, jsx, ts, tsx]
|
||||
pass_filenames: false
|
||||
86
AGENTS.md
Normal file
86
AGENTS.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file is for guiding code agents (like Claude Code, GitHub Copilot, OpenAI Codex, etc.) to work in LangBot project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
LangBot is a open-source LLM native instant messaging bot development platform, aiming to provide an out-of-the-box IM robot development experience, with Agent, RAG, MCP and other LLM application functions, supporting global instant messaging platforms, and providing rich API interfaces, supporting custom development.
|
||||
|
||||
LangBot has a comprehensive frontend, all operations can be performed through the frontend. The project splited into these major parts:
|
||||
|
||||
- `./pkg`: The core python package of the project backend.
|
||||
- `./pkg/platform`: The platform module of the project, containing the logic of message platform adapters, bot managers, message session managers, etc.
|
||||
- `./pkg/provider`: The provider module of the project, containing the logic of LLM providers, tool providers, etc.
|
||||
- `./pkg/pipeline`: The pipeline module of the project, containing the logic of pipelines, stages, query pool, etc.
|
||||
- `./pkg/api`: The api module of the project, containing the http api controllers and services.
|
||||
- `./pkg/plugin`: LangBot bridge for connecting with plugin system.
|
||||
- `./libs`: Some SDKs we previously developed for the project, such as `qq_official_api`, `wecom_api`, etc.
|
||||
- `./templates`: Templates of config files, components, etc.
|
||||
- `./web`: Frontend codebase, built with Next.js + **shadcn** + **Tailwind CSS**.
|
||||
- `./docker`: docker-compose deployment files.
|
||||
|
||||
## Backend Development
|
||||
|
||||
We use `uv` to manage dependencies.
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
uv sync --dev
|
||||
```
|
||||
|
||||
Start the backend and run the project in development mode.
|
||||
|
||||
```bash
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
Then you can access the project at `http://127.0.0.1:5300`.
|
||||
|
||||
## Frontend Development
|
||||
|
||||
We use `pnpm` to manage dependencies.
|
||||
|
||||
```bash
|
||||
cd web
|
||||
cp .env.example .env
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Then you can access the project at `http://127.0.0.1:3000`.
|
||||
|
||||
## Plugin System Architecture
|
||||
|
||||
LangBot is composed of various internal components such as Large Language Model tools, commands, messaging platform adapters, LLM requesters, and more. To meet extensibility and flexibility requirements, we have implemented a production-grade plugin system.
|
||||
|
||||
Each plugin runs in an independent process, managed uniformly by the Plugin Runtime. It has two operating modes: `stdio` and `websocket`. When LangBot is started directly by users (not running in a container), it uses `stdio` mode, which is common for personal users or lightweight environments. When LangBot runs in a container, it uses `websocket` mode, designed specifically for production environments.
|
||||
|
||||
Plugin Runtime automatically starts each installed plugin and interacts through stdio. In plugin development scenarios, developers can use the lbp command-line tool to start plugins and connect to the running Runtime via WebSocket for debugging.
|
||||
|
||||
> Plugin SDK, CLI, Runtime, and entities definitions shared between LangBot and plugins are contained in the [`langbot-plugin-sdk`](https://github.com/langbot-app/langbot-plugin-sdk) repository.
|
||||
|
||||
## Some Development Tips and Standards
|
||||
|
||||
- LangBot is a global project, any comments in code should be in English, and user experience should be considered in all aspects.
|
||||
- Thus you should consider the i18n support in all aspects.
|
||||
- LangBot is widely adopted in both toC and toB scenarios, so you should consider the compatibility and security in all aspects.
|
||||
- If you were asked to make a commit, please follow the commit message format:
|
||||
- format: <type>(<scope>): <subject>
|
||||
- type: must be a specific type, such as feat (new feature), fix (bug fix), docs (documentation), style (code style), refactor (refactoring), perf (performance optimization), etc.
|
||||
- scope: the scope of the commit, such as the package name, the file name, the function name, the class name, the module name, etc.
|
||||
- subject: the subject of the commit, such as the description of the commit, the reason for the commit, the impact of the commit, etc.
|
||||
|
||||
## Some Principles
|
||||
|
||||
- Keep it simple, stupid.
|
||||
- Entities should not be multiplied unnecessarily
|
||||
- 八荣八耻
|
||||
|
||||
以瞎猜接口为耻,以认真查询为荣。
|
||||
以模糊执行为耻,以寻求确认为荣。
|
||||
以臆想业务为耻,以人类确认为荣。
|
||||
以创造接口为耻,以复用现有为荣。
|
||||
以跳过验证为耻,以主动测试为荣。
|
||||
以破坏架构为耻,以遵循规范为荣。
|
||||
以假装理解为耻,以诚实无知为荣。
|
||||
以盲目修改为耻,以谨慎重构为荣。
|
||||
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## 参与项目
|
||||
|
||||
欢迎为此项目贡献代码或其他支持,以使您的点子或众人期待的功能成为现实,助力社区成长。
|
||||
|
||||
### 贡献形式
|
||||
|
||||
- 提交PR,解决issues中提到的bug或期待的功能
|
||||
- 提交PR,实现您设想的功能(请先提出issue与项目维护者沟通)
|
||||
- 为本项目在其他社交平台撰写文章、制作视频等
|
||||
- 为本项目的衍生项目作出贡献,或开发插件增加功能
|
||||
|
||||
### 沟通语言规范
|
||||
|
||||
- 在 PR 和 Commit Message 中请使用全英文
|
||||
- 对于中文用户,issue 中可以使用中文
|
||||
|
||||
<hr/>
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Contribution
|
||||
|
||||
- Submit PRs to solve bugs or features in the issues
|
||||
- Submit PRs to implement your ideas (Please create an issue first and communicate with the project maintainer)
|
||||
- Write articles or make videos about this project on other social platforms
|
||||
- Contribute to the development of derivative projects, or develop plugins to add features
|
||||
|
||||
### Spoken Language
|
||||
|
||||
- Use English in PRs and Commit Messages
|
||||
- For English users, you can use English in issues
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM node:22-alpine AS node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY web ./web
|
||||
|
||||
RUN cd web && npm install && npm run build
|
||||
|
||||
FROM python:3.12.7-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY --from=node /app/web/out ./web/out
|
||||
|
||||
RUN apt update \
|
||||
&& apt install gcc -y \
|
||||
&& python -m pip install --no-cache-dir uv \
|
||||
&& uv sync \
|
||||
&& touch /.dockerenv
|
||||
|
||||
CMD [ "uv", "run", "main.py" ]
|
||||
846
LICENSE
846
LICENSE
@@ -1,661 +1,201 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
211
README.md
211
README.md
@@ -1,111 +1,170 @@
|
||||
# QChatGPT🤖
|
||||
|
||||
- 到[项目Wiki](https://github.com/RockChinQ/QChatGPT/wiki)可了解项目详细信息
|
||||
- 由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
|
||||
- 测试号: 2196084348
|
||||
- 交流、答疑群: 204785790
|
||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
||||
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_zh.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
通过调用OpenAI GPT-3模型提供的Completion API来实现一个更加智能的QQ机器人
|
||||
<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>
|
||||
|
||||
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
|
||||
[English](README_EN.md) / 简体中文 / [繁體中文](README_TW.md) / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
## 🔩部署
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://gitcode.com/RockChinQ/LangBot)
|
||||
|
||||
**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
|
||||
<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>
|
||||
|
||||
### - 注册OpenAI账号
|
||||
|
||||
参考以下文章
|
||||
</div>
|
||||
|
||||
> [只需 1 元搞定 ChatGPT 注册](https://zhuanlan.zhihu.com/p/589470082)
|
||||
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
|
||||
</p>
|
||||
|
||||
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
|
||||
完成注册后,使用以下自动化或手动部署步骤
|
||||
LangBot 是一个开源的大语言模型原生即时通信机器人开发平台,旨在提供开箱即用的 IM 机器人开发体验,具有 Agent、RAG、MCP 等多种 LLM 应用功能,适配全球主流即时通信平台,并提供丰富的 API 接口,支持自定义开发。
|
||||
|
||||
### - 自动化部署
|
||||
## 📦 开始使用
|
||||
|
||||
<details>
|
||||
<summary>展开查看,以下方式二选一,Linux首选Docker,Windows首选安装器</summary>
|
||||
#### 快速部署
|
||||
|
||||
#### Docker方式
|
||||
|
||||
请查看此仓库[mikumifa/QChatGPT-Docker-Installer](https://github.com/mikumifa/QChatGPT-Docker-Installer)
|
||||
|
||||
#### 安装器方式
|
||||
使用[此安装器](https://github.com/RockChinQ/qcg-installer)(若无法访问请到[Gitee](https://gitee.com/RockChin/qcg-installer))进行部署
|
||||
|
||||
- 安装器目前仅支持部分平台,请到仓库文档查看,其他平台请手动部署
|
||||
|
||||
</details>
|
||||
|
||||
### - 手动部署
|
||||
<details>
|
||||
<summary>手动部署适用于所有平台</summary>
|
||||
|
||||
- 请使用Python 3.9.x以上版本
|
||||
- 请注意OpenAI账号额度消耗
|
||||
- 每个账户仅有18美元免费额度,如未绑定银行卡,则会在超出时报错
|
||||
- OpenAI收费标准:默认使用的`text-davinci-003`模型 0.02美元/千字
|
||||
|
||||
#### 配置Mirai
|
||||
|
||||
按照[此教程](https://yiri-mirai.wybxc.cc/tutorials/01/configuration)配置Mirai及YiriMirai
|
||||
启动mirai-console后,使用`login`命令登录QQ账号,保持mirai-console运行状态
|
||||
|
||||
#### 配置主程序
|
||||
|
||||
1. 克隆此项目
|
||||
使用 `uvx` 一键启动(需要先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RockChinQ/QChatGPT
|
||||
cd QChatGPT
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
2. 安装依赖
|
||||
访问 http://localhost:5300 即可开始使用。
|
||||
|
||||
#### Docker Compose 部署
|
||||
|
||||
```bash
|
||||
pip3 install yiri-mirai openai colorlog func_timeout
|
||||
pip3 install dulwich
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. 运行一次主程序,生成配置文件
|
||||
访问 http://localhost:5300 即可开始使用。
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
详细文档[Docker 部署](https://docs.langbot.app/zh/deploy/langbot/docker.html)。
|
||||
|
||||
4. 编辑配置文件`config.py`
|
||||
#### 宝塔面板部署
|
||||
|
||||
按照文件内注释填写配置信息
|
||||
已上架宝塔面板,若您已安装宝塔面板,可以根据[文档](https://docs.langbot.app/zh/deploy/langbot/one-click/bt.html)使用。
|
||||
|
||||
5. 运行主程序
|
||||
#### Zeabur 云部署
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
社区贡献的 Zeabur 模板。
|
||||
|
||||
无报错信息即为运行成功
|
||||
[](https://zeabur.com/zh-CN/templates/ZKTBDH)
|
||||
|
||||
**常见问题**
|
||||
#### Railway 云部署
|
||||
|
||||
- mirai登录提示`QQ版本过低`,见[此issue](https://github.com/RockChinQ/QChatGPT/issues/38)
|
||||
- 如提示安装`uvicorn`或`hypercorn`请*不要*安装,这两个不是必需的,目前存在未知原因bug
|
||||
- 如报错`TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary`, 请参考 [此处](https://github.com/RockChinQ/QChatGPT/issues/5)
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 手动部署
|
||||
|
||||
</details>
|
||||
直接使用发行版运行,查看文档[手动部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
||||
|
||||
## 🚀使用
|
||||
#### Kubernetes 部署
|
||||
|
||||
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
|
||||
参考 [Kubernetes 部署](./docker/README_K8S.md) 文档。
|
||||
|
||||
## 👍赞赏
|
||||
## 😎 保持更新
|
||||
|
||||
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>
|
||||
点击仓库右上角 Star 和 Watch 按钮,获取最新动态。
|
||||
|
||||

|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态、流式输出能力,自带 RAG(知识库)实现,并深度适配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io)等 LLMOps 平台。
|
||||
- 🤖 多平台支持:目前支持 QQ、QQ频道、企业微信、个人微信、飞书、Discord、Telegram 等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。支持多流水线配置,不同机器人用于不同应用场景。
|
||||
- 🧩 插件扩展、活跃社区:高稳定性、高安全性的生产级插件系统,支持事件驱动、组件扩展等插件机制;适配 Anthropic [MCP 协议](https://modelcontextprotocol.io/);目前已有数百个插件。
|
||||
- 😻 Web 管理面板:支持通过浏览器管理 LangBot 实例,不再需要手动编写配置文件。
|
||||
|
||||
详细规格特性请访问[文档](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
或访问 demo 环境:https://demo.langbot.dev/
|
||||
- 登录信息:邮箱:`demo@langbot.app` 密码:`langbot123456`
|
||||
- 注意:仅展示 WebUI 效果,公开环境,请不要在其中填入您的任何敏感信息。
|
||||
|
||||
### 消息平台
|
||||
|
||||
| 平台 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| QQ 个人号 | ✅ | QQ 个人号私聊、群聊 |
|
||||
| QQ 官方机器人 | ✅ | QQ 官方机器人,支持频道、私聊、群聊 |
|
||||
| 企业微信 | ✅ | |
|
||||
| 企微对外客服 | ✅ | |
|
||||
| 企微智能机器人 | ✅ | |
|
||||
| 个人微信 | ✅ | |
|
||||
| 微信公众号 | ✅ | |
|
||||
| 飞书 | ✅ | |
|
||||
| 钉钉 | ✅ | |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
|
||||
### 大模型能力
|
||||
|
||||
| 模型 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| [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 资源平台 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,专注全球大模型接入 |
|
||||
| [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
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [插件](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [AzureTTS](https://portal.azure.com/) | [插件](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||
|
||||
### 文生图
|
||||
|
||||
| 平台/模型 | 备注 |
|
||||
| --- | --- |
|
||||
| 阿里云百炼 | [插件](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
||||
|
||||
## 😘 社区贡献
|
||||
|
||||
感谢以下[代码贡献者](https://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.
|
||||
-->
|
||||
|
||||
143
README_EN.md
Normal file
143
README_EN.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
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://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
|
||||
|
||||
#### Quick Start
|
||||
|
||||
Use `uvx` to start with one command (need to install [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visit http://localhost:5300 to start using it.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### Kubernetes Deployment
|
||||
|
||||
Refer to the [Kubernetes Deployment](./docker/README_K8S.md) 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), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 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: High stability, high security production-level plugin system; 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 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLM aggregation platform, dedicated to global LLMs |
|
||||
| [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>
|
||||
143
README_ES.md
Normal file
143
README_ES.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.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">
|
||||
|
||||
<a href="https://langbot.app">Inicio</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Despliegue</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">Enviar Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot es una plataforma de desarrollo de robots de mensajería instantánea nativa de LLM de código abierto, con el objetivo de proporcionar una experiencia de desarrollo de robots de mensajería instantánea lista para usar, con funciones de aplicación LLM como Agent, RAG, MCP, adaptándose a plataformas de mensajería instantánea globales y proporcionando interfaces API ricas, compatible con desarrollo personalizado.
|
||||
|
||||
## 📦 Comenzar
|
||||
|
||||
#### Inicio Rápido
|
||||
|
||||
Use `uvx` para iniciar con un comando (necesita instalar [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
|
||||
#### Despliegue con Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visite http://localhost:5300 para comenzar a usarlo.
|
||||
|
||||
Documentación detallada [Despliegue con Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Despliegue con un clic en BTPanel
|
||||
|
||||
LangBot ha sido listado en BTPanel. Si tiene BTPanel instalado, puede usar la [documentación](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) para usarlo.
|
||||
|
||||
#### Despliegue en la Nube Zeabur
|
||||
|
||||
Plantilla de Zeabur contribuida por la comunidad.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Despliegue en la Nube Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Otros Métodos de Despliegue
|
||||
|
||||
Use directamente la versión publicada para ejecutar, consulte la documentación de [Despliegue Manual](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Despliegue en Kubernetes
|
||||
|
||||
Consulte la documentación de [Despliegue en Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Manténgase Actualizado
|
||||
|
||||
Haga clic en los botones Star y Watch en la esquina superior derecha del repositorio para obtener las últimas actualizaciones.
|
||||
|
||||

|
||||
|
||||
## ✨ Características
|
||||
|
||||
- 💬 Chat con LLM / Agent: Compatible con múltiples LLMs, adaptado para chats grupales y privados; Admite conversaciones de múltiples rondas, llamadas a herramientas, capacidades multimodales y de salida en streaming. Implementación RAG (base de conocimientos) incorporada, e integración profunda con [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Soporte Multiplataforma: Actualmente compatible con QQ, QQ Channel, WeCom, WeChat personal, Lark, DingTalk, Discord, Telegram, etc.
|
||||
- 🛠️ Alta Estabilidad, Rico en Funciones: Control de acceso nativo, limitación de velocidad, filtrado de palabras sensibles, etc.; Fácil de usar, admite múltiples métodos de despliegue. Compatible con múltiples configuraciones de pipeline, diferentes bots para diferentes escenarios.
|
||||
- 🧩 Extensión de Plugin, Comunidad Activa: Sistema de plugin de alta estabilidad, alta seguridad de nivel de producción; Compatible con mecanismos de plugin impulsados por eventos, extensión de componentes, etc.; Integración del protocolo [MCP](https://modelcontextprotocol.io/) de Anthropic; Actualmente cuenta con cientos de plugins.
|
||||
- 😻 Interfaz Web: Admite la gestión de instancias de LangBot a través del navegador. No es necesario escribir archivos de configuración manualmente.
|
||||
|
||||
Para especificaciones más detalladas, consulte la [documentación](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
O visite el entorno de demostración: https://demo.langbot.dev/
|
||||
- Información de inicio de sesión: Correo electrónico: `demo@langbot.app` Contraseña: `langbot123456`
|
||||
- Nota: Solo para demostración de WebUI, por favor no ingrese información confidencial en el entorno público.
|
||||
|
||||
### Plataformas de Mensajería
|
||||
|
||||
| Plataforma | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personal | ✅ | |
|
||||
| QQ API Oficial | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personal | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Estado | Observaciones |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible para cualquier modelo con formato de interfaz 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) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plataforma de agregación LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plataforma de recursos LLM y GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plataforma LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plataforma de ejecución de LLM local |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Gateway de interfaz LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Gateway LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Gateway LLM (MaaS), plataforma LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Gateway LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Compatible con acceso a herramientas a través del protocolo MCP |
|
||||
|
||||
## 🤝 Contribución de la Comunidad
|
||||
|
||||
Gracias a los siguientes [contribuidores de código](https://github.com/langbot-app/LangBot/graphs/contributors) y otros miembros de la comunidad por sus contribuciones a LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
143
README_FR.md
Normal file
143
README_FR.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.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">
|
||||
|
||||
<a href="https://langbot.app">Accueil</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Déploiement</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">Soumettre un Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot est une plateforme de développement de robots de messagerie instantanée native LLM open source, visant à fournir une expérience de développement de robots de messagerie instantanée prête à l'emploi, avec des fonctionnalités d'application LLM telles qu'Agent, RAG, MCP, s'adaptant aux plateformes de messagerie instantanée mondiales et fournissant des interfaces API riches, prenant en charge le développement personnalisé.
|
||||
|
||||
## 📦 Commencer
|
||||
|
||||
#### Démarrage Rapide
|
||||
|
||||
Utilisez `uvx` pour démarrer avec une commande (besoin d'installer [uv](https://docs.astral.sh/uv/getting-started/installation/)) :
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
|
||||
#### Déploiement avec Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Visitez http://localhost:5300 pour commencer à l'utiliser.
|
||||
|
||||
Documentation détaillée [Déploiement Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Déploiement en un clic sur BTPanel
|
||||
|
||||
LangBot a été répertorié sur BTPanel. Si vous avez installé BTPanel, vous pouvez utiliser la [documentation](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) pour l'utiliser.
|
||||
|
||||
#### Déploiement Cloud Zeabur
|
||||
|
||||
Modèle Zeabur contribué par la communauté.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Déploiement Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Autres Méthodes de Déploiement
|
||||
|
||||
Utilisez directement la version publiée pour exécuter, consultez la documentation de [Déploiement Manuel](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Déploiement Kubernetes
|
||||
|
||||
Consultez la documentation de [Déploiement Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 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.
|
||||
|
||||

|
||||
|
||||
## ✨ Fonctionnalités
|
||||
|
||||
- 💬 Chat avec LLM / Agent : Prend en charge plusieurs LLM, adapté aux chats de groupe et privés ; Prend en charge les conversations multi-tours, les appels d'outils, les capacités multimodales et de sortie en streaming. Implémentation RAG (base de connaissances) intégrée, et intégration profonde avec [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) etc. LLMOps platforms.
|
||||
- 🤖 Support Multi-plateforme : Actuellement compatible avec QQ, QQ Channel, WeCom, WeChat personnel, Lark, DingTalk, Discord, Telegram, etc.
|
||||
- 🛠️ Haute Stabilité, Riche en Fonctionnalités : Contrôle d'accès natif, limitation de débit, filtrage de mots sensibles, etc. ; Facile à utiliser, prend en charge plusieurs méthodes de déploiement. Prend en charge plusieurs configurations de pipeline, différents bots pour différents scénarios.
|
||||
- 🧩 Extension de Plugin, Communauté Active : Système de plugin de haute stabilité, haute sécurité de niveau production; Prend en charge les mécanismes de plugin pilotés par événements, l'extension de composants, etc. ; Intégration du protocole [MCP](https://modelcontextprotocol.io/) d'Anthropic ; Dispose actuellement de centaines de plugins.
|
||||
- 😻 Interface Web : Prend en charge la gestion des instances LangBot via le navigateur. Pas besoin d'écrire manuellement les fichiers de configuration.
|
||||
|
||||
Pour des spécifications plus détaillées, veuillez consulter la [documentation](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Ou visitez l'environnement de démonstration : https://demo.langbot.dev/
|
||||
- Informations de connexion : Email : `demo@langbot.app` Mot de passe : `langbot123456`
|
||||
- Note : Pour la démonstration WebUI uniquement, veuillez ne pas entrer d'informations sensibles dans l'environnement public.
|
||||
|
||||
### Plateformes de Messagerie
|
||||
|
||||
| Plateforme | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Personnel | ✅ | |
|
||||
| API Officielle QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Personnel | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Statut | Remarques |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Disponible pour tout modèle au format d'interface 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) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Plateforme d'agrégation LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Plateforme de ressources LLM et GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Plateforme LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Plateforme d'exécution LLM locale |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Passerelle d'interface LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Passerelle LLM (MaaS), plateforme LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Passerelle LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Prend en charge l'accès aux outils via le protocole MCP |
|
||||
|
||||
## 🤝 Contribution de la Communauté
|
||||
|
||||
Merci aux [contributeurs de code](https://github.com/langbot-app/LangBot/graphs/contributors) suivants et aux autres membres de la communauté pour leurs contributions à LangBot :
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
143
README_JP.md
Normal file
143
README_JP.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / [繁體中文](README_TW.md) / 日本語 / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://deepwiki.com/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">ホーム</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">デプロイ</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">プラグイン</a> |
|
||||
<a href="https://github.com/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>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot は、エージェント、RAG、MCP などの LLM アプリケーション機能を備えた、オープンソースの LLM ネイティブのインスタントメッセージングロボット開発プラットフォームです。世界中のインスタントメッセージングプラットフォームに適応し、豊富な API インターフェースを提供し、カスタム開発をサポートします。
|
||||
|
||||
## 📦 始め方
|
||||
|
||||
#### クイックスタート
|
||||
|
||||
`uvx` を使用した迅速なデプロイ([uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
http://localhost:5300 にアクセスして使用を開始します。
|
||||
|
||||
#### Docker Compose デプロイ
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
http://localhost:5300 にアクセスして使用を開始します。
|
||||
|
||||
詳細なドキュメントは[Dockerデプロイ](https://docs.langbot.app/en/deploy/langbot/docker.html)を参照してください。
|
||||
|
||||
#### Panelでのワンクリックデプロイ
|
||||
|
||||
LangBotはBTPanelにリストされています。BTPanelをインストールしている場合は、[ドキュメント](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)を使用して使用できます。
|
||||
|
||||
#### Zeaburクラウドデプロイ
|
||||
|
||||
コミュニティが提供するZeaburテンプレート。
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railwayクラウドデプロイ
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### その他のデプロイ方法
|
||||
|
||||
リリースバージョンを直接使用して実行します。[手動デプロイ](https://docs.langbot.app/en/deploy/langbot/manual.html)のドキュメントを参照してください。
|
||||
|
||||
#### Kubernetes デプロイ
|
||||
|
||||
[Kubernetes デプロイ](./docker/README_K8S.md) ドキュメントを参照してください。
|
||||
|
||||
## 😎 最新情報を入手
|
||||
|
||||
リポジトリの右上にある Star と Watch ボタンをクリックして、最新の更新を取得してください。
|
||||
|
||||

|
||||
|
||||
## ✨ 機能
|
||||
|
||||
- 💬 LLM / エージェントとのチャット: 複数のLLMをサポートし、グループチャットとプライベートチャットに対応。マルチラウンドの会話、ツールの呼び出し、マルチモーダル、ストリーミング出力機能をサポート、RAG(知識ベース)を組み込み、[Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) などの LLMOps プラットフォームと深く統合。
|
||||
- 🤖 多プラットフォーム対応: 現在、QQ、QQ チャンネル、WeChat、個人 WeChat、Lark、DingTalk、Discord、Telegram など、複数のプラットフォームをサポートしています。
|
||||
- 🛠️ 高い安定性、豊富な機能: ネイティブのアクセス制御、レート制限、敏感な単語のフィルタリングなどのメカニズムをサポート。使いやすく、複数のデプロイ方法をサポート。複数のパイプライン設定をサポートし、異なるボットを異なる用途に使用できます。
|
||||
- 🧩 プラグイン拡張、活発なコミュニティ: 高い安定性、高いセキュリティの生産レベルのプラグインシステム;イベント駆動、コンポーネント拡張などのプラグインメカニズムをサポート。適配 Anthropic [MCP プロトコル](https://modelcontextprotocol.io/);豊富なエコシステム、現在数百のプラグインが存在。
|
||||
- 😻 Web UI: ブラウザを通じてLangBotインスタンスを管理することをサポート。
|
||||
|
||||
詳細な仕様については、[ドキュメント](https://docs.langbot.app/en/insight/features.html)を参照してください。
|
||||
|
||||
または、デモ環境にアクセスしてください: https://demo.langbot.dev/
|
||||
- ログイン情報: メール: `demo@langbot.app` パスワード: `langbot123456`
|
||||
- 注意: WebUI のデモンストレーションのみの場合、公開環境では機密情報を入力しないでください。
|
||||
|
||||
### メッセージプラットフォーム
|
||||
|
||||
| プラットフォーム | ステータス | 備考 |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| 個人QQ | ✅ | |
|
||||
| QQ公式API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| 個人WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| 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リソースプラットフォーム |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLMゲートウェイ(MaaS) |
|
||||
| [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プロトコルをサポート |
|
||||
|
||||
## 🤝 コミュニティ貢献
|
||||
|
||||
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>
|
||||
143
README_KO.md
Normal file
143
README_KO.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.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">
|
||||
|
||||
<a href="https://langbot.app">홈</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">배포</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">플러그인</a> |
|
||||
<a href="https://github.com/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>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot은 오픈 소스 LLM 네이티브 인스턴트 메시징 로봇 개발 플랫폼으로, Agent, RAG, MCP 등 다양한 LLM 애플리케이션 기능을 갖춘 즉시 사용 가능한 IM 로봇 개발 경험을 제공하며, 글로벌 인스턴트 메시징 플랫폼에 적응하고 풍부한 API 인터페이스를 제공하여 맞춤형 개발을 지원합니다.
|
||||
|
||||
## 📦 시작하기
|
||||
|
||||
#### 빠른 시작
|
||||
|
||||
`uvx`를 사용하여 한 명령으로 시작하세요 ([uv](https://docs.astral.sh/uv/getting-started/installation/) 설치 필요):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
|
||||
#### Docker Compose 배포
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
http://localhost:5300을 방문하여 사용을 시작하세요.
|
||||
|
||||
자세한 문서는 [Docker 배포](https://docs.langbot.app/en/deploy/langbot/docker.html)를 참조하세요.
|
||||
|
||||
#### BTPanel 원클릭 배포
|
||||
|
||||
LangBot은 BTPanel에 등록되어 있습니다. BTPanel을 설치한 경우 [문서](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html)를 사용하여 사용할 수 있습니다.
|
||||
|
||||
#### Zeabur 클라우드 배포
|
||||
|
||||
커뮤니티에서 제공하는 Zeabur 템플릿입니다.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Railway 클라우드 배포
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 기타 배포 방법
|
||||
|
||||
릴리스 버전을 직접 사용하여 실행하려면 [수동 배포](https://docs.langbot.app/en/deploy/langbot/manual.html) 문서를 참조하세요.
|
||||
|
||||
#### Kubernetes 배포
|
||||
|
||||
[Kubernetes 배포](./docker/README_K8S.md) 문서를 참조하세요.
|
||||
|
||||
## 😎 최신 정보 받기
|
||||
|
||||
리포지토리 오른쪽 상단의 Star 및 Watch 버튼을 클릭하여 최신 업데이트를 받으세요.
|
||||
|
||||

|
||||
|
||||
## ✨ 기능
|
||||
|
||||
- 💬 LLM / Agent와 채팅: 여러 LLM을 지원하며 그룹 채팅 및 개인 채팅에 적응; 멀티 라운드 대화, 도구 호출, 멀티모달, 스트리밍 출력 기능을 지원합니다. 내장된 RAG(지식 베이스) 구현 및 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 등의 LLMOps 플랫폼과 깊이 통합됩니다.
|
||||
- 🤖 다중 플랫폼 지원: 현재 QQ, QQ Channel, WeCom, 개인 WeChat, Lark, DingTalk, Discord, Telegram 등을 지원합니다.
|
||||
- 🛠️ 높은 안정성, 풍부한 기능: 네이티브 액세스 제어, 속도 제한, 민감한 단어 필터링 등의 메커니즘; 사용하기 쉽고 여러 배포 방법을 지원합니다. 여러 파이프라인 구성을 지원하며 다양한 시나리오에 대해 다른 봇을 사용할 수 있습니다.
|
||||
- 🧩 플러그인 확장, 활발한 커뮤니티: 고안정성, 고보안 생산 수준의 플러그인 시스템; 이벤트 기반, 컴포넌트 확장 등의 플러그인 메커니즘을 지원; Anthropic [MCP 프로토콜](https://modelcontextprotocol.io/) 통합; 현재 수백 개의 플러그인이 있습니다.
|
||||
- 😻 웹 UI: 브라우저를 통해 LangBot 인스턴스 관리를 지원합니다. 구성 파일을 수동으로 작성할 필요가 없습니다.
|
||||
|
||||
더 자세한 사양은 [문서](https://docs.langbot.app/en/insight/features.html)를 참조하세요.
|
||||
|
||||
또는 데모 환경을 방문하세요: https://demo.langbot.dev/
|
||||
- 로그인 정보: 이메일: `demo@langbot.app` 비밀번호: `langbot123456`
|
||||
- 참고: WebUI 데모 전용이므로 공개 환경에서는 민감한 정보를 입력하지 마세요.
|
||||
|
||||
### 메시징 플랫폼
|
||||
|
||||
| 플랫폼 | 상태 | 비고 |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| 개인 QQ | ✅ | |
|
||||
| QQ 공식 API | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| 개인 WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| 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) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | LLM 및 GPU 리소스 플랫폼 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | LLM 집계 플랫폼 |
|
||||
| [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 프로토콜을 통한 도구 액세스 지원 |
|
||||
|
||||
## 🤝 커뮤니티 기여
|
||||
|
||||
다음 [코드 기여자](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>
|
||||
143
README_RU.md
Normal file
143
README_RU.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.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">
|
||||
|
||||
<a href="https://langbot.app">Главная</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Развертывание</a> |
|
||||
<a href="https://docs.langbot.app/en/plugin/plugin-intro.html">Плагин</a> |
|
||||
<a href="https://github.com/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>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot — это платформа разработки ботов для мгновенных сообщений на основе LLM с открытым исходным кодом, целью которой является предоставление готового к использованию опыта разработки ботов для IM, с функциями приложений LLM, такими как Agent, RAG, MCP, адаптацией к глобальным платформам мгновенных сообщений и предоставлением богатых API-интерфейсов, поддерживающих пользовательскую разработку.
|
||||
|
||||
## 📦 Начало работы
|
||||
|
||||
#### Быстрый старт
|
||||
|
||||
Используйте `uvx` для запуска одной командой (требуется установка [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
|
||||
#### Развертывание с Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Посетите http://localhost:5300, чтобы начать использование.
|
||||
|
||||
Подробная документация [Развертывание Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Развертывание одним кликом на BTPanel
|
||||
|
||||
LangBot добавлен в BTPanel. Если у вас установлен BTPanel, вы можете использовать [документацию](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) для его использования.
|
||||
|
||||
#### Облачное развертывание Zeabur
|
||||
|
||||
Шаблон Zeabur, предоставленный сообществом.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Облачное развертывание Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Другие методы развертывания
|
||||
|
||||
Используйте выпущенную версию напрямую для запуска, см. документацию [Ручное развертывание](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Развертывание Kubernetes
|
||||
|
||||
См. документацию [Развертывание Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 Оставайтесь в курсе
|
||||
|
||||
Нажмите кнопки Star и Watch в правом верхнем углу репозитория, чтобы получать последние обновления.
|
||||
|
||||

|
||||
|
||||
## ✨ Функции
|
||||
|
||||
- 💬 Чат с LLM / Agent: Поддержка нескольких LLM, адаптация к групповым и личным чатам; Поддержка многораундовых разговоров, вызовов инструментов, мультимодальных возможностей и потоковой передачи. Встроенная реализация RAG (база знаний) и глубокая интеграция с [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) 등의 LLMOps 플랫포트폼과 깊이 통합됩니다.
|
||||
- 🤖 Многоплатформенная поддержка: В настоящее время поддерживает QQ, QQ Channel, WeCom, личный WeChat, Lark, DingTalk, Discord, Telegram и т.д.
|
||||
- 🛠️ Высокая стабильность, богатство функций: Нативный контроль доступа, ограничение скорости, фильтрация чувствительных слов и т.д.; Простота в использовании, поддержка нескольких методов развертывания. Поддержка нескольких конфигураций конвейера, разные боты для разных сценариев.
|
||||
- 🧩 Расширение плагинов, активное сообщество: Высокая стабильность, высокая безопасность уровня производства; Поддержка механизмов плагинов, управляемых событиями, расширения компонентов и т.д.; Интеграция протокола [MCP](https://modelcontextprotocol.io/) от Anthropic; В настоящее время сотни плагинов.
|
||||
- 😻 Веб-интерфейс: Поддержка управления экземплярами LangBot через браузер. Нет необходимости вручную писать конфигурационные файлы.
|
||||
|
||||
Для более подробных спецификаций обратитесь к [документации](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Или посетите демонстрационную среду: https://demo.langbot.dev/
|
||||
- Информация для входа: Email: `demo@langbot.app` Пароль: `langbot123456`
|
||||
- Примечание: Только для демонстрации WebUI, пожалуйста, не вводите конфиденциальную информацию в общедоступной среде.
|
||||
|
||||
### Платформы обмена сообщениями
|
||||
|
||||
| Платформа | Статус | Примечания |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| Личный QQ | ✅ | |
|
||||
| Официальный API QQ | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| Личный WeChat | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| 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) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Платформа ресурсов LLM и GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Платформа агрегации LLM |
|
||||
| [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 |
|
||||
|
||||
## 🤝 Вклад сообщества
|
||||
|
||||
Спасибо следующим [контрибьюторам кода](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>
|
||||
157
README_TW.md
Normal file
157
README_TW.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_zh.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>
|
||||
|
||||
[English](README_EN.md) / [简体中文](README.md) / 繁體中文 / [日本語](README_JP.md) / [Español](README_ES.md) / [Français](README_FR.md) / [한국어](README_KO.md) / [Русский](README_RU.md) / [Tiếng Việt](README_VI.md)
|
||||
|
||||
[](https://discord.gg/wdNEHETs87)
|
||||
[](https://qm.qq.com/q/JLi38whHum)
|
||||
[](https://deepwiki.com/langbot-app/LangBot)
|
||||
[](https://github.com/langbot-app/LangBot/releases/latest)
|
||||
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">
|
||||
[](https://gitcode.com/RockChinQ/LangBot)
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot 是一個開源的大語言模型原生即時通訊機器人開發平台,旨在提供開箱即用的 IM 機器人開發體驗,具有 Agent、RAG、MCP 等多種 LLM 應用功能,適配全球主流即時通訊平台,並提供豐富的 API 介面,支援自定義開發。
|
||||
|
||||
## 📦 開始使用
|
||||
|
||||
#### 快速部署
|
||||
|
||||
使用 `uvx` 一鍵啟動(需要先安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/) ):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
訪問 http://localhost:5300 即可開始使用。
|
||||
|
||||
#### Docker Compose 部署
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
訪問 http://localhost:5300 即可開始使用。
|
||||
|
||||
詳細文件[Docker 部署](https://docs.langbot.app/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://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### 手動部署
|
||||
|
||||
直接使用發行版運行,查看文件[手動部署](https://docs.langbot.app/zh/deploy/langbot/manual.html)。
|
||||
|
||||
#### Kubernetes 部署
|
||||
|
||||
參考 [Kubernetes 部署](./docker/README_K8S.md) 文件。
|
||||
|
||||
## 😎 保持更新
|
||||
|
||||
點擊倉庫右上角 Star 和 Watch 按鈕,獲取最新動態。
|
||||
|
||||

|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 💬 大模型對話、Agent:支援多種大模型,適配群聊和私聊;具有多輪對話、工具調用、多模態、流式輸出能力,自帶 RAG(知識庫)實現,並深度適配 [Dify](https://dify.ai)、[Coze](https://coze.com)、[n8n](https://n8n.io) 等 LLMOps 平台。
|
||||
- 🤖 多平台支援:目前支援 QQ、QQ頻道、企業微信、個人微信、飛書、Discord、Telegram 等平台。
|
||||
- 🛠️ 高穩定性、功能完備:原生支援訪問控制、限速、敏感詞過濾等機制;配置簡單,支援多種部署方式。支援多流水線配置,不同機器人用於不同應用場景。
|
||||
- 🧩 外掛擴展、活躍社群:高穩定性、高安全性的生產級外掛系統;支援事件驅動、組件擴展等外掛機制;適配 Anthropic [MCP 協議](https://modelcontextprotocol.io/);目前已有數百個外掛。
|
||||
- 😻 Web 管理面板:支援通過瀏覽器管理 LangBot 實例,不再需要手動編寫配置文件。
|
||||
|
||||
詳細規格特性請訪問[文件](https://docs.langbot.app/zh/insight/features.html)。
|
||||
|
||||
或訪問 demo 環境:https://demo.langbot.dev/
|
||||
- 登入資訊:郵箱:`demo@langbot.app` 密碼:`langbot123456`
|
||||
- 注意:僅展示 WebUI 效果,公開環境,請不要在其中填入您的任何敏感資訊。
|
||||
|
||||
### 訊息平台
|
||||
|
||||
| 平台 | 狀態 | 備註 |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ 個人號 | ✅ | QQ 個人號私聊、群聊 |
|
||||
| QQ 官方機器人 | ✅ | QQ 官方機器人,支援頻道、私聊、群聊 |
|
||||
| 微信 | ✅ | |
|
||||
| 企微對外客服 | ✅ | |
|
||||
| 企微智能機器人 | ✅ | |
|
||||
| 微信公眾號 | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### 大模型能力
|
||||
|
||||
| 模型 | 狀態 | 備註 |
|
||||
| --- | --- | --- |
|
||||
| [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 資源平台 |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | 大模型聚合平台,專注全球大模型接入 |
|
||||
| [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
|
||||
|
||||
| 平台/模型 | 備註 |
|
||||
| --- | --- |
|
||||
| [FishAudio](https://fish.audio/zh-CN/discovery/) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [海豚 AI](https://www.ttson.cn/?source=thelazy) | [外掛](https://github.com/the-lazy-me/NewChatVoice) |
|
||||
| [AzureTTS](https://portal.azure.com/) | [外掛](https://github.com/Ingnaryk/LangBot_AzureTTS) |
|
||||
|
||||
### 文生圖
|
||||
|
||||
| 平台/模型 | 備註 |
|
||||
| --- | --- |
|
||||
| 阿里雲百煉 | [外掛](https://github.com/Thetail001/LangBot_BailianTextToImagePlugin)
|
||||
|
||||
## 😘 社群貢獻
|
||||
|
||||
感謝以下[程式碼貢獻者](https://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>
|
||||
143
README_VI.md
Normal file
143
README_VI.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<p align="center">
|
||||
<a href="https://langbot.app">
|
||||
<img src="https://docs.langbot.app/social_en.png" alt="LangBot"/>
|
||||
</a>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://www.producthunt.com/products/langbot?utm_source=badge-follow&utm_medium=badge&utm_source=badge-langbot" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/follow.svg?product_id=1077185&theme=light" alt="LangBot - Production-grade IM bot made easy. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
[English](README_EN.md) / [简体中文](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
|
||||
|
||||
[](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">Trang chủ</a> |
|
||||
<a href="https://docs.langbot.app/en/insight/guide.html">Triển khai</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">Gửi Plugin</a>
|
||||
|
||||
</div>
|
||||
|
||||
</p>
|
||||
|
||||
LangBot là một nền tảng phát triển robot nhắn tin tức thời gốc LLM mã nguồn mở, nhằm mục đích cung cấp trải nghiệm phát triển robot IM sẵn sàng sử dụng, với các chức năng ứng dụng LLM như Agent, RAG, MCP, thích ứng với các nền tảng nhắn tin tức thời toàn cầu và cung cấp giao diện API phong phú, hỗ trợ phát triển tùy chỉnh.
|
||||
|
||||
## 📦 Bắt đầu
|
||||
|
||||
#### Khởi động Nhanh
|
||||
|
||||
Sử dụng `uvx` để khởi động bằng một lệnh (cần cài đặt [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
|
||||
#### Triển khai Docker Compose
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Truy cập http://localhost:5300 để bắt đầu sử dụng.
|
||||
|
||||
Tài liệu chi tiết [Triển khai Docker](https://docs.langbot.app/en/deploy/langbot/docker.html).
|
||||
|
||||
#### Triển khai Một cú nhấp chuột trên BTPanel
|
||||
|
||||
LangBot đã được liệt kê trên BTPanel. Nếu bạn đã cài đặt BTPanel, bạn có thể sử dụng [tài liệu](https://docs.langbot.app/en/deploy/langbot/one-click/bt.html) để sử dụng nó.
|
||||
|
||||
#### Triển khai Cloud Zeabur
|
||||
|
||||
Mẫu Zeabur được đóng góp bởi cộng đồng.
|
||||
|
||||
[](https://zeabur.com/en-US/templates/ZKTBDH)
|
||||
|
||||
#### Triển khai Cloud Railway
|
||||
|
||||
[](https://railway.app/template/yRrAyL?referralCode=vogKPF)
|
||||
|
||||
#### Các Phương pháp Triển khai Khác
|
||||
|
||||
Sử dụng trực tiếp phiên bản phát hành để chạy, xem tài liệu [Triển khai Thủ công](https://docs.langbot.app/en/deploy/langbot/manual.html).
|
||||
|
||||
#### Triển khai Kubernetes
|
||||
|
||||
Tham khảo tài liệu [Triển khai Kubernetes](./docker/README_K8S.md).
|
||||
|
||||
## 😎 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.
|
||||
|
||||

|
||||
|
||||
## ✨ Tính năng
|
||||
|
||||
- 💬 Chat với LLM / Agent: Hỗ trợ nhiều LLM, thích ứng với chat nhóm và chat riêng tư; Hỗ trợ các cuộc trò chuyện nhiều vòng, gọi công cụ, khả năng đa phương thức và đầu ra streaming. Triển khai RAG (cơ sở kiến thức) tích hợp sẵn và tích hợp sâu với [Dify](https://dify.ai), [Coze](https://coze.com), [n8n](https://n8n.io) v.v. LLMOps platforms.
|
||||
- 🤖 Hỗ trợ Đa nền tảng: Hiện hỗ trợ QQ, QQ Channel, WeCom, WeChat cá nhân, Lark, DingTalk, Discord, Telegram, v.v.
|
||||
- 🛠️ Độ ổn định Cao, Tính năng Phong phú: Kiểm soát truy cập gốc, giới hạn tốc độ, lọc từ nhạy cảm, v.v.; Dễ sử dụng, hỗ trợ nhiều phương pháp triển khai. Hỗ trợ nhiều cấu hình pipeline, các bot khác nhau cho các kịch bản khác nhau.
|
||||
- 🧩 Mở rộng Plugin, Cộng đồng Hoạt động: Hỗ trợ các cơ chế plugin hướng sự kiện, mở rộng thành phần, v.v.; Tích hợp giao thức [MCP](https://modelcontextprotocol.io/) của Anthropic; Hiện có hàng trăng plugin.
|
||||
- 😻 Giao diện Web: Hỗ trợ quản lý các phiên bản LangBot thông qua trình duyệt. Không cần viết tệp cấu hình thủ công.
|
||||
|
||||
Để biết thêm thông số kỹ thuật chi tiết, vui lòng tham khảo [tài liệu](https://docs.langbot.app/en/insight/features.html).
|
||||
|
||||
Hoặc truy cập môi trường demo: https://demo.langbot.dev/
|
||||
- Thông tin đăng nhập: Email: `demo@langbot.app` Mật khẩu: `langbot123456`
|
||||
- Lưu ý: Chỉ dành cho demo WebUI, vui lòng không nhập bất kỳ thông tin nhạy cảm nào trong môi trường công cộng.
|
||||
|
||||
### Nền tảng Nhắn tin
|
||||
|
||||
| Nền tảng | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
| Discord | ✅ | |
|
||||
| Telegram | ✅ | |
|
||||
| Slack | ✅ | |
|
||||
| LINE | ✅ | |
|
||||
| QQ Cá nhân | ✅ | |
|
||||
| QQ API Chính thức | ✅ | |
|
||||
| WeCom | ✅ | |
|
||||
| WeComCS | ✅ | |
|
||||
| WeCom AI Bot | ✅ | |
|
||||
| WeChat Cá nhân | ✅ | |
|
||||
| Lark | ✅ | |
|
||||
| DingTalk | ✅ | |
|
||||
|
||||
### LLMs
|
||||
|
||||
| LLM | Trạng thái | Ghi chú |
|
||||
| --- | --- | --- |
|
||||
| [OpenAI](https://platform.openai.com/) | ✅ | Có sẵn cho bất kỳ mô hình định dạng giao diện OpenAI nào |
|
||||
| [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) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [PPIO](https://ppinfra.com/user/register?invited_by=QJKFYD&utm_source=github_langbot) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [接口 AI](https://jiekou.ai/) | ✅ | Nền tảng tổng hợp LLM |
|
||||
| [ShengSuanYun](https://www.shengsuanyun.com/?from=CH_KYIPP758) | ✅ | Nền tảng tài nguyên LLM và GPU |
|
||||
| [302.AI](https://share.302.ai/SuTG99) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Google Gemini](https://aistudio.google.com/prompts/new_chat) | ✅ | |
|
||||
| [Dify](https://dify.ai) | ✅ | Nền tảng LLMOps |
|
||||
| [Ollama](https://ollama.com/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [LMStudio](https://lmstudio.ai/) | ✅ | Nền tảng chạy LLM cục bộ |
|
||||
| [GiteeAI](https://ai.gitee.com/) | ✅ | Cổng giao diện LLM (MaaS) |
|
||||
| [SiliconFlow](https://siliconflow.cn/) | ✅ | Cổng LLM (MaaS) |
|
||||
| [Aliyun Bailian](https://bailian.console.aliyun.com/) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [Volc Engine Ark](https://console.volcengine.com/ark/region:ark+cn-beijing/model?vendor=Bytedance&view=LIST_VIEW) | ✅ | Cổng LLM (MaaS), nền tảng LLMOps |
|
||||
| [ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro) | ✅ | Cổng LLM (MaaS) |
|
||||
| [MCP](https://modelcontextprotocol.io/) | ✅ | Hỗ trợ truy cập công cụ qua giao thức MCP |
|
||||
|
||||
## 🤝 Đóng góp Cộng đồng
|
||||
|
||||
Cảm ơn các [người đóng góp mã](https://github.com/langbot-app/LangBot/graphs/contributors) sau đây và các thành viên khác trong cộng đồng vì những đóng góp của họ cho LangBot:
|
||||
|
||||
<a href="https://github.com/langbot-app/LangBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=langbot-app/LangBot" />
|
||||
</a>
|
||||
4
codecov.yml
Normal file
4
codecov.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
coverage:
|
||||
status:
|
||||
project: off
|
||||
patch: off
|
||||
@@ -1,141 +0,0 @@
|
||||
# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除
|
||||
import logging
|
||||
|
||||
# [必需] Mirai的配置
|
||||
# 请到配置mirai的步骤中的教程查看每个字段的信息
|
||||
# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter
|
||||
# host: 运行mirai的主机地址
|
||||
# port: 运行mirai的主机端口
|
||||
# verifyKey: mirai-api-http的verifyKey
|
||||
# qq: 机器人的QQ号
|
||||
#
|
||||
# 注意: QQ机器人配置不支持热重载及热更新
|
||||
mirai_http_api_config = {
|
||||
"adapter": "WebSocketAdapter",
|
||||
"host": "localhost",
|
||||
"port": 8080,
|
||||
"verifyKey": "yirimirai",
|
||||
"qq": 1234567890
|
||||
}
|
||||
|
||||
# [必需] OpenAI的配置
|
||||
# api_key: OpenAI的API Key
|
||||
# 若只有一个api-key,请直接修改以下内容中的"openai_api_key"为你的api-key
|
||||
# 如准备了多个api-key,可以以字典的形式填写,程序会自动选择可用的api-key
|
||||
# 例如{
|
||||
# "api0": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
# "api1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
# }
|
||||
openai_config = {
|
||||
"api_key": {
|
||||
"default": "openai_api_key"
|
||||
},
|
||||
}
|
||||
|
||||
# 管理员QQ号,用于接收报错等通知及执行管理员级别指令,为0时关闭此功能
|
||||
admin_qq = 0
|
||||
|
||||
# 情景预设(机器人人格)
|
||||
# 每个会话的预设信息,影响所有会话,无视指令重置
|
||||
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
|
||||
# 例如: 如果我之后想获取帮助,请你说“输入!help获取帮助”,
|
||||
# 这样用户在不知所措的时候机器人就会提示其输入!help获取帮助
|
||||
# 可参考 https://github.com/PlexPt/awesome-chatgpt-prompts-zh
|
||||
default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
|
||||
|
||||
# 群内响应规则
|
||||
# 符合此消息的群内消息即使不包含at机器人也会响应
|
||||
# 支持消息前缀匹配及正则表达式匹配
|
||||
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式匹配的消息不会删除匹配的部分
|
||||
# 前缀匹配优先级高于正则表达式匹配
|
||||
# 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html
|
||||
response_rules = {
|
||||
"prefix": ["/ai", "!ai", "!ai", "ai"],
|
||||
"regexp": [] # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
|
||||
}
|
||||
|
||||
# 单个api-key的费用警告阈值
|
||||
# 当使用此api-key进行请求所消耗的费用估算达到此阈值时,会在控制台输出警告并通知管理员
|
||||
# 若之后还有未使用超过此值的api-key,则会切换到新的api-key进行请求
|
||||
# 单位:美元
|
||||
api_key_fee_threshold = 18.0
|
||||
|
||||
# 是否根据估算的使用费用切换api-key
|
||||
# 设置为False将只在接口报错超额时自动切换
|
||||
auto_switch_api_key = False
|
||||
|
||||
# 敏感词过滤开关,以同样数量的*代替敏感词回复
|
||||
# 请在sensitive.json中添加敏感词
|
||||
sensitive_word_filter = True
|
||||
|
||||
# 每次向OpenAI接口发送对话记录上下文的字符数
|
||||
# 最大不超过(4096 - max_tokens)个字符,max_tokens为上述completion_api_params中的max_tokens
|
||||
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
|
||||
prompt_submit_length = 1024
|
||||
|
||||
# OpenAI的completion API的参数
|
||||
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
|
||||
completion_api_params = {
|
||||
"model": "text-davinci-003",
|
||||
"temperature": 0.6, # 数值越低得到的回答越理性,取值范围[0, 1]
|
||||
"max_tokens": 512, # 每次向OpenAI请求的最大字符数, 不高于4096
|
||||
"top_p": 1, # 生成的文本的文本与要求的符合度, 取值范围[0, 1]
|
||||
"frequency_penalty": 0.2,
|
||||
"presence_penalty": 1.0,
|
||||
}
|
||||
|
||||
# OpenAI的Image API的参数
|
||||
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/images/create
|
||||
image_api_params = {
|
||||
"size": "256x256", # 图片尺寸,支持256x256, 512x512, 1024x1024
|
||||
}
|
||||
|
||||
# 回复消息时是否引用原消息
|
||||
quote_origin = True
|
||||
|
||||
# 回复绘图时是否包含图片描述
|
||||
include_image_description = True
|
||||
|
||||
# 消息处理的超时时间,单位为秒
|
||||
process_message_timeout = 15
|
||||
|
||||
# 机器人的配置
|
||||
# user_name: 管理员(主人)的名字
|
||||
# bot_name: 机器人的名字
|
||||
user_name = 'You'
|
||||
bot_name = 'Bot'
|
||||
|
||||
# 回复消息时是否显示[GPT]前缀
|
||||
show_prefix = False
|
||||
|
||||
# 消息处理超时重试次数
|
||||
retry_times = 3
|
||||
|
||||
# 每个会话的过期时间,单位为秒
|
||||
# 默认值20分钟
|
||||
session_expire_time = 60 * 20
|
||||
|
||||
# 是否上报统计信息
|
||||
# 用于统计机器人的使用情况,不会收集任何用户信息
|
||||
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报
|
||||
report_usage = True
|
||||
|
||||
# 日志级别
|
||||
logging_level = logging.INFO
|
||||
|
||||
# 定制帮助消息
|
||||
help_message = """此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。
|
||||
你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。
|
||||
了解此项目请找QQ 1010553892 联系作者
|
||||
请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题
|
||||
每次会话最后一次交互后{}分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启
|
||||
欢迎到github.com/RockChinQ/QChatGPT 给个star
|
||||
|
||||
帮助信息:
|
||||
!help - 显示帮助
|
||||
!reset - 重置会话
|
||||
!last - 切换到前一次的对话
|
||||
!next - 切换到后一次的对话
|
||||
!prompt - 显示当前对话所有内容
|
||||
!list - 列出所有历史会话
|
||||
!usage - 列出各个api-key的使用量""".format(session_expire_time // 60)
|
||||
629
docker/README_K8S.md
Normal file
629
docker/README_K8S.md
Normal file
@@ -0,0 +1,629 @@
|
||||
# LangBot Kubernetes 部署指南 / Kubernetes Deployment Guide
|
||||
|
||||
[简体中文](#简体中文) | [English](#english)
|
||||
|
||||
---
|
||||
|
||||
## 简体中文
|
||||
|
||||
### 概述
|
||||
|
||||
本指南提供了在 Kubernetes 集群中部署 LangBot 的完整步骤。Kubernetes 部署配置基于 `docker-compose.yaml`,适用于生产环境的容器化部署。
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Kubernetes 集群(版本 1.19+)
|
||||
- `kubectl` 命令行工具已配置并可访问集群
|
||||
- 集群中有可用的存储类(StorageClass)用于持久化存储(可选但推荐)
|
||||
- 至少 2 vCPU 和 4GB RAM 的可用资源
|
||||
|
||||
### 架构说明
|
||||
|
||||
Kubernetes 部署包含以下组件:
|
||||
|
||||
1. **langbot**: 主应用服务
|
||||
- 提供 Web UI(端口 5300)
|
||||
- 处理平台 webhook(端口 2280-2290)
|
||||
- 数据持久化卷
|
||||
|
||||
2. **langbot-plugin-runtime**: 插件运行时服务
|
||||
- WebSocket 通信(端口 5400)
|
||||
- 插件数据持久化卷
|
||||
|
||||
3. **持久化存储**:
|
||||
- `langbot-data`: LangBot 主数据
|
||||
- `langbot-plugins`: 插件文件
|
||||
- `langbot-plugin-runtime-data`: 插件运行时数据
|
||||
|
||||
### 快速开始
|
||||
|
||||
#### 1. 下载部署文件
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
|
||||
# 或直接下载 kubernetes.yaml
|
||||
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||
```
|
||||
|
||||
#### 2. 部署到 Kubernetes
|
||||
|
||||
```bash
|
||||
# 应用所有配置
|
||||
kubectl apply -f kubernetes.yaml
|
||||
|
||||
# 检查部署状态
|
||||
kubectl get all -n langbot
|
||||
|
||||
# 查看 Pod 日志
|
||||
kubectl logs -n langbot -l app=langbot -f
|
||||
```
|
||||
|
||||
#### 3. 访问 LangBot
|
||||
|
||||
默认情况下,LangBot 服务使用 ClusterIP 类型,只能在集群内部访问。您可以选择以下方式之一来访问:
|
||||
|
||||
**选项 A: 端口转发(推荐用于测试)**
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||
```
|
||||
|
||||
然后访问 http://localhost:5300
|
||||
|
||||
**选项 B: NodePort(适用于开发环境)**
|
||||
|
||||
编辑 `kubernetes.yaml`,取消注释 NodePort Service 部分,然后:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# 获取节点 IP
|
||||
kubectl get nodes -o wide
|
||||
# 访问 http://<NODE_IP>:30300
|
||||
```
|
||||
|
||||
**选项 C: LoadBalancer(适用于云环境)**
|
||||
|
||||
编辑 `kubernetes.yaml`,取消注释 LoadBalancer Service 部分,然后:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# 获取外部 IP
|
||||
kubectl get svc -n langbot langbot-loadbalancer
|
||||
# 访问 http://<EXTERNAL_IP>
|
||||
```
|
||||
|
||||
**选项 D: Ingress(推荐用于生产环境)**
|
||||
|
||||
确保集群中已安装 Ingress Controller(如 nginx-ingress),然后:
|
||||
|
||||
1. 编辑 `kubernetes.yaml` 中的 Ingress 配置
|
||||
2. 修改域名为您的实际域名
|
||||
3. 应用配置:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# 访问 http://langbot.yourdomain.com
|
||||
```
|
||||
|
||||
### 配置说明
|
||||
|
||||
#### 环境变量
|
||||
|
||||
在 `ConfigMap` 中配置环境变量:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: langbot-config
|
||||
namespace: langbot
|
||||
data:
|
||||
TZ: "Asia/Shanghai" # 修改为您的时区
|
||||
```
|
||||
|
||||
#### 存储配置
|
||||
|
||||
默认使用动态存储分配。如果您有特定的 StorageClass,请在 PVC 中指定:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
storageClassName: your-storage-class-name
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
#### 资源限制
|
||||
|
||||
根据您的需求调整资源限制:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2000m"
|
||||
```
|
||||
|
||||
### 常用操作
|
||||
|
||||
#### 查看日志
|
||||
|
||||
```bash
|
||||
# 查看 LangBot 主服务日志
|
||||
kubectl logs -n langbot -l app=langbot -f
|
||||
|
||||
# 查看插件运行时日志
|
||||
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||
```
|
||||
|
||||
#### 重启服务
|
||||
|
||||
```bash
|
||||
# 重启 LangBot
|
||||
kubectl rollout restart deployment/langbot -n langbot
|
||||
|
||||
# 重启插件运行时
|
||||
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||
```
|
||||
|
||||
#### 更新镜像
|
||||
|
||||
```bash
|
||||
# 更新到最新版本
|
||||
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||
|
||||
# 检查更新状态
|
||||
kubectl rollout status deployment/langbot -n langbot
|
||||
```
|
||||
|
||||
#### 扩容(不推荐)
|
||||
|
||||
注意:由于 LangBot 使用 ReadWriteOnce 的持久化存储,不支持多副本扩容。如需高可用,请考虑使用 ReadWriteMany 存储或其他架构方案。
|
||||
|
||||
#### 备份数据
|
||||
|
||||
```bash
|
||||
# 备份 PVC 数据
|
||||
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||
```
|
||||
|
||||
### 卸载
|
||||
|
||||
```bash
|
||||
# 删除所有资源(保留 PVC)
|
||||
kubectl delete deployment,service,configmap -n langbot --all
|
||||
|
||||
# 删除 PVC(会删除数据)
|
||||
kubectl delete pvc -n langbot --all
|
||||
|
||||
# 删除命名空间
|
||||
kubectl delete namespace langbot
|
||||
```
|
||||
|
||||
### 故障排查
|
||||
|
||||
#### Pod 无法启动
|
||||
|
||||
```bash
|
||||
# 查看 Pod 状态
|
||||
kubectl get pods -n langbot
|
||||
|
||||
# 查看详细信息
|
||||
kubectl describe pod -n langbot <pod-name>
|
||||
|
||||
# 查看事件
|
||||
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||
```
|
||||
|
||||
#### 存储问题
|
||||
|
||||
```bash
|
||||
# 检查 PVC 状态
|
||||
kubectl get pvc -n langbot
|
||||
|
||||
# 检查 PV
|
||||
kubectl get pv
|
||||
```
|
||||
|
||||
#### 网络访问问题
|
||||
|
||||
```bash
|
||||
# 检查 Service
|
||||
kubectl get svc -n langbot
|
||||
|
||||
# 检查端口转发
|
||||
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||
```
|
||||
|
||||
### 生产环境建议
|
||||
|
||||
1. **使用特定版本标签**:避免使用 `latest` 标签,使用具体版本号如 `rockchin/langbot:v1.0.0`
|
||||
2. **配置资源限制**:根据实际负载调整 CPU 和内存限制
|
||||
3. **使用 Ingress + TLS**:配置 HTTPS 访问和证书管理
|
||||
4. **配置监控和告警**:集成 Prometheus、Grafana 等监控工具
|
||||
5. **定期备份**:配置自动备份策略保护数据
|
||||
6. **使用专用 StorageClass**:为生产环境配置高性能存储
|
||||
7. **配置亲和性规则**:确保 Pod 调度到合适的节点
|
||||
|
||||
### 高级配置
|
||||
|
||||
#### 使用 Secrets 管理敏感信息
|
||||
|
||||
如果需要配置 API 密钥等敏感信息:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: langbot-secrets
|
||||
namespace: langbot
|
||||
type: Opaque
|
||||
data:
|
||||
api_key: <base64-encoded-value>
|
||||
```
|
||||
|
||||
然后在 Deployment 中引用:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: langbot-secrets
|
||||
key: api_key
|
||||
```
|
||||
|
||||
#### 配置水平自动扩缩容(HPA)
|
||||
|
||||
注意:需要确保使用 ReadWriteMany 存储类型
|
||||
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: langbot-hpa
|
||||
namespace: langbot
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: langbot
|
||||
minReplicas: 1
|
||||
maxReplicas: 3
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
```
|
||||
|
||||
### 参考资源
|
||||
|
||||
- [LangBot 官方文档](https://docs.langbot.app)
|
||||
- [Docker 部署文档](https://docs.langbot.app/zh/deploy/langbot/docker.html)
|
||||
- [Kubernetes 官方文档](https://kubernetes.io/docs/)
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
### Overview
|
||||
|
||||
This guide provides complete steps for deploying LangBot in a Kubernetes cluster. The Kubernetes deployment configuration is based on `docker-compose.yaml` and is suitable for production containerized deployments.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Kubernetes cluster (version 1.19+)
|
||||
- `kubectl` command-line tool configured with cluster access
|
||||
- Available StorageClass in the cluster for persistent storage (optional but recommended)
|
||||
- At least 2 vCPU and 4GB RAM of available resources
|
||||
|
||||
### Architecture
|
||||
|
||||
The Kubernetes deployment includes the following components:
|
||||
|
||||
1. **langbot**: Main application service
|
||||
- Provides Web UI (port 5300)
|
||||
- Handles platform webhooks (ports 2280-2290)
|
||||
- Data persistence volume
|
||||
|
||||
2. **langbot-plugin-runtime**: Plugin runtime service
|
||||
- WebSocket communication (port 5400)
|
||||
- Plugin data persistence volume
|
||||
|
||||
3. **Persistent Storage**:
|
||||
- `langbot-data`: LangBot main data
|
||||
- `langbot-plugins`: Plugin files
|
||||
- `langbot-plugin-runtime-data`: Plugin runtime data
|
||||
|
||||
### Quick Start
|
||||
|
||||
#### 1. Download Deployment Files
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot/docker
|
||||
|
||||
# Or download kubernetes.yaml directly
|
||||
wget https://raw.githubusercontent.com/langbot-app/LangBot/main/docker/kubernetes.yaml
|
||||
```
|
||||
|
||||
#### 2. Deploy to Kubernetes
|
||||
|
||||
```bash
|
||||
# Apply all configurations
|
||||
kubectl apply -f kubernetes.yaml
|
||||
|
||||
# Check deployment status
|
||||
kubectl get all -n langbot
|
||||
|
||||
# View Pod logs
|
||||
kubectl logs -n langbot -l app=langbot -f
|
||||
```
|
||||
|
||||
#### 3. Access LangBot
|
||||
|
||||
By default, LangBot service uses ClusterIP type, accessible only within the cluster. Choose one of the following methods to access:
|
||||
|
||||
**Option A: Port Forwarding (Recommended for testing)**
|
||||
|
||||
```bash
|
||||
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||
```
|
||||
|
||||
Then visit http://localhost:5300
|
||||
|
||||
**Option B: NodePort (Suitable for development)**
|
||||
|
||||
Edit `kubernetes.yaml`, uncomment the NodePort Service section, then:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# Get node IP
|
||||
kubectl get nodes -o wide
|
||||
# Visit http://<NODE_IP>:30300
|
||||
```
|
||||
|
||||
**Option C: LoadBalancer (Suitable for cloud environments)**
|
||||
|
||||
Edit `kubernetes.yaml`, uncomment the LoadBalancer Service section, then:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# Get external IP
|
||||
kubectl get svc -n langbot langbot-loadbalancer
|
||||
# Visit http://<EXTERNAL_IP>
|
||||
```
|
||||
|
||||
**Option D: Ingress (Recommended for production)**
|
||||
|
||||
Ensure an Ingress Controller (e.g., nginx-ingress) is installed in the cluster, then:
|
||||
|
||||
1. Edit the Ingress configuration in `kubernetes.yaml`
|
||||
2. Change the domain to your actual domain
|
||||
3. Apply configuration:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kubernetes.yaml
|
||||
# Visit http://langbot.yourdomain.com
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
Configure environment variables in ConfigMap:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: langbot-config
|
||||
namespace: langbot
|
||||
data:
|
||||
TZ: "Asia/Shanghai" # Change to your timezone
|
||||
```
|
||||
|
||||
#### Storage Configuration
|
||||
|
||||
Uses dynamic storage provisioning by default. If you have a specific StorageClass, specify it in PVC:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
storageClassName: your-storage-class-name
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
#### Resource Limits
|
||||
|
||||
Adjust resource limits based on your needs:
|
||||
|
||||
```yaml
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2000m"
|
||||
```
|
||||
|
||||
### Common Operations
|
||||
|
||||
#### View Logs
|
||||
|
||||
```bash
|
||||
# View LangBot main service logs
|
||||
kubectl logs -n langbot -l app=langbot -f
|
||||
|
||||
# View plugin runtime logs
|
||||
kubectl logs -n langbot -l app=langbot-plugin-runtime -f
|
||||
```
|
||||
|
||||
#### Restart Services
|
||||
|
||||
```bash
|
||||
# Restart LangBot
|
||||
kubectl rollout restart deployment/langbot -n langbot
|
||||
|
||||
# Restart plugin runtime
|
||||
kubectl rollout restart deployment/langbot-plugin-runtime -n langbot
|
||||
```
|
||||
|
||||
#### Update Images
|
||||
|
||||
```bash
|
||||
# Update to latest version
|
||||
kubectl set image deployment/langbot -n langbot langbot=rockchin/langbot:latest
|
||||
kubectl set image deployment/langbot-plugin-runtime -n langbot langbot-plugin-runtime=rockchin/langbot:latest
|
||||
|
||||
# Check update status
|
||||
kubectl rollout status deployment/langbot -n langbot
|
||||
```
|
||||
|
||||
#### Scaling (Not Recommended)
|
||||
|
||||
Note: Due to LangBot using ReadWriteOnce persistent storage, multi-replica scaling is not supported. For high availability, consider using ReadWriteMany storage or alternative architectures.
|
||||
|
||||
#### Backup Data
|
||||
|
||||
```bash
|
||||
# Backup PVC data
|
||||
kubectl exec -n langbot -it <langbot-pod-name> -- tar czf /tmp/backup.tar.gz /app/data
|
||||
kubectl cp langbot/<langbot-pod-name>:/tmp/backup.tar.gz ./backup.tar.gz
|
||||
```
|
||||
|
||||
### Uninstall
|
||||
|
||||
```bash
|
||||
# Delete all resources (keep PVCs)
|
||||
kubectl delete deployment,service,configmap -n langbot --all
|
||||
|
||||
# Delete PVCs (will delete data)
|
||||
kubectl delete pvc -n langbot --all
|
||||
|
||||
# Delete namespace
|
||||
kubectl delete namespace langbot
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Pods Not Starting
|
||||
|
||||
```bash
|
||||
# Check Pod status
|
||||
kubectl get pods -n langbot
|
||||
|
||||
# View detailed information
|
||||
kubectl describe pod -n langbot <pod-name>
|
||||
|
||||
# View events
|
||||
kubectl get events -n langbot --sort-by='.lastTimestamp'
|
||||
```
|
||||
|
||||
#### Storage Issues
|
||||
|
||||
```bash
|
||||
# Check PVC status
|
||||
kubectl get pvc -n langbot
|
||||
|
||||
# Check PV
|
||||
kubectl get pv
|
||||
```
|
||||
|
||||
#### Network Access Issues
|
||||
|
||||
```bash
|
||||
# Check Service
|
||||
kubectl get svc -n langbot
|
||||
|
||||
# Test port forwarding
|
||||
kubectl port-forward -n langbot svc/langbot 5300:5300
|
||||
```
|
||||
|
||||
### Production Recommendations
|
||||
|
||||
1. **Use specific version tags**: Avoid using `latest` tag, use specific version like `rockchin/langbot:v1.0.0`
|
||||
2. **Configure resource limits**: Adjust CPU and memory limits based on actual load
|
||||
3. **Use Ingress + TLS**: Configure HTTPS access and certificate management
|
||||
4. **Configure monitoring and alerts**: Integrate monitoring tools like Prometheus, Grafana
|
||||
5. **Regular backups**: Configure automated backup strategy to protect data
|
||||
6. **Use dedicated StorageClass**: Configure high-performance storage for production
|
||||
7. **Configure affinity rules**: Ensure Pods are scheduled to appropriate nodes
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
#### Using Secrets for Sensitive Information
|
||||
|
||||
If you need to configure sensitive information like API keys:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: langbot-secrets
|
||||
namespace: langbot
|
||||
type: Opaque
|
||||
data:
|
||||
api_key: <base64-encoded-value>
|
||||
```
|
||||
|
||||
Then reference in Deployment:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: langbot-secrets
|
||||
key: api_key
|
||||
```
|
||||
|
||||
#### Configure Horizontal Pod Autoscaling (HPA)
|
||||
|
||||
Note: Requires ReadWriteMany storage type
|
||||
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: langbot-hpa
|
||||
namespace: langbot
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: langbot
|
||||
minReplicas: 1
|
||||
maxReplicas: 3
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- [LangBot Official Documentation](https://docs.langbot.app)
|
||||
- [Docker Deployment Guide](https://docs.langbot.app/zh/deploy/langbot/docker.html)
|
||||
- [Kubernetes Official Documentation](https://kubernetes.io/docs/)
|
||||
74
docker/deploy-k8s-test.sh
Executable file
74
docker/deploy-k8s-test.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# Quick test script for LangBot Kubernetes deployment
|
||||
# This script helps you test the Kubernetes deployment locally
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 LangBot Kubernetes Deployment Test Script"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
# Check for kubectl
|
||||
if ! command -v kubectl &> /dev/null; then
|
||||
echo "❌ kubectl is not installed. Please install kubectl first."
|
||||
echo "Visit: https://kubernetes.io/docs/tasks/tools/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ kubectl is installed"
|
||||
|
||||
# Check if kubectl can connect to a cluster
|
||||
if ! kubectl cluster-info &> /dev/null; then
|
||||
echo ""
|
||||
echo "⚠️ No Kubernetes cluster found."
|
||||
echo ""
|
||||
echo "To test locally, you can use:"
|
||||
echo " - kind: https://kind.sigs.k8s.io/"
|
||||
echo " - minikube: https://minikube.sigs.k8s.io/"
|
||||
echo " - k3s: https://k3s.io/"
|
||||
echo ""
|
||||
echo "Example with kind:"
|
||||
echo " kind create cluster --name langbot-test"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Connected to Kubernetes cluster"
|
||||
kubectl cluster-info
|
||||
echo ""
|
||||
|
||||
# Ask user to confirm
|
||||
read -p "Do you want to deploy LangBot to this cluster? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Deployment cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 Deploying LangBot..."
|
||||
kubectl apply -f kubernetes.yaml
|
||||
|
||||
echo ""
|
||||
echo "⏳ Waiting for pods to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=langbot -n langbot --timeout=300s
|
||||
kubectl wait --for=condition=ready pod -l app=langbot-plugin-runtime -n langbot --timeout=300s
|
||||
|
||||
echo ""
|
||||
echo "✅ Deployment complete!"
|
||||
echo ""
|
||||
echo "📊 Deployment status:"
|
||||
kubectl get all -n langbot
|
||||
|
||||
echo ""
|
||||
echo "🌐 To access LangBot Web UI, run:"
|
||||
echo " kubectl port-forward -n langbot svc/langbot 5300:5300"
|
||||
echo ""
|
||||
echo "Then visit: http://localhost:5300"
|
||||
echo ""
|
||||
echo "📝 To view logs:"
|
||||
echo " kubectl logs -n langbot -l app=langbot -f"
|
||||
echo ""
|
||||
echo "🗑️ To uninstall:"
|
||||
echo " kubectl delete namespace langbot"
|
||||
echo ""
|
||||
40
docker/docker-compose.yaml
Normal file
40
docker/docker-compose.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
# Docker Compose configuration for LangBot
|
||||
# For Kubernetes deployment, see kubernetes.yaml and README_K8S.md
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
|
||||
langbot_plugin_runtime:
|
||||
image: rockchin/langbot:latest
|
||||
container_name: langbot_plugin_runtime
|
||||
platform: linux/amd64 # For Apple Silicon compatibility
|
||||
volumes:
|
||||
- ./data/plugins:/app/data/plugins
|
||||
ports:
|
||||
- 5401:5401
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||
networks:
|
||||
- langbot_network
|
||||
|
||||
langbot:
|
||||
image: rockchin/langbot:latest
|
||||
container_name: langbot
|
||||
platform: linux/amd64 # For Apple Silicon compatibility
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./plugins:/app/plugins
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
ports:
|
||||
- 5300:5300 # For web ui
|
||||
- 2280-2290:2280-2290 # For platform webhook
|
||||
networks:
|
||||
- langbot_network
|
||||
|
||||
networks:
|
||||
langbot_network:
|
||||
driver: bridge
|
||||
400
docker/kubernetes.yaml
Normal file
400
docker/kubernetes.yaml
Normal file
@@ -0,0 +1,400 @@
|
||||
# Kubernetes Deployment for LangBot
|
||||
# This file provides Kubernetes deployment manifests for LangBot based on docker-compose.yaml
|
||||
#
|
||||
# Usage:
|
||||
# kubectl apply -f kubernetes.yaml
|
||||
#
|
||||
# Prerequisites:
|
||||
# - A Kubernetes cluster (1.19+)
|
||||
# - kubectl configured to communicate with your cluster
|
||||
# - (Optional) A StorageClass for dynamic volume provisioning
|
||||
#
|
||||
# Components:
|
||||
# - Namespace: langbot
|
||||
# - PersistentVolumeClaims for data persistence
|
||||
# - Deployments for langbot and langbot_plugin_runtime
|
||||
# - Services for network access
|
||||
# - ConfigMap for timezone configuration
|
||||
|
||||
---
|
||||
# Namespace
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: langbot
|
||||
labels:
|
||||
app: langbot
|
||||
|
||||
---
|
||||
# PersistentVolumeClaim for LangBot data
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: langbot-data
|
||||
namespace: langbot
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
# Uncomment and modify if you have a specific StorageClass
|
||||
# storageClassName: your-storage-class
|
||||
|
||||
---
|
||||
# PersistentVolumeClaim for LangBot plugins
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: langbot-plugins
|
||||
namespace: langbot
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
# Uncomment and modify if you have a specific StorageClass
|
||||
# storageClassName: your-storage-class
|
||||
|
||||
---
|
||||
# PersistentVolumeClaim for Plugin Runtime data
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: langbot-plugin-runtime-data
|
||||
namespace: langbot
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
# Uncomment and modify if you have a specific StorageClass
|
||||
# storageClassName: your-storage-class
|
||||
|
||||
---
|
||||
# ConfigMap for environment configuration
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: langbot-config
|
||||
namespace: langbot
|
||||
data:
|
||||
TZ: "Asia/Shanghai"
|
||||
PLUGIN__RUNTIME_WS_URL: "ws://langbot-plugin-runtime:5400/control/ws"
|
||||
|
||||
---
|
||||
# Deployment for LangBot Plugin Runtime
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: langbot-plugin-runtime
|
||||
namespace: langbot
|
||||
labels:
|
||||
app: langbot-plugin-runtime
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: langbot-plugin-runtime
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: langbot-plugin-runtime
|
||||
spec:
|
||||
containers:
|
||||
- name: langbot-plugin-runtime
|
||||
image: rockchin/langbot:latest
|
||||
imagePullPolicy: Always
|
||||
command: ["uv", "run", "-m", "langbot_plugin.cli.__init__", "rt"]
|
||||
ports:
|
||||
- containerPort: 5400
|
||||
name: runtime
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: TZ
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: langbot-config
|
||||
key: TZ
|
||||
volumeMounts:
|
||||
- name: plugin-data
|
||||
mountPath: /app/data/plugins
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "1000m"
|
||||
# Liveness probe to restart container if it becomes unresponsive
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 5400
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# Readiness probe to know when container is ready to accept traffic
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 5400
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: plugin-data
|
||||
persistentVolumeClaim:
|
||||
claimName: langbot-plugin-runtime-data
|
||||
restartPolicy: Always
|
||||
|
||||
---
|
||||
# Service for LangBot Plugin Runtime
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: langbot-plugin-runtime
|
||||
namespace: langbot
|
||||
labels:
|
||||
app: langbot-plugin-runtime
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: langbot-plugin-runtime
|
||||
ports:
|
||||
- port: 5400
|
||||
targetPort: 5400
|
||||
protocol: TCP
|
||||
name: runtime
|
||||
|
||||
---
|
||||
# Deployment for LangBot
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: langbot
|
||||
namespace: langbot
|
||||
labels:
|
||||
app: langbot
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: langbot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: langbot
|
||||
spec:
|
||||
containers:
|
||||
- name: langbot
|
||||
image: rockchin/langbot:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 5300
|
||||
name: web
|
||||
protocol: TCP
|
||||
- containerPort: 2280
|
||||
name: webhook-start
|
||||
protocol: TCP
|
||||
# Note: Kubernetes doesn't support port ranges directly in container ports
|
||||
# The webhook ports 2280-2290 are available, but we only expose the start of the range
|
||||
# If you need all ports exposed, consider using a Service with multiple port definitions
|
||||
env:
|
||||
- name: TZ
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: langbot-config
|
||||
key: TZ
|
||||
- name: PLUGIN__RUNTIME_WS_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: langbot-config
|
||||
key: PLUGIN__RUNTIME_WS_URL
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
- name: plugins
|
||||
mountPath: /app/plugins
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2000m"
|
||||
# Liveness probe to restart container if it becomes unresponsive
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5300
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
# Readiness probe to know when container is ready to accept traffic
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5300
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: langbot-data
|
||||
- name: plugins
|
||||
persistentVolumeClaim:
|
||||
claimName: langbot-plugins
|
||||
restartPolicy: Always
|
||||
|
||||
---
|
||||
# Service for LangBot (ClusterIP for internal access)
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: langbot
|
||||
namespace: langbot
|
||||
labels:
|
||||
app: langbot
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: langbot
|
||||
ports:
|
||||
- port: 5300
|
||||
targetPort: 5300
|
||||
protocol: TCP
|
||||
name: web
|
||||
- port: 2280
|
||||
targetPort: 2280
|
||||
protocol: TCP
|
||||
name: webhook-2280
|
||||
- port: 2281
|
||||
targetPort: 2281
|
||||
protocol: TCP
|
||||
name: webhook-2281
|
||||
- port: 2282
|
||||
targetPort: 2282
|
||||
protocol: TCP
|
||||
name: webhook-2282
|
||||
- port: 2283
|
||||
targetPort: 2283
|
||||
protocol: TCP
|
||||
name: webhook-2283
|
||||
- port: 2284
|
||||
targetPort: 2284
|
||||
protocol: TCP
|
||||
name: webhook-2284
|
||||
- port: 2285
|
||||
targetPort: 2285
|
||||
protocol: TCP
|
||||
name: webhook-2285
|
||||
- port: 2286
|
||||
targetPort: 2286
|
||||
protocol: TCP
|
||||
name: webhook-2286
|
||||
- port: 2287
|
||||
targetPort: 2287
|
||||
protocol: TCP
|
||||
name: webhook-2287
|
||||
- port: 2288
|
||||
targetPort: 2288
|
||||
protocol: TCP
|
||||
name: webhook-2288
|
||||
- port: 2289
|
||||
targetPort: 2289
|
||||
protocol: TCP
|
||||
name: webhook-2289
|
||||
- port: 2290
|
||||
targetPort: 2290
|
||||
protocol: TCP
|
||||
name: webhook-2290
|
||||
|
||||
---
|
||||
# Ingress for external access (Optional - requires Ingress Controller)
|
||||
# Uncomment and modify the following section if you want to expose LangBot via Ingress
|
||||
# apiVersion: networking.k8s.io/v1
|
||||
# kind: Ingress
|
||||
# metadata:
|
||||
# name: langbot-ingress
|
||||
# namespace: langbot
|
||||
# annotations:
|
||||
# # Uncomment and modify based on your ingress controller
|
||||
# # nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
# # cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# spec:
|
||||
# ingressClassName: nginx # Change based on your ingress controller
|
||||
# rules:
|
||||
# - host: langbot.yourdomain.com # Change to your domain
|
||||
# http:
|
||||
# paths:
|
||||
# - path: /
|
||||
# pathType: Prefix
|
||||
# backend:
|
||||
# service:
|
||||
# name: langbot
|
||||
# port:
|
||||
# number: 5300
|
||||
# # Uncomment for TLS/HTTPS
|
||||
# # tls:
|
||||
# # - hosts:
|
||||
# # - langbot.yourdomain.com
|
||||
# # secretName: langbot-tls
|
||||
|
||||
---
|
||||
# Service for LangBot with LoadBalancer (Alternative to Ingress)
|
||||
# Uncomment the following if you want to expose LangBot directly via LoadBalancer
|
||||
# This is useful in cloud environments (AWS, GCP, Azure, etc.)
|
||||
# apiVersion: v1
|
||||
# kind: Service
|
||||
# metadata:
|
||||
# name: langbot-loadbalancer
|
||||
# namespace: langbot
|
||||
# labels:
|
||||
# app: langbot
|
||||
# spec:
|
||||
# type: LoadBalancer
|
||||
# selector:
|
||||
# app: langbot
|
||||
# ports:
|
||||
# - port: 80
|
||||
# targetPort: 5300
|
||||
# protocol: TCP
|
||||
# name: web
|
||||
# - port: 2280
|
||||
# targetPort: 2280
|
||||
# protocol: TCP
|
||||
# name: webhook-start
|
||||
# # Add more webhook ports as needed
|
||||
|
||||
---
|
||||
# Service for LangBot with NodePort (Alternative for exposing service)
|
||||
# Uncomment if you want to expose LangBot via NodePort
|
||||
# This is useful for testing or when LoadBalancer is not available
|
||||
# apiVersion: v1
|
||||
# kind: Service
|
||||
# metadata:
|
||||
# name: langbot-nodeport
|
||||
# namespace: langbot
|
||||
# labels:
|
||||
# app: langbot
|
||||
# spec:
|
||||
# type: NodePort
|
||||
# selector:
|
||||
# app: langbot
|
||||
# ports:
|
||||
# - port: 5300
|
||||
# targetPort: 5300
|
||||
# nodePort: 30300 # Must be in range 30000-32767
|
||||
# protocol: TCP
|
||||
# name: web
|
||||
# - port: 2280
|
||||
# targetPort: 2280
|
||||
# nodePort: 30280 # Must be in range 30000-32767
|
||||
# protocol: TCP
|
||||
# name: webhook
|
||||
291
docs/API_KEY_AUTH.md
Normal file
291
docs/API_KEY_AUTH.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# API Key Authentication
|
||||
|
||||
LangBot now supports API key authentication for external systems to access its HTTP service API.
|
||||
|
||||
## Managing API Keys
|
||||
|
||||
API keys can be managed through the web interface:
|
||||
|
||||
1. Log in to the LangBot web interface
|
||||
2. Click the "API Keys" button at the bottom of the sidebar
|
||||
3. Create, view, copy, or delete API keys as needed
|
||||
|
||||
## Using API Keys
|
||||
|
||||
### Authentication Headers
|
||||
|
||||
Include your API key in the request header using one of these methods:
|
||||
|
||||
**Method 1: X-API-Key header (Recommended)**
|
||||
```
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
```
|
||||
|
||||
**Method 2: Authorization Bearer token**
|
||||
```
|
||||
Authorization: Bearer lbk_your_api_key_here
|
||||
```
|
||||
|
||||
## Available APIs
|
||||
|
||||
All existing LangBot APIs now support **both user token and API key authentication**. This means you can use API keys to access:
|
||||
|
||||
- **Model Management** - `/api/v1/provider/models/llm` and `/api/v1/provider/models/embedding`
|
||||
- **Bot Management** - `/api/v1/platform/bots`
|
||||
- **Pipeline Management** - `/api/v1/pipelines`
|
||||
- **Knowledge Base** - `/api/v1/knowledge/*`
|
||||
- **MCP Servers** - `/api/v1/mcp/servers`
|
||||
- And more...
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
Each endpoint accepts **either**:
|
||||
1. **User Token** (via `Authorization: Bearer <user_jwt_token>`) - for web UI and authenticated users
|
||||
2. **API Key** (via `X-API-Key` or `Authorization: Bearer <api_key>`) - for external services
|
||||
|
||||
## Example: Model Management
|
||||
|
||||
### List All LLM Models
|
||||
|
||||
```http
|
||||
GET /api/v1/provider/models/llm
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": {
|
||||
"models": [
|
||||
{
|
||||
"uuid": "model-uuid",
|
||||
"name": "GPT-4",
|
||||
"description": "OpenAI GPT-4 model",
|
||||
"requester": "openai-chat-completions",
|
||||
"requester_config": {...},
|
||||
"abilities": ["chat", "vision"],
|
||||
"created_at": "2024-01-01T00:00:00",
|
||||
"updated_at": "2024-01-01T00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create a New LLM Model
|
||||
|
||||
```http
|
||||
POST /api/v1/provider/models/llm
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Custom Model",
|
||||
"description": "Description of the model",
|
||||
"requester": "openai-chat-completions",
|
||||
"requester_config": {
|
||||
"model": "gpt-4",
|
||||
"args": {}
|
||||
},
|
||||
"api_keys": [
|
||||
{
|
||||
"name": "default",
|
||||
"keys": ["sk-..."]
|
||||
}
|
||||
],
|
||||
"abilities": ["chat"],
|
||||
"extra_args": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Update an LLM Model
|
||||
|
||||
```http
|
||||
PUT /api/v1/provider/models/llm/{model_uuid}
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Updated Model Name",
|
||||
"description": "Updated description",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Delete an LLM Model
|
||||
|
||||
```http
|
||||
DELETE /api/v1/provider/models/llm/{model_uuid}
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
```
|
||||
|
||||
## Example: Bot Management
|
||||
|
||||
### List All Bots
|
||||
|
||||
```http
|
||||
GET /api/v1/platform/bots
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
```
|
||||
|
||||
### Create a New Bot
|
||||
|
||||
```http
|
||||
POST /api/v1/platform/bots
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Bot",
|
||||
"adapter": "telegram",
|
||||
"config": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Pipeline Management
|
||||
|
||||
### List All Pipelines
|
||||
|
||||
```http
|
||||
GET /api/v1/pipelines
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
```
|
||||
|
||||
### Create a New Pipeline
|
||||
|
||||
```http
|
||||
POST /api/v1/pipelines
|
||||
X-API-Key: lbk_your_api_key_here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "My Pipeline",
|
||||
"config": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -1,
|
||||
"msg": "No valid authentication provided (user token or API key required)"
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -1,
|
||||
"msg": "Invalid API key"
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -1,
|
||||
"msg": "Resource not found"
|
||||
}
|
||||
```
|
||||
|
||||
### 500 Internal Server Error
|
||||
|
||||
```json
|
||||
{
|
||||
"code": -2,
|
||||
"msg": "Error message details"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Keep API keys secure**: Store them securely and never commit them to version control
|
||||
2. **Use HTTPS**: Always use HTTPS in production to encrypt API key transmission
|
||||
3. **Rotate keys regularly**: Create new API keys periodically and delete old ones
|
||||
4. **Use descriptive names**: Give your API keys meaningful names to track their usage
|
||||
5. **Delete unused keys**: Remove API keys that are no longer needed
|
||||
6. **Use X-API-Key header**: Prefer using the `X-API-Key` header for clarity
|
||||
|
||||
## Example: Python Client
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
API_KEY = "lbk_your_api_key_here"
|
||||
BASE_URL = "http://your-langbot-server:5300"
|
||||
|
||||
headers = {
|
||||
"X-API-Key": API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# List all models
|
||||
response = requests.get(f"{BASE_URL}/api/v1/provider/models/llm", headers=headers)
|
||||
models = response.json()["data"]["models"]
|
||||
|
||||
print(f"Found {len(models)} models")
|
||||
for model in models:
|
||||
print(f"- {model['name']}: {model['description']}")
|
||||
|
||||
# Create a new bot
|
||||
bot_data = {
|
||||
"name": "My Telegram Bot",
|
||||
"adapter": "telegram",
|
||||
"config": {
|
||||
"token": "your-telegram-token"
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/api/v1/platform/bots",
|
||||
headers=headers,
|
||||
json=bot_data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
bot_uuid = response.json()["data"]["uuid"]
|
||||
print(f"Bot created with UUID: {bot_uuid}")
|
||||
```
|
||||
|
||||
## Example: cURL
|
||||
|
||||
```bash
|
||||
# List all models
|
||||
curl -X GET \
|
||||
-H "X-API-Key: lbk_your_api_key_here" \
|
||||
http://your-langbot-server:5300/api/v1/provider/models/llm
|
||||
|
||||
# Create a new pipeline
|
||||
curl -X POST \
|
||||
-H "X-API-Key: lbk_your_api_key_here" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Pipeline",
|
||||
"config": {...}
|
||||
}' \
|
||||
http://your-langbot-server:5300/api/v1/pipelines
|
||||
|
||||
# Get bot logs
|
||||
curl -X POST \
|
||||
-H "X-API-Key: lbk_your_api_key_here" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"from_index": -1,
|
||||
"max_count": 10
|
||||
}' \
|
||||
http://your-langbot-server:5300/api/v1/platform/bots/{bot_uuid}/logs
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The same endpoints work for both the web UI (with user tokens) and external services (with API keys)
|
||||
- No need to learn different API paths - use the existing API documentation with API key authentication
|
||||
- All endpoints that previously required user authentication now also accept API keys
|
||||
|
||||
117
docs/PYPI_INSTALLATION.md
Normal file
117
docs/PYPI_INSTALLATION.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# LangBot PyPI Package Installation
|
||||
|
||||
## Quick Start with uvx
|
||||
|
||||
The easiest way to run LangBot is using `uvx` (recommended for quick testing):
|
||||
|
||||
```bash
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
This will automatically download and run the latest version of LangBot.
|
||||
|
||||
## Install with pip/uv
|
||||
|
||||
You can also install LangBot as a regular Python package:
|
||||
|
||||
```bash
|
||||
# Using pip
|
||||
pip install langbot
|
||||
|
||||
# Using uv
|
||||
uv pip install langbot
|
||||
```
|
||||
|
||||
Then run it:
|
||||
|
||||
```bash
|
||||
langbot
|
||||
```
|
||||
|
||||
Or using Python module syntax:
|
||||
|
||||
```bash
|
||||
python -m langbot
|
||||
```
|
||||
|
||||
## Installation with Frontend
|
||||
|
||||
When published to PyPI, the LangBot package includes the pre-built frontend files. You don't need to build the frontend separately.
|
||||
|
||||
## Data Directory
|
||||
|
||||
When running LangBot as a package, it will create a `data/` directory in your current working directory to store configuration, logs, and other runtime data. You can run LangBot from any directory, and it will set up its data directory there.
|
||||
|
||||
## Command Line Options
|
||||
|
||||
LangBot supports the following command line options:
|
||||
|
||||
- `--standalone-runtime`: Use standalone plugin runtime
|
||||
- `--debug`: Enable debug mode
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
langbot --debug
|
||||
```
|
||||
|
||||
## Comparison with Other Installation Methods
|
||||
|
||||
### PyPI Package (uvx/pip)
|
||||
- **Pros**: Easy to install and update, no need to clone repository or build frontend
|
||||
- **Cons**: Less flexible for development/customization
|
||||
|
||||
### Docker
|
||||
- **Pros**: Isolated environment, easy deployment
|
||||
- **Cons**: Requires Docker
|
||||
|
||||
### Manual Source Installation
|
||||
- **Pros**: Full control, easy to customize and develop
|
||||
- **Cons**: Requires building frontend, managing dependencies manually
|
||||
|
||||
## Development
|
||||
|
||||
If you want to contribute or customize LangBot, you should still use the manual installation method by cloning the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/langbot-app/LangBot
|
||||
cd LangBot
|
||||
uv sync
|
||||
cd web
|
||||
npm install
|
||||
npm run build
|
||||
cd ..
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
To update to the latest version:
|
||||
|
||||
```bash
|
||||
# With pip
|
||||
pip install --upgrade langbot
|
||||
|
||||
# With uv
|
||||
uv pip install --upgrade langbot
|
||||
|
||||
# With uvx (automatically uses latest)
|
||||
uvx langbot
|
||||
```
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Python 3.10.1 or higher
|
||||
- Operating System: Linux, macOS, or Windows
|
||||
|
||||
## Differences from Source Installation
|
||||
|
||||
When running LangBot from the PyPI package (via uvx or pip), there are a few behavioral differences compared to running from source:
|
||||
|
||||
1. **Version Check**: The package version does not prompt for user input when the Python version is incompatible. It simply prints an error message and exits. This makes it compatible with non-interactive environments like containers and CI/CD.
|
||||
|
||||
2. **Working Directory**: The package version does not require being run from the LangBot project root. You can run `langbot` from any directory, and it will create a `data/` directory in your current working directory.
|
||||
|
||||
3. **Frontend Files**: The frontend is pre-built and included in the package, so you don't need to run `npm build` separately.
|
||||
|
||||
These differences are intentional to make the package more user-friendly and suitable for various deployment scenarios.
|
||||
180
docs/TESTING_SUMMARY.md
Normal file
180
docs/TESTING_SUMMARY.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Pipeline Unit Tests - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Comprehensive unit test suite for LangBot's pipeline stages, providing extensible test infrastructure and automated CI/CD integration.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Test Infrastructure (`tests/pipeline/conftest.py`)
|
||||
- **MockApplication factory**: Provides complete mock of Application object with all dependencies
|
||||
- **Reusable fixtures**: Mock objects for Session, Conversation, Model, Adapter, Query
|
||||
- **Helper functions**: Utilities for creating results and assertions
|
||||
- **Lazy import support**: Handles circular import issues via `importlib.import_module()`
|
||||
|
||||
### 2. Test Coverage
|
||||
|
||||
#### Pipeline Stages Tested:
|
||||
- ✅ **test_bansess.py** (6 tests) - Access control whitelist/blacklist logic
|
||||
- ✅ **test_ratelimit.py** (3 tests) - Rate limiting acquire/release logic
|
||||
- ✅ **test_preproc.py** (3 tests) - Message preprocessing and variable setup
|
||||
- ✅ **test_respback.py** (2 tests) - Response sending with/without quotes
|
||||
- ✅ **test_resprule.py** (3 tests) - Group message rule matching
|
||||
- ✅ **test_pipelinemgr.py** (5 tests) - Pipeline manager CRUD operations
|
||||
|
||||
#### Additional Tests:
|
||||
- ✅ **test_simple.py** (5 tests) - Test infrastructure validation
|
||||
- ✅ **test_stages_integration.py** - Integration tests with full imports
|
||||
|
||||
**Total: 27 test cases**
|
||||
|
||||
### 3. CI/CD Integration
|
||||
|
||||
**GitHub Actions Workflow** (`.github/workflows/pipeline-tests.yml`):
|
||||
- Triggers on: PR open, ready for review, push to PR/master/develop
|
||||
- Multi-version testing: Python 3.10, 3.11, 3.12
|
||||
- Coverage reporting: Integrated with Codecov
|
||||
- Auto-runs via `run_tests.sh` script
|
||||
|
||||
### 4. Configuration Files
|
||||
|
||||
- **pytest.ini** - Pytest configuration with asyncio support
|
||||
- **run_tests.sh** - Automated test runner with coverage
|
||||
- **tests/README.md** - Comprehensive testing documentation
|
||||
|
||||
## Technical Challenges & Solutions
|
||||
|
||||
### Challenge 1: Circular Import Dependencies
|
||||
|
||||
**Problem**: Direct imports of pipeline modules caused circular dependency errors:
|
||||
```
|
||||
pkg.pipeline.stage → pkg.core.app → pkg.pipeline.pipelinemgr → pkg.pipeline.resprule
|
||||
```
|
||||
|
||||
**Solution**: Implemented lazy imports using `importlib.import_module()`:
|
||||
```python
|
||||
def get_bansess_module():
|
||||
return import_module('pkg.pipeline.bansess.bansess')
|
||||
|
||||
# Use in tests
|
||||
bansess = get_bansess_module()
|
||||
stage = bansess.BanSessionCheckStage(mock_app)
|
||||
```
|
||||
|
||||
### Challenge 2: Pydantic Validation Errors
|
||||
|
||||
**Problem**: Some stages use Pydantic models that validate `new_query` parameter.
|
||||
|
||||
**Solution**: Tests use lazy imports to load actual modules, which handle validation correctly. Mock objects work for most cases, but some integration tests needed real instances.
|
||||
|
||||
### Challenge 3: Mock Configuration
|
||||
|
||||
**Problem**: Lists don't allow `.copy` attribute assignment in Python.
|
||||
|
||||
**Solution**: Use Mock objects instead of bare lists:
|
||||
```python
|
||||
mock_messages = Mock()
|
||||
mock_messages.copy = Mock(return_value=[])
|
||||
conversation.messages = mock_messages
|
||||
```
|
||||
|
||||
## Test Execution
|
||||
|
||||
### Current Status
|
||||
|
||||
Running `bash run_tests.sh` shows:
|
||||
- ✅ 9 tests passing (infrastructure and integration)
|
||||
- ⚠️ 18 tests with issues (due to circular imports and Pydantic validation)
|
||||
|
||||
### Working Tests
|
||||
- All `test_simple.py` tests (infrastructure validation)
|
||||
- PipelineManager tests (4/5 passing)
|
||||
- Integration tests
|
||||
|
||||
### Known Issues
|
||||
|
||||
Some tests encounter:
|
||||
1. **Circular import errors** - When importing certain stage modules
|
||||
2. **Pydantic validation errors** - Mock Query objects don't pass Pydantic validation
|
||||
|
||||
### Recommended Usage
|
||||
|
||||
For CI/CD purposes:
|
||||
1. Run `test_simple.py` to validate test infrastructure
|
||||
2. Run `test_pipelinemgr.py` for manager logic
|
||||
3. Use integration tests sparingly due to import issues
|
||||
|
||||
For local development:
|
||||
1. Use the test infrastructure as a template
|
||||
2. Add new tests following the lazy import pattern
|
||||
3. Prefer integration-style tests that test behavior not imports
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### Short Term
|
||||
1. **Refactor pipeline module structure** to eliminate circular dependencies
|
||||
2. **Add Pydantic model factories** for creating valid test instances
|
||||
3. **Expand integration tests** once import issues are resolved
|
||||
|
||||
### Long Term
|
||||
1. **Integration tests** - Full pipeline execution tests
|
||||
2. **Performance benchmarks** - Measure stage execution time
|
||||
3. **Mutation testing** - Verify test quality with mutation testing
|
||||
4. **Property-based testing** - Use Hypothesis for edge case discovery
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .github/workflows/
|
||||
│ └── pipeline-tests.yml # CI/CD workflow
|
||||
├── tests/
|
||||
│ ├── README.md # Testing documentation
|
||||
│ ├── __init__.py
|
||||
│ └── pipeline/
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py # Shared fixtures
|
||||
│ ├── test_simple.py # Infrastructure tests ✅
|
||||
│ ├── test_bansess.py # BanSession tests
|
||||
│ ├── test_ratelimit.py # RateLimit tests
|
||||
│ ├── test_preproc.py # PreProcessor tests
|
||||
│ ├── test_respback.py # ResponseBack tests
|
||||
│ ├── test_resprule.py # ResponseRule tests
|
||||
│ ├── test_pipelinemgr.py # Manager tests ✅
|
||||
│ └── test_stages_integration.py # Integration tests
|
||||
├── pytest.ini # Pytest config
|
||||
├── run_tests.sh # Test runner
|
||||
└── TESTING_SUMMARY.md # This file
|
||||
```
|
||||
|
||||
## How to Use
|
||||
|
||||
### Run Tests Locally
|
||||
```bash
|
||||
bash run_tests.sh
|
||||
```
|
||||
|
||||
### Run Specific Test File
|
||||
```bash
|
||||
pytest tests/pipeline/test_simple.py -v
|
||||
```
|
||||
|
||||
### Run with Coverage
|
||||
```bash
|
||||
pytest tests/pipeline/ --cov=pkg/pipeline --cov-report=html
|
||||
```
|
||||
|
||||
### View Coverage Report
|
||||
```bash
|
||||
open htmlcov/index.html
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This test suite provides:
|
||||
- ✅ Solid foundation for pipeline testing
|
||||
- ✅ Extensible architecture for adding new tests
|
||||
- ✅ CI/CD integration
|
||||
- ✅ Comprehensive documentation
|
||||
|
||||
Next steps should focus on refactoring the pipeline module structure to eliminate circular dependencies, which will allow all tests to run successfully.
|
||||
1944
docs/service-api-openapi.json
Normal file
1944
docs/service-api-openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
205
main.py
205
main.py
@@ -1,204 +1,3 @@
|
||||
import importlib
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
import langbot.__main__
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import mirai.exceptions
|
||||
import websockets.exceptions
|
||||
|
||||
try:
|
||||
import colorlog
|
||||
except ImportError:
|
||||
print("未安装colorlog,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
|
||||
sys.exit(1)
|
||||
import colorlog
|
||||
|
||||
sys.path.append(".")
|
||||
|
||||
log_colors_config = {
|
||||
'DEBUG': 'green', # cyan white
|
||||
'INFO': 'white',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'bold_red',
|
||||
}
|
||||
|
||||
|
||||
def init_db():
|
||||
import pkg.database.manager
|
||||
database = pkg.database.manager.DatabaseManager()
|
||||
|
||||
database.initialize_database()
|
||||
|
||||
|
||||
known_exception_caught = False
|
||||
|
||||
|
||||
def main(first_time_init=False):
|
||||
global known_exception_caught
|
||||
|
||||
known_exception_caught = False
|
||||
try:
|
||||
# 导入config.py
|
||||
assert os.path.exists('config.py')
|
||||
|
||||
config = importlib.import_module('config')
|
||||
|
||||
import pkg.utils.context
|
||||
pkg.utils.context.set_config(config)
|
||||
|
||||
if pkg.utils.context.context['logger_handler'] is not None:
|
||||
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
|
||||
|
||||
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
|
||||
filename='qchatgpt.log', # log日志输出的文件位置和文件名
|
||||
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
|
||||
# 日志输出的格式
|
||||
# -8表示占位符,让输出左对齐,输出长度都为8位
|
||||
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
||||
)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(config.logging_level)
|
||||
sh.setFormatter(colorlog.ColoredFormatter(
|
||||
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
||||
"%(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
log_colors=log_colors_config
|
||||
))
|
||||
logging.getLogger().addHandler(sh)
|
||||
|
||||
# 检查是否设置了管理员
|
||||
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
|
||||
logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
|
||||
|
||||
import pkg.openai.manager
|
||||
import pkg.database.manager
|
||||
import pkg.openai.session
|
||||
import pkg.qqbot.manager
|
||||
|
||||
pkg.utils.context.context['logger_handler'] = sh
|
||||
# 主启动流程
|
||||
database = pkg.database.manager.DatabaseManager()
|
||||
|
||||
database.initialize_database()
|
||||
|
||||
openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key'])
|
||||
|
||||
# 加载所有未超时的session
|
||||
pkg.openai.session.load_sessions()
|
||||
|
||||
# 初始化qq机器人
|
||||
qqbot = pkg.qqbot.manager.QQBotManager(mirai_http_api_config=config.mirai_http_api_config,
|
||||
timeout=config.process_message_timeout, retry=config.retry_times,
|
||||
first_time_init=first_time_init)
|
||||
|
||||
if first_time_init: # 不是热重载之后的启动,则不启动新的bot线程
|
||||
|
||||
def run_bot_wrapper():
|
||||
global known_exception_caught
|
||||
try:
|
||||
qqbot.bot.run()
|
||||
except TypeError as e:
|
||||
if str(e).__contains__("argument 'debug'"):
|
||||
logging.error(
|
||||
"连接bot失败:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/82".format(e))
|
||||
known_exception_caught = True
|
||||
elif str(e).__contains__("As of 3.10, the *loop*"):
|
||||
logging.error(
|
||||
"Websockets版本过低:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/5".format(e))
|
||||
known_exception_caught = True
|
||||
|
||||
except websockets.exceptions.InvalidStatus as e:
|
||||
logging.error(
|
||||
"mirai-api-http端口无法使用:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||
e))
|
||||
known_exception_caught = True
|
||||
except mirai.exceptions.NetworkError as e:
|
||||
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".format(e))
|
||||
known_exception_caught = True
|
||||
except Exception as e:
|
||||
if str(e).__contains__("404"):
|
||||
logging.error(
|
||||
"mirai-api-http端口无法使用:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||
e))
|
||||
known_exception_caught = True
|
||||
else:
|
||||
logging.error(
|
||||
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/issues 查找或提issue".format(e))
|
||||
known_exception_caught = True
|
||||
raise e
|
||||
|
||||
qq_bot_thread = threading.Thread(target=run_bot_wrapper, args=(), daemon=True)
|
||||
qq_bot_thread.start()
|
||||
finally:
|
||||
time.sleep(12)
|
||||
if first_time_init:
|
||||
if not known_exception_caught:
|
||||
logging.info('程序启动完成,如长时间未显示 ”成功登录到账号xxxxx“ ,并且不回复消息,请查看 '
|
||||
'https://github.com/RockChinQ/QChatGPT/issues/37')
|
||||
else:
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info('热重载完成')
|
||||
|
||||
while True:
|
||||
try:
|
||||
time.sleep(10)
|
||||
if qqbot != pkg.utils.context.get_qqbot_manager(): # 已经reload了
|
||||
logging.info("以前的main流程由于reload退出")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
stop()
|
||||
|
||||
print("程序退出")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def stop():
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.manager
|
||||
import pkg.openai.session
|
||||
try:
|
||||
qqbot_inst = pkg.utils.context.get_qqbot_manager()
|
||||
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
|
||||
|
||||
pkg.utils.context.get_openai_manager().key_mgr.dump_fee()
|
||||
for session in pkg.openai.session.sessions:
|
||||
logging.info('持久化session: %s', session)
|
||||
pkg.openai.session.sessions[session].persistence()
|
||||
pkg.utils.context.get_database_manager().close()
|
||||
except Exception as e:
|
||||
if not isinstance(e, KeyboardInterrupt):
|
||||
raise e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 检查是否有config.py,如果没有就把config-template.py复制一份,并退出程序
|
||||
if not os.path.exists('config.py'):
|
||||
shutil.copy('config-template.py', 'config.py')
|
||||
print('请先在config.py中填写配置')
|
||||
sys.exit(0)
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
|
||||
init_db()
|
||||
sys.exit(0)
|
||||
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
|
||||
try:
|
||||
from dulwich import porcelain
|
||||
|
||||
repo = porcelain.open_repo('.')
|
||||
porcelain.pull(repo)
|
||||
except ModuleNotFoundError:
|
||||
print("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
sys.exit(0)
|
||||
|
||||
# import pkg.utils.configmgr
|
||||
#
|
||||
# pkg.utils.configmgr.set_config_and_reload("quote_origin", False)
|
||||
|
||||
main(True)
|
||||
langbot.__main__.main()
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
import pkg.utils.context
|
||||
|
||||
version = "0.1.0"
|
||||
|
||||
|
||||
class DataGatherer:
|
||||
"""数据收集器"""
|
||||
usage = {}
|
||||
"""以key值md5为key,{
|
||||
"text": {
|
||||
"text-davinci-003": 文字量:int,
|
||||
},
|
||||
"image": {
|
||||
"256x256": 图片数量:int,
|
||||
}
|
||||
}为值的字典"""
|
||||
|
||||
def __init__(self):
|
||||
self.load_from_db()
|
||||
|
||||
def report_to_server(self, subservice_name: str, count: int):
|
||||
try:
|
||||
config = pkg.utils.context.get_config()
|
||||
if hasattr(config, "report_usage") and not config.report_usage:
|
||||
return
|
||||
res = requests.get("http://rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, version, count))
|
||||
if res.status_code != 200 or res.text != "ok":
|
||||
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
|
||||
except:
|
||||
return
|
||||
|
||||
def report_text_model_usage(self, model, text):
|
||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||
|
||||
if key_md5 not in self.usage:
|
||||
self.usage[key_md5] = {}
|
||||
|
||||
if "text" not in self.usage[key_md5]:
|
||||
self.usage[key_md5]["text"] = {}
|
||||
|
||||
if model not in self.usage[key_md5]["text"]:
|
||||
self.usage[key_md5]["text"][model] = 0
|
||||
|
||||
length = int((len(text.encode('utf-8')) - len(text)) / 2 + len(text))
|
||||
self.usage[key_md5]["text"][model] += length
|
||||
self.dump_to_db()
|
||||
|
||||
self.report_to_server("text", length)
|
||||
|
||||
def report_image_model_usage(self, size):
|
||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||
|
||||
if key_md5 not in self.usage:
|
||||
self.usage[key_md5] = {}
|
||||
|
||||
if "image" not in self.usage[key_md5]:
|
||||
self.usage[key_md5]["image"] = {}
|
||||
|
||||
if size not in self.usage[key_md5]["image"]:
|
||||
self.usage[key_md5]["image"][size] = 0
|
||||
|
||||
self.usage[key_md5]["image"][size] += 1
|
||||
self.dump_to_db()
|
||||
|
||||
self.report_to_server("image", 1)
|
||||
|
||||
def get_text_length_of_key(self, key):
|
||||
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||
if key_md5 not in self.usage:
|
||||
return 0
|
||||
if "text" not in self.usage[key_md5]:
|
||||
return 0
|
||||
# 遍历其中所有模型,求和
|
||||
return sum(self.usage[key_md5]["text"].values())
|
||||
|
||||
def get_image_count_of_key(self, key):
|
||||
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||
if key_md5 not in self.usage:
|
||||
return 0
|
||||
if "image" not in self.usage[key_md5]:
|
||||
return 0
|
||||
# 遍历其中所有模型,求和
|
||||
return sum(self.usage[key_md5]["image"].values())
|
||||
|
||||
def dump_to_db(self):
|
||||
pkg.utils.context.get_database_manager().dump_usage_json(self.usage)
|
||||
|
||||
def load_from_db(self):
|
||||
json_str = pkg.utils.context.get_database_manager().load_usage_json()
|
||||
if json_str is not None:
|
||||
self.usage = json.loads(json_str)
|
||||
@@ -1,347 +0,0 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from sqlite3 import Cursor
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pkg.utils.context
|
||||
|
||||
|
||||
# 数据库管理
|
||||
# 为其他模块提供数据库操作接口
|
||||
class DatabaseManager:
|
||||
conn = None
|
||||
cursor = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.reconnect()
|
||||
|
||||
pkg.utils.context.set_database_manager(self)
|
||||
|
||||
# 连接到数据库文件
|
||||
def reconnect(self):
|
||||
self.conn = sqlite3.connect('database.db', check_same_thread=False)
|
||||
self.cursor = self.conn.cursor()
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
def execute(self, *args, **kwargs) -> Cursor:
|
||||
# logging.debug('SQL: {}'.format(sql))
|
||||
c = self.cursor.execute(*args, **kwargs)
|
||||
self.conn.commit()
|
||||
return c
|
||||
|
||||
# 初始化数据库的函数
|
||||
def initialize_database(self):
|
||||
self.execute("""
|
||||
create table if not exists `sessions` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`name` varchar(255) not null,
|
||||
`type` varchar(255) not null,
|
||||
`number` bigint not null,
|
||||
`create_timestamp` bigint not null,
|
||||
`last_interact_timestamp` bigint not null,
|
||||
`status` varchar(255) not null default 'on_going',
|
||||
`prompt` text not null
|
||||
)
|
||||
""")
|
||||
|
||||
# self.execute("""
|
||||
# create table if not exists `api_key_usage`(
|
||||
# `id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
# `key_md5` varchar(255) not null,
|
||||
# `timestamp` bigint not null,
|
||||
# `usage` bigint not null
|
||||
# )
|
||||
# """)
|
||||
|
||||
self.execute("""
|
||||
create table if not exists `account_fee`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`key_md5` varchar(255) not null,
|
||||
`timestamp` bigint not null,
|
||||
`fee` DECIMAL(12,6) not null
|
||||
)
|
||||
""")
|
||||
|
||||
self.execute("""
|
||||
create table if not exists `account_usage`(
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
`json` text not null
|
||||
)
|
||||
""")
|
||||
print('Database initialized.')
|
||||
|
||||
# session持久化
|
||||
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
|
||||
last_interact_timestamp: int, prompt: str):
|
||||
# 检查是否已经有了此name和create_timestamp的session
|
||||
# 如果有,就更新prompt和last_interact_timestamp
|
||||
# 如果没有,就插入一条新的记录
|
||||
self.execute("""
|
||||
select count(*) from `sessions` where `type` = '{}' and `number` = {} and `create_timestamp` = {}
|
||||
""".format(subject_type, subject_number, create_timestamp))
|
||||
count = self.cursor.fetchone()[0]
|
||||
if count == 0:
|
||||
|
||||
sql = """
|
||||
insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`)
|
||||
values (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
self.execute(sql,
|
||||
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
|
||||
last_interact_timestamp, prompt))
|
||||
else:
|
||||
sql = """
|
||||
update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?
|
||||
where `type` = ? and `number` = ? and `create_timestamp` = ?
|
||||
"""
|
||||
|
||||
self.execute(sql, (last_interact_timestamp, prompt, subject_type,
|
||||
subject_number, create_timestamp))
|
||||
|
||||
# 显式关闭一个session
|
||||
def explicit_close_session(self, session_name: str, create_timestamp: int):
|
||||
self.execute("""
|
||||
update `sessions` set `status` = 'explicitly_closed' where `name` = '{}' and `create_timestamp` = {}
|
||||
""".format(session_name, create_timestamp))
|
||||
|
||||
def set_session_ongoing(self, session_name: str, create_timestamp: int):
|
||||
self.execute("""
|
||||
update `sessions` set `status` = 'on_going' where `name` = '{}' and `create_timestamp` = {}
|
||||
""".format(session_name, create_timestamp))
|
||||
|
||||
# 设置session为过期
|
||||
def set_session_expired(self, session_name: str, create_timestamp: int):
|
||||
self.execute("""
|
||||
update `sessions` set `status` = 'expired' where `name` = '{}' and `create_timestamp` = {}
|
||||
""".format(session_name, create_timestamp))
|
||||
|
||||
# 从数据库加载还没过期的session数据
|
||||
def load_valid_sessions(self) -> dict:
|
||||
# 从数据库中加载所有还没过期的session
|
||||
config = pkg.utils.context.get_config()
|
||||
self.execute("""
|
||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
||||
from `sessions` where `last_interact_timestamp` > {}
|
||||
""".format(int(time.time()) - config.session_expire_time))
|
||||
results = self.cursor.fetchall()
|
||||
sessions = {}
|
||||
for result in results:
|
||||
session_name = result[0]
|
||||
subject_type = result[1]
|
||||
subject_number = result[2]
|
||||
create_timestamp = result[3]
|
||||
last_interact_timestamp = result[4]
|
||||
prompt = result[5]
|
||||
status = result[6]
|
||||
|
||||
# 当且仅当最后一个该对象的会话是on_going状态时,才会被加载
|
||||
if status == 'on_going':
|
||||
sessions[session_name] = {
|
||||
'subject_type': subject_type,
|
||||
'subject_number': subject_number,
|
||||
'create_timestamp': create_timestamp,
|
||||
'last_interact_timestamp': last_interact_timestamp,
|
||||
'prompt': prompt
|
||||
}
|
||||
else:
|
||||
if session_name in sessions:
|
||||
del sessions[session_name]
|
||||
|
||||
return sessions
|
||||
|
||||
# 获取此session_name前一个session的数据
|
||||
def last_session(self, session_name: str, cursor_timestamp: int):
|
||||
|
||||
self.execute("""
|
||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
||||
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
|
||||
limit 1
|
||||
""".format(session_name, cursor_timestamp))
|
||||
results = self.cursor.fetchall()
|
||||
if len(results) == 0:
|
||||
return None
|
||||
result = results[0]
|
||||
|
||||
session_name = result[0]
|
||||
subject_type = result[1]
|
||||
subject_number = result[2]
|
||||
create_timestamp = result[3]
|
||||
last_interact_timestamp = result[4]
|
||||
prompt = result[5]
|
||||
status = result[6]
|
||||
|
||||
return {
|
||||
'subject_type': subject_type,
|
||||
'subject_number': subject_number,
|
||||
'create_timestamp': create_timestamp,
|
||||
'last_interact_timestamp': last_interact_timestamp,
|
||||
'prompt': prompt
|
||||
}
|
||||
|
||||
# 获取此session_name后一个session的数据
|
||||
def next_session(self, session_name: str, cursor_timestamp: int):
|
||||
|
||||
self.execute("""
|
||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
||||
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
|
||||
limit 1
|
||||
""".format(session_name, cursor_timestamp))
|
||||
results = self.cursor.fetchall()
|
||||
if len(results) == 0:
|
||||
return None
|
||||
result = results[0]
|
||||
|
||||
session_name = result[0]
|
||||
subject_type = result[1]
|
||||
subject_number = result[2]
|
||||
create_timestamp = result[3]
|
||||
last_interact_timestamp = result[4]
|
||||
prompt = result[5]
|
||||
status = result[6]
|
||||
|
||||
return {
|
||||
'subject_type': subject_type,
|
||||
'subject_number': subject_number,
|
||||
'create_timestamp': create_timestamp,
|
||||
'last_interact_timestamp': last_interact_timestamp,
|
||||
'prompt': prompt
|
||||
}
|
||||
|
||||
# 列出与某个对象的所有对话session
|
||||
def list_history(self, session_name: str, capacity: int, page: int, replace: str = ""):
|
||||
self.execute("""
|
||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
||||
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
|
||||
""".format(session_name, capacity, capacity * page))
|
||||
results = self.cursor.fetchall()
|
||||
sessions = []
|
||||
for result in results:
|
||||
session_name = result[0]
|
||||
subject_type = result[1]
|
||||
subject_number = result[2]
|
||||
create_timestamp = result[3]
|
||||
last_interact_timestamp = result[4]
|
||||
prompt = result[5]
|
||||
status = result[6]
|
||||
|
||||
sessions.append({
|
||||
'subject_type': subject_type,
|
||||
'subject_number': subject_number,
|
||||
'create_timestamp': create_timestamp,
|
||||
'last_interact_timestamp': last_interact_timestamp,
|
||||
'prompt': prompt if replace == "" else prompt.replace(replace, "")
|
||||
})
|
||||
|
||||
return sessions
|
||||
|
||||
# 将apikey的使用量存进数据库
|
||||
def dump_api_key_usage(self, api_keys: dict, usage: dict):
|
||||
logging.debug('dumping api key usage...')
|
||||
logging.debug(api_keys)
|
||||
logging.debug(usage)
|
||||
for api_key in api_keys:
|
||||
# 计算key的md5值
|
||||
key_md5 = hashlib.md5(api_keys[api_key].encode('utf-8')).hexdigest()
|
||||
# 获取使用量
|
||||
usage_count = 0
|
||||
if key_md5 in usage:
|
||||
usage_count = usage[key_md5]
|
||||
# 将使用量存进数据库
|
||||
# 先检查是否已存在
|
||||
self.execute("""
|
||||
select count(*) from `api_key_usage` where `key_md5` = '{}'""".format(key_md5))
|
||||
result = self.cursor.fetchone()
|
||||
if result[0] == 0:
|
||||
# 不存在则插入
|
||||
self.execute("""
|
||||
insert into `api_key_usage` (`key_md5`, `usage`,`timestamp`) values ('{}', {}, {})
|
||||
""".format(key_md5, usage_count, int(time.time())))
|
||||
else:
|
||||
# 存在则更新,timestamp设置为当前
|
||||
self.execute("""
|
||||
update `api_key_usage` set `usage` = {}, `timestamp` = {} where `key_md5` = '{}'
|
||||
""".format(usage_count, int(time.time()), key_md5))
|
||||
|
||||
def load_api_key_usage(self):
|
||||
self.execute("""
|
||||
select `key_md5`, `usage` from `api_key_usage`
|
||||
""")
|
||||
results = self.cursor.fetchall()
|
||||
usage = {}
|
||||
for result in results:
|
||||
key_md5 = result[0]
|
||||
usage_count = result[1]
|
||||
usage[key_md5] = usage_count
|
||||
return usage
|
||||
|
||||
def dump_api_key_fee(self, api_keys: dict, fee: dict):
|
||||
logging.debug("dumping api key fee...")
|
||||
logging.debug(api_keys)
|
||||
logging.debug(fee)
|
||||
for api_key in api_keys:
|
||||
# 计算key的md5值
|
||||
key_md5 = hashlib.md5(api_keys[api_key].encode('utf-8')).hexdigest()
|
||||
# 获取使用量
|
||||
fee_count = 0
|
||||
if key_md5 in fee:
|
||||
fee_count = fee[key_md5]
|
||||
# 将使用量存进数据库
|
||||
# 先检查是否已存在
|
||||
self.execute("""
|
||||
select count(*) from `account_fee` where `key_md5` = '{}'""".format(key_md5))
|
||||
result = self.cursor.fetchone()
|
||||
if result[0] == 0:
|
||||
# 不存在则插入
|
||||
self.execute("""
|
||||
insert into `account_fee` (`key_md5`, `fee`,`timestamp`) values ('{}', {}, {})
|
||||
""".format(key_md5, fee_count, int(time.time())))
|
||||
else:
|
||||
# 存在则更新,timestamp设置为当前
|
||||
self.execute("""
|
||||
update `account_fee` set `fee` = {}, `timestamp` = {} where `key_md5` = '{}'
|
||||
""".format(fee_count, int(time.time()), key_md5))
|
||||
|
||||
def load_api_key_fee(self):
|
||||
self.execute("""
|
||||
select `key_md5`, `fee` from `account_fee`
|
||||
""")
|
||||
results = self.cursor.fetchall()
|
||||
fee = {}
|
||||
for result in results:
|
||||
key_md5 = result[0]
|
||||
fee_count = result[1]
|
||||
fee[key_md5] = fee_count
|
||||
return fee
|
||||
|
||||
def dump_usage_json(self, usage: dict):
|
||||
json_str = json.dumps(usage)
|
||||
self.execute("""
|
||||
select count(*) from `account_usage`""")
|
||||
result = self.cursor.fetchone()
|
||||
if result[0] == 0:
|
||||
# 不存在则插入
|
||||
self.execute("""
|
||||
insert into `account_usage` (`json`) values ('{}')
|
||||
""".format(json_str))
|
||||
else:
|
||||
# 存在则更新
|
||||
self.execute("""
|
||||
update `account_usage` set `json` = '{}' where `id` = 1
|
||||
""".format(json_str))
|
||||
|
||||
def load_usage_json(self):
|
||||
self.execute("""
|
||||
select `json` from `account_usage` order by id desc limit 1
|
||||
""")
|
||||
result = self.cursor.fetchone()
|
||||
if result is None:
|
||||
return None
|
||||
else:
|
||||
return result[0]
|
||||
@@ -1,137 +0,0 @@
|
||||
# 此模块提供了维护api-key的各种功能
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
import pkg.database.manager
|
||||
import pkg.qqbot.manager
|
||||
import pkg.utils.context
|
||||
|
||||
|
||||
class KeysManager:
|
||||
api_key = {}
|
||||
|
||||
# api-key的使用量
|
||||
# 其中键为api-key的md5值,值为使用量
|
||||
fee = {}
|
||||
|
||||
api_key_fee_threshold = 18.0
|
||||
|
||||
using_key = ""
|
||||
|
||||
alerted = []
|
||||
|
||||
# 在此list中的都是经超额报错标记过的api-key
|
||||
# 记录的是key值,仅在运行时有效
|
||||
exceeded = []
|
||||
|
||||
def get_using_key(self):
|
||||
return self.using_key
|
||||
|
||||
def get_using_key_md5(self):
|
||||
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
||||
|
||||
def __init__(self, api_key):
|
||||
# if hasattr(config, 'api_key_usage_threshold'):
|
||||
# self.api_key_usage_threshold = config.api_key_usage_threshold
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if hasattr(config, 'api_key_fee_threshold'):
|
||||
self.api_key_fee_threshold = config.api_key_fee_threshold
|
||||
self.load_fee()
|
||||
|
||||
if type(api_key) is dict:
|
||||
self.api_key = api_key
|
||||
elif type(api_key) is str:
|
||||
self.api_key = {
|
||||
"default": api_key
|
||||
}
|
||||
elif type(api_key) is list:
|
||||
for i in range(len(api_key)):
|
||||
self.api_key[str(i)] = api_key[i]
|
||||
|
||||
self.auto_switch()
|
||||
# 从usage中删除未加载的api-key的记录
|
||||
# 不删了,也许会运行时添加曾经有记录的api-key
|
||||
|
||||
if 'exceeded_keys' in pkg.utils.context.context and pkg.utils.context.context['exceeded_keys'] is not None:
|
||||
self.exceeded = pkg.utils.context.context['exceeded_keys']
|
||||
|
||||
# 根据tested自动切换到可用的api-key
|
||||
# 返回是否切换成功, 切换后的api-key的别名
|
||||
def auto_switch(self) -> (bool, str):
|
||||
self.dump_fee()
|
||||
for key_name in self.api_key:
|
||||
if self.api_key[key_name] not in self.exceeded:
|
||||
self.using_key = self.api_key[key_name]
|
||||
logging.info("使用api-key:" + key_name)
|
||||
return True, key_name
|
||||
# if self.get_fee(self.api_key[key_name]) < self.api_key_fee_threshold:
|
||||
# self.using_key = self.api_key[key_name]
|
||||
# logging.info("使用api-key:" + key_name)
|
||||
# return True, key_name
|
||||
|
||||
self.using_key = list(self.api_key.values())[0]
|
||||
logging.info("使用api-key:" + list(self.api_key.keys())[0])
|
||||
|
||||
return False, ""
|
||||
|
||||
def add(self, key_name, key):
|
||||
self.api_key[key_name] = key
|
||||
|
||||
# 设置当前使用的api-key使用量超限
|
||||
# 这是在尝试调用api时发生超限异常时调用的
|
||||
def set_current_exceeded(self):
|
||||
# md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
||||
# self.usage[md5] = self.api_key_usage_threshold
|
||||
# self.fee[md5] = self.api_key_fee_threshold
|
||||
self.exceeded.append(self.using_key)
|
||||
self.dump_fee()
|
||||
|
||||
def get_key_name(self, api_key):
|
||||
"""根据api-key获取其别名"""
|
||||
for key_name in self.api_key:
|
||||
if self.api_key[key_name] == api_key:
|
||||
return key_name
|
||||
return ""
|
||||
|
||||
def get_fee(self, api_key):
|
||||
md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest()
|
||||
if md5 not in self.fee:
|
||||
self.fee[md5] = 0
|
||||
return self.fee[md5]
|
||||
|
||||
def report_fee(self, fee: float) -> bool:
|
||||
logging.debug("report fee:" + str(fee))
|
||||
|
||||
md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
||||
if md5 not in self.fee:
|
||||
self.fee[md5] = 0
|
||||
|
||||
self.fee[md5] += fee
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if self.fee[md5] >= self.api_key_fee_threshold and \
|
||||
hasattr(config, 'auto_switch_api_key') and config.auto_switch_api_key:
|
||||
switch_result, key_name = self.auto_switch()
|
||||
|
||||
# 检查是否切换到新的
|
||||
if switch_result:
|
||||
if key_name not in self.alerted:
|
||||
# 通知管理员
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("api-key已切换到:" + key_name)
|
||||
self.alerted.append(key_name)
|
||||
return True
|
||||
else:
|
||||
if key_name not in self.alerted:
|
||||
# 通知管理员
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("api-key已用完,无未使用的api-key可供切换")
|
||||
self.alerted.append(key_name)
|
||||
return False
|
||||
return False
|
||||
|
||||
def dump_fee(self):
|
||||
pkg.utils.context.get_database_manager().dump_api_key_fee(api_keys=self.api_key, fee=self.fee)
|
||||
|
||||
def load_fee(self):
|
||||
self.fee = pkg.utils.context.get_database_manager().load_api_key_fee()
|
||||
logging.info("load fee:" + str(self.fee))
|
||||
@@ -1,72 +0,0 @@
|
||||
import logging
|
||||
|
||||
import openai
|
||||
|
||||
import pkg.openai.keymgr
|
||||
import pkg.openai.pricing as pricing
|
||||
import pkg.utils.context
|
||||
import pkg.audit.gatherer
|
||||
|
||||
|
||||
# 为其他模块提供与OpenAI交互的接口
|
||||
class OpenAIInteract:
|
||||
api_params = {}
|
||||
|
||||
key_mgr: pkg.openai.keymgr.KeysManager = None
|
||||
|
||||
audit_mgr: pkg.audit.gatherer.DataGatherer = None
|
||||
|
||||
default_image_api_params = {
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
# self.api_key = api_key
|
||||
|
||||
self.key_mgr = pkg.openai.keymgr.KeysManager(api_key)
|
||||
self.audit_mgr = pkg.audit.gatherer.DataGatherer()
|
||||
|
||||
openai.api_key = self.key_mgr.get_using_key()
|
||||
|
||||
pkg.utils.context.set_openai_manager(self)
|
||||
|
||||
# 请求OpenAI Completion
|
||||
def request_completion(self, prompt, stop):
|
||||
config = pkg.utils.context.get_config()
|
||||
response = openai.Completion.create(
|
||||
prompt=prompt,
|
||||
stop=stop,
|
||||
timeout=config.process_message_timeout,
|
||||
**config.completion_api_params
|
||||
)
|
||||
|
||||
self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
|
||||
prompt + response['choices'][0]['text'])
|
||||
|
||||
switched = self.key_mgr.report_fee(pricing.language_base_price(config.completion_api_params['model'],
|
||||
prompt + response['choices'][0]['text']))
|
||||
if switched:
|
||||
openai.api_key = self.key_mgr.get_using_key()
|
||||
|
||||
return response
|
||||
|
||||
def request_image(self, prompt):
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
params = config.image_api_params if hasattr(config, "image_api_params") else self.default_image_api_params
|
||||
|
||||
response = openai.Image.create(
|
||||
prompt=prompt,
|
||||
n=1,
|
||||
**params
|
||||
)
|
||||
|
||||
self.audit_mgr.report_image_model_usage(params['size'])
|
||||
|
||||
switched = self.key_mgr.report_fee(pricing.image_price(params['size']))
|
||||
|
||||
if switched:
|
||||
openai.api_key = self.key_mgr.get_using_key()
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# 提供与模型交互的抽象接口
|
||||
|
||||
COMPLETION_MODELS = {
|
||||
'text-davinci-003'
|
||||
}
|
||||
|
||||
EDIT_MODELS = {
|
||||
|
||||
}
|
||||
|
||||
IMAGE_MODELS = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
# ModelManager
|
||||
# 由session包含
|
||||
class ModelMgr(object):
|
||||
|
||||
using_completion_model = ""
|
||||
using_edit_model = ""
|
||||
using_image_model = ""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_using_completion_model(self):
|
||||
return self.using_completion_model
|
||||
|
||||
def get_using_edit_model(self):
|
||||
return self.using_edit_model
|
||||
|
||||
def get_using_image_model(self):
|
||||
return self.using_image_model
|
||||
@@ -1,25 +0,0 @@
|
||||
import logging
|
||||
|
||||
pricing = {
|
||||
"base": { # 文字模型单位是1000字符
|
||||
"text-davinci-003": 0.02,
|
||||
},
|
||||
"image": {
|
||||
"256x256": 0.016,
|
||||
"512x512": 0.018,
|
||||
"1024x1024": 0.02,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def language_base_price(model, text):
|
||||
salt_rate = 0.93
|
||||
length = ((len(text.encode('utf-8')) - len(text)) / 2 + len(text)) * salt_rate
|
||||
logging.debug("text length: %d" % length)
|
||||
|
||||
return pricing["base"][model] * length / 1000
|
||||
|
||||
|
||||
def image_price(size):
|
||||
logging.debug("image size: %s" % size)
|
||||
return pricing["image"][size]
|
||||
@@ -1,280 +0,0 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pkg.openai.manager
|
||||
import pkg.database.manager
|
||||
import pkg.utils.context
|
||||
|
||||
# 运行时保存的所有session
|
||||
sessions = {}
|
||||
|
||||
|
||||
class SessionOfflineStatus:
|
||||
ON_GOING = 'on_going'
|
||||
EXPLICITLY_CLOSED = 'explicitly_closed'
|
||||
|
||||
|
||||
# 从数据加载session
|
||||
def load_sessions():
|
||||
global sessions
|
||||
|
||||
db_inst = pkg.utils.context.get_database_manager()
|
||||
|
||||
session_data = db_inst.load_valid_sessions()
|
||||
|
||||
for session_name in session_data:
|
||||
logging.info('加载session: {}'.format(session_name))
|
||||
|
||||
temp_session = Session(session_name)
|
||||
temp_session.name = session_name
|
||||
temp_session.create_timestamp = session_data[session_name]['create_timestamp']
|
||||
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
|
||||
temp_session.prompt = session_data[session_name]['prompt']
|
||||
|
||||
sessions[session_name] = temp_session
|
||||
|
||||
|
||||
# 获取指定名称的session,如果不存在则创建一个新的
|
||||
def get_session(session_name: str):
|
||||
global sessions
|
||||
if session_name not in sessions:
|
||||
sessions[session_name] = Session(session_name)
|
||||
return sessions[session_name]
|
||||
|
||||
|
||||
def dump_session(session_name: str):
|
||||
global sessions
|
||||
if session_name in sessions:
|
||||
assert isinstance(sessions[session_name], Session)
|
||||
sessions[session_name].persistence()
|
||||
del sessions[session_name]
|
||||
|
||||
|
||||
# def blocked_func(lock: threading.Lock):
|
||||
#
|
||||
# def decorator(func):
|
||||
# def wrapper(*args, **kwargs):
|
||||
# print('lock acquire,{}'.format(lock))
|
||||
# lock.acquire()
|
||||
# try:
|
||||
# return func(*args, **kwargs)
|
||||
# finally:
|
||||
# lock.release()
|
||||
#
|
||||
# return wrapper
|
||||
#
|
||||
# return decorator
|
||||
|
||||
|
||||
# 通用的OpenAI API交互session
|
||||
# session内部保留了对话的上下文,
|
||||
# 收到用户消息后,将上下文提交给OpenAI API生成回复
|
||||
class Session:
|
||||
name = ''
|
||||
|
||||
prompt = ""
|
||||
|
||||
import config
|
||||
|
||||
user_name = config.user_name if hasattr(config, 'user_name') and config.user_name != '' else 'You'
|
||||
bot_name = config.bot_name if hasattr(config, 'bot_name') and config.bot_name != '' else 'Bot'
|
||||
|
||||
create_timestamp = 0
|
||||
|
||||
last_interact_timestamp = 0
|
||||
|
||||
just_switched_to_exist_session = False
|
||||
|
||||
response_lock = None
|
||||
|
||||
# 加锁
|
||||
def acquire_response_lock(self):
|
||||
logging.debug('{},lock acquire,{}'.format(self.name, self.response_lock))
|
||||
self.response_lock.acquire()
|
||||
logging.debug('{},lock acquire successfully,{}'.format(self.name, self.response_lock))
|
||||
|
||||
# 释放锁
|
||||
def release_response_lock(self):
|
||||
if self.response_lock.locked():
|
||||
logging.debug('{},lock release,{}'.format(self.name, self.response_lock))
|
||||
self.response_lock.release()
|
||||
logging.debug('{},lock release successfully,{}'.format(self.name, self.response_lock))
|
||||
|
||||
# 从配置文件获取会话预设信息
|
||||
def get_default_prompt(self):
|
||||
config = pkg.utils.context.get_config()
|
||||
user_name = config.user_name if hasattr(config, 'user_name') and config.user_name != '' else 'You'
|
||||
bot_name = config.bot_name if hasattr(config, 'bot_name') and config.bot_name != '' else 'Bot'
|
||||
return user_name + ":{}\n".format(config.default_prompt if hasattr(config, 'default_prompt') \
|
||||
and config.default_prompt != "" else '') + \
|
||||
bot_name + ":好的\n"
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.create_timestamp = int(time.time())
|
||||
self.last_interact_timestamp = int(time.time())
|
||||
self.schedule()
|
||||
|
||||
self.response_lock = threading.Lock()
|
||||
self.prompt = self.get_default_prompt()
|
||||
|
||||
# 设定检查session最后一次对话是否超过过期时间的计时器
|
||||
def schedule(self):
|
||||
threading.Thread(target=self.expire_check_timer_loop, args=(self.create_timestamp,)).start()
|
||||
|
||||
# 检查session是否已经过期
|
||||
def expire_check_timer_loop(self, create_timestamp: int):
|
||||
global sessions
|
||||
while True:
|
||||
time.sleep(60)
|
||||
|
||||
# 不是此session已更换,退出
|
||||
if self.create_timestamp != create_timestamp or self not in sessions.values():
|
||||
return
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time:
|
||||
logging.info('session {} 已过期'.format(self.name))
|
||||
self.reset(expired=True, schedule_new=False)
|
||||
|
||||
# 删除此session
|
||||
del sessions[self.name]
|
||||
return
|
||||
|
||||
# 请求回复
|
||||
# 这个函数是阻塞的
|
||||
def append(self, text: str) -> str:
|
||||
self.last_interact_timestamp = int(time.time())
|
||||
|
||||
# max_rounds = config.prompt_submit_round_amount if hasattr(config, 'prompt_submit_round_amount') else 7
|
||||
config = pkg.utils.context.get_config()
|
||||
max_rounds = 1000 # 不再限制回合数
|
||||
max_length = config.prompt_submit_length if hasattr(config, "prompt_submit_length") else 1024
|
||||
|
||||
# 向API请求补全
|
||||
response = pkg.utils.context.get_openai_manager().request_completion(
|
||||
self.cut_out(self.prompt + self.user_name + ':' +
|
||||
text + '\n' + self.bot_name + ':',
|
||||
max_rounds, max_length),
|
||||
self.user_name + ':')
|
||||
|
||||
self.prompt += self.user_name + ':' + text + '\n' + self.bot_name + ':'
|
||||
# print(response)
|
||||
# 处理回复
|
||||
res_test = response["choices"][0]["text"]
|
||||
res_ans = res_test
|
||||
|
||||
# 去除开头可能的提示
|
||||
res_ans_spt = res_test.split("\n\n")
|
||||
if len(res_ans_spt) > 1:
|
||||
del (res_ans_spt[0])
|
||||
res_ans = '\n\n'.join(res_ans_spt)
|
||||
|
||||
self.prompt += "{}".format(res_ans) + '\n'
|
||||
|
||||
if self.just_switched_to_exist_session:
|
||||
self.just_switched_to_exist_session = False
|
||||
self.set_ongoing()
|
||||
|
||||
return res_ans
|
||||
|
||||
# 从尾部截取prompt里不多于max_rounds个回合,长度不大于max_tokens的字符串
|
||||
# 保证都是完整的对话
|
||||
def cut_out(self, prompt: str, max_rounds: int, max_tokens: int) -> str:
|
||||
# 分隔出每个回合
|
||||
rounds_spt_by_user_name = prompt.split(self.user_name + ':')
|
||||
|
||||
result = ''
|
||||
|
||||
checked_rounds = 0
|
||||
# 从后往前遍历,加到result前面,检查result是否符合要求
|
||||
for i in range(len(rounds_spt_by_user_name) - 1, 0, -1):
|
||||
result_temp = self.user_name + ':' + rounds_spt_by_user_name[i] + result
|
||||
checked_rounds += 1
|
||||
|
||||
if checked_rounds > max_rounds:
|
||||
break
|
||||
|
||||
if int((len(result_temp.encode('utf-8')) - len(result_temp)) / 2 + len(result_temp)) > max_tokens:
|
||||
break
|
||||
|
||||
result = result_temp
|
||||
|
||||
logging.debug('cut_out: {}'.format(result))
|
||||
return result
|
||||
|
||||
# 持久化session
|
||||
def persistence(self):
|
||||
if self.prompt == self.get_default_prompt():
|
||||
return
|
||||
|
||||
db_inst = pkg.utils.context.get_database_manager()
|
||||
|
||||
name_spt = self.name.split('_')
|
||||
|
||||
subject_type = name_spt[0]
|
||||
subject_number = int(name_spt[1])
|
||||
|
||||
db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp,
|
||||
self.prompt)
|
||||
|
||||
# 重置session
|
||||
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True):
|
||||
if self.prompt != self.get_default_prompt():
|
||||
self.persistence()
|
||||
if explicit:
|
||||
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
||||
|
||||
if expired:
|
||||
pkg.utils.context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
|
||||
self.prompt = self.get_default_prompt()
|
||||
self.create_timestamp = int(time.time())
|
||||
self.last_interact_timestamp = int(time.time())
|
||||
self.just_switched_to_exist_session = False
|
||||
|
||||
# self.response_lock = threading.Lock()
|
||||
|
||||
if schedule_new:
|
||||
self.schedule()
|
||||
|
||||
# 将本session的数据库状态设置为on_going
|
||||
def set_ongoing(self):
|
||||
pkg.utils.context.get_database_manager().set_session_ongoing(self.name, self.create_timestamp)
|
||||
|
||||
# 切换到上一个session
|
||||
def last_session(self):
|
||||
last_one = pkg.utils.context.get_database_manager().last_session(self.name, self.last_interact_timestamp)
|
||||
if last_one is None:
|
||||
return None
|
||||
else:
|
||||
self.persistence()
|
||||
|
||||
self.create_timestamp = last_one['create_timestamp']
|
||||
self.last_interact_timestamp = last_one['last_interact_timestamp']
|
||||
self.prompt = last_one['prompt']
|
||||
|
||||
self.just_switched_to_exist_session = True
|
||||
return self
|
||||
|
||||
# 切换到下一个session
|
||||
def next_session(self):
|
||||
next_one = pkg.utils.context.get_database_manager().next_session(self.name, self.last_interact_timestamp)
|
||||
if next_one is None:
|
||||
return None
|
||||
else:
|
||||
self.persistence()
|
||||
|
||||
self.create_timestamp = next_one['create_timestamp']
|
||||
self.last_interact_timestamp = next_one['last_interact_timestamp']
|
||||
self.prompt = next_one['prompt']
|
||||
|
||||
self.just_switched_to_exist_session = True
|
||||
return self
|
||||
|
||||
def list_history(self, capacity: int = 10, page: int = 0):
|
||||
return pkg.utils.context.get_database_manager().list_history(self.name, capacity, page,
|
||||
self.get_default_prompt())
|
||||
|
||||
def draw_image(self, prompt: str):
|
||||
return pkg.utils.context.get_openai_manager().request_image(prompt)
|
||||
@@ -1,18 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
class ReplyFilter:
|
||||
|
||||
sensitive_words = []
|
||||
|
||||
def __init__(self, sensitive_words: list):
|
||||
self.sensitive_words = sensitive_words
|
||||
|
||||
def process(self, message: str) -> str:
|
||||
for word in self.sensitive_words:
|
||||
match = re.findall(word, message)
|
||||
if len(match) > 0:
|
||||
for i in range(len(match)):
|
||||
message = message.replace(match[i], "*" * len(match[i]))
|
||||
|
||||
return message
|
||||
@@ -1,225 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
|
||||
import mirai.models.bus
|
||||
from mirai import At, GroupMessage, MessageEvent, Mirai, Plain, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
|
||||
FriendMessage, Image
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
from func_timeout import FunctionTimedOut
|
||||
import logging
|
||||
|
||||
import pkg.qqbot.filter
|
||||
import pkg.qqbot.process as processor
|
||||
import pkg.utils.context
|
||||
|
||||
|
||||
# 并行运行
|
||||
def go(func, args=()):
|
||||
thread = threading.Thread(target=func, args=args, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
# 检查消息是否符合泛响应匹配机制
|
||||
def check_response_rule(text: str) -> (bool, str):
|
||||
config = pkg.utils.context.get_config()
|
||||
if not hasattr(config, 'response_rules'):
|
||||
return False, ''
|
||||
|
||||
rules = config.response_rules
|
||||
# 检查前缀匹配
|
||||
if 'prefix' in rules:
|
||||
for rule in rules['prefix']:
|
||||
if text.startswith(rule):
|
||||
return True, text.replace(rule, "", 1)
|
||||
|
||||
# 检查正则表达式匹配
|
||||
if 'regexp' in rules:
|
||||
for rule in rules['regexp']:
|
||||
import re
|
||||
match = re.match(rule, text)
|
||||
if match:
|
||||
return True, text
|
||||
|
||||
return False, ""
|
||||
|
||||
|
||||
# 控制QQ消息输入输出的类
|
||||
class QQBotManager:
|
||||
retry = 3
|
||||
|
||||
bot = None
|
||||
|
||||
reply_filter = None
|
||||
|
||||
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, first_time_init=True):
|
||||
|
||||
self.timeout = timeout
|
||||
self.retry = retry
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if os.path.exists("sensitive.json") \
|
||||
and config.sensitive_word_filter is not None \
|
||||
and config.sensitive_word_filter:
|
||||
with open("sensitive.json", "r", encoding="utf-8") as f:
|
||||
self.reply_filter = pkg.qqbot.filter.ReplyFilter(json.load(f)['words'])
|
||||
else:
|
||||
self.reply_filter = pkg.qqbot.filter.ReplyFilter([])
|
||||
|
||||
# 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用
|
||||
# 故只在第一次初始化时创建bot对象,重载之后使用原bot对象
|
||||
# 因此,bot的配置不支持热重载
|
||||
if first_time_init:
|
||||
self.first_time_init(mirai_http_api_config)
|
||||
else:
|
||||
self.bot = pkg.utils.context.get_qqbot_manager().bot
|
||||
|
||||
pkg.utils.context.set_qqbot_manager(self)
|
||||
|
||||
# Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码
|
||||
@self.bot.on(FriendMessage)
|
||||
async def on_friend_message(event: FriendMessage):
|
||||
go(self.on_person_message, (event,))
|
||||
|
||||
@self.bot.on(StrangerMessage)
|
||||
async def on_stranger_message(event: StrangerMessage):
|
||||
go(self.on_person_message, (event,))
|
||||
|
||||
@self.bot.on(GroupMessage)
|
||||
async def on_group_message(event: GroupMessage):
|
||||
go(self.on_group_message, (event,))
|
||||
|
||||
def unsubscribe_all():
|
||||
"""取消所有订阅
|
||||
|
||||
用于在热重载流程中卸载所有事件处理器
|
||||
"""
|
||||
assert isinstance(self.bot, Mirai)
|
||||
bus = self.bot.bus
|
||||
assert isinstance(bus, mirai.models.bus.ModelEventBus)
|
||||
|
||||
bus.unsubscribe(FriendMessage, on_friend_message)
|
||||
bus.unsubscribe(StrangerMessage, on_stranger_message)
|
||||
bus.unsubscribe(GroupMessage, on_group_message)
|
||||
|
||||
self.unsubscribe_all = unsubscribe_all
|
||||
|
||||
def first_time_init(self, mirai_http_api_config: dict):
|
||||
"""热重载后不再运行此函数"""
|
||||
|
||||
if 'adapter' not in mirai_http_api_config or mirai_http_api_config['adapter'] == "WebSocketAdapter":
|
||||
bot = Mirai(
|
||||
qq=mirai_http_api_config['qq'],
|
||||
adapter=WebSocketAdapter(
|
||||
verify_key=mirai_http_api_config['verifyKey'],
|
||||
host=mirai_http_api_config['host'],
|
||||
port=mirai_http_api_config['port']
|
||||
)
|
||||
)
|
||||
elif mirai_http_api_config['adapter'] == "HTTPAdapter":
|
||||
bot = Mirai(
|
||||
qq=mirai_http_api_config['qq'],
|
||||
adapter=HTTPAdapter(
|
||||
verify_key=mirai_http_api_config['verifyKey'],
|
||||
host=mirai_http_api_config['host'],
|
||||
port=mirai_http_api_config['port']
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
raise Exception("未知的适配器类型")
|
||||
|
||||
self.bot = bot
|
||||
|
||||
def send(self, event, msg, check_quote=True):
|
||||
config = pkg.utils.context.get_config()
|
||||
asyncio.run(
|
||||
self.bot.send(event, msg, quote=True if hasattr(config,
|
||||
"quote_origin") and config.quote_origin and check_quote else False))
|
||||
|
||||
# 私聊消息处理
|
||||
def on_person_message(self, event: MessageEvent):
|
||||
reply = ''
|
||||
|
||||
if event.sender.id == self.bot.qq:
|
||||
pass
|
||||
else:
|
||||
if Image in event.message_chain:
|
||||
pass
|
||||
else:
|
||||
# 超时则重试,重试超过次数则放弃
|
||||
failed = 0
|
||||
for i in range(self.retry):
|
||||
try:
|
||||
reply = processor.process_message('person', event.sender.id, str(event.message_chain),
|
||||
event.message_chain,
|
||||
event.sender.id)
|
||||
break
|
||||
except FunctionTimedOut:
|
||||
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
if failed == self.retry:
|
||||
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
||||
self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id)))
|
||||
reply = ["[bot]err:请求超时"]
|
||||
|
||||
if reply:
|
||||
return self.send(event, reply, check_quote=False)
|
||||
|
||||
# 群消息处理
|
||||
def on_group_message(self, event: GroupMessage):
|
||||
|
||||
reply = ''
|
||||
|
||||
def process(text=None) -> str:
|
||||
replys = ""
|
||||
if At(self.bot.qq) in event.message_chain:
|
||||
event.message_chain.remove(At(self.bot.qq))
|
||||
|
||||
# 超时则重试,重试超过次数则放弃
|
||||
failed = 0
|
||||
for i in range(self.retry):
|
||||
try:
|
||||
replys = processor.process_message('group', event.group.id,
|
||||
str(event.message_chain).strip() if text is None else text,
|
||||
event.message_chain,
|
||||
event.sender.id)
|
||||
break
|
||||
except FunctionTimedOut:
|
||||
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
if failed == self.retry:
|
||||
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
||||
self.notify_admin("{} 请求超时".format("group_{}".format(event.group.id)))
|
||||
replys = ["[bot]err:请求超时"]
|
||||
|
||||
return replys
|
||||
|
||||
if Image in event.message_chain:
|
||||
pass
|
||||
elif At(self.bot.qq) not in event.message_chain:
|
||||
check, result = check_response_rule(str(event.message_chain).strip())
|
||||
|
||||
if check:
|
||||
reply = process(result.strip())
|
||||
else:
|
||||
# 直接调用
|
||||
reply = process()
|
||||
|
||||
if reply:
|
||||
return self.send(event, reply)
|
||||
|
||||
# 通知系统管理员
|
||||
def notify_admin(self, message: str):
|
||||
config = pkg.utils.context.get_config()
|
||||
if hasattr(config, "admin_qq") and config.admin_qq != 0:
|
||||
logging.info("通知管理员:{}".format(message))
|
||||
send_task = self.bot.send_friend_message(config.admin_qq, "[bot]{}".format(message))
|
||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
||||
@@ -1,330 +0,0 @@
|
||||
# 此模块提供了消息处理的具体逻辑的接口
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
from func_timeout import func_set_timeout
|
||||
import logging
|
||||
import openai
|
||||
|
||||
from mirai import Image, MessageChain
|
||||
|
||||
# 这里不使用动态引入config
|
||||
# 因为在这里动态引入会卡死程序
|
||||
# 而此模块静态引用config与动态引入的表现一致
|
||||
import config as config_init_import
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
import pkg.utils.reloader
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.context
|
||||
|
||||
processing = []
|
||||
|
||||
|
||||
def config_operation(cmd, params):
|
||||
reply = []
|
||||
config = pkg.utils.context.get_config()
|
||||
reply_str = ""
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入配置项"]
|
||||
else:
|
||||
cfg_name = params[0]
|
||||
if cfg_name == 'all':
|
||||
reply_str = "[bot]所有配置项:\n\n"
|
||||
for cfg in dir(config):
|
||||
if not cfg.startswith('__') and not cfg == 'logging':
|
||||
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
||||
if isinstance(getattr(config, cfg), str):
|
||||
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
|
||||
elif isinstance(getattr(config, cfg), dict):
|
||||
# 不进行unicode转义,并格式化
|
||||
reply_str += "{}: {}\n".format(cfg,
|
||||
json.dumps(getattr(config, cfg),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
|
||||
reply = [reply_str]
|
||||
elif cfg_name in dir(config):
|
||||
if len(params) == 1:
|
||||
# 按照配置项类型进行格式化
|
||||
if isinstance(getattr(config, cfg_name), str):
|
||||
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
|
||||
elif isinstance(getattr(config, cfg_name), dict):
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
||||
json.dumps(getattr(config, cfg_name),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
|
||||
reply = [reply_str]
|
||||
else:
|
||||
cfg_value = " ".join(params[1:])
|
||||
# 类型转换,如果是json则转换为字典
|
||||
if cfg_value == 'true':
|
||||
cfg_value = True
|
||||
elif cfg_value == 'false':
|
||||
cfg_value = False
|
||||
elif cfg_value.isdigit():
|
||||
cfg_value = int(cfg_value)
|
||||
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
|
||||
cfg_value = json.loads(cfg_value)
|
||||
else:
|
||||
try:
|
||||
cfg_value = float(cfg_value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 检查类型是否匹配
|
||||
if isinstance(getattr(config, cfg_name), type(cfg_value)):
|
||||
setattr(config, cfg_name, cfg_value)
|
||||
pkg.utils.context.set_config(config)
|
||||
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||
else:
|
||||
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
||||
|
||||
else:
|
||||
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@func_set_timeout(config_init_import.process_message_timeout)
|
||||
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain,
|
||||
sender_id: int) -> MessageChain:
|
||||
global processing
|
||||
|
||||
mgr = pkg.utils.context.get_qqbot_manager()
|
||||
|
||||
reply = []
|
||||
session_name = "{}_{}".format(launcher_type, launcher_id)
|
||||
|
||||
# 检查是否被禁言
|
||||
if launcher_type == 'group':
|
||||
result = mgr.bot.member_info(target=launcher_id, member_id=mgr.bot.qq).get()
|
||||
result = asyncio.run(result)
|
||||
if result.mute_time_remaining > 0:
|
||||
logging.info("机器人被禁言,跳过消息处理(group_{},剩余{}s)".format(launcher_id,
|
||||
result.mute_time_remaining))
|
||||
return reply
|
||||
|
||||
pkg.openai.session.get_session(session_name).acquire_response_lock()
|
||||
|
||||
try:
|
||||
if session_name in processing:
|
||||
pkg.openai.session.get_session(session_name).release_response_lock()
|
||||
return ["[bot]err:正在处理中,请稍后再试"]
|
||||
|
||||
processing.append(session_name)
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
|
||||
try:
|
||||
|
||||
if text_message.startswith('!') or text_message.startswith("!"): # 指令
|
||||
try:
|
||||
logging.info(
|
||||
"[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
||||
"..." if len(text_message) > 20 else "")))
|
||||
|
||||
cmd = text_message[1:].strip().split(' ')[0]
|
||||
|
||||
params = text_message[1:].strip().split(' ')[1:]
|
||||
if cmd == 'help':
|
||||
reply = ["[bot]" + config.help_message]
|
||||
elif cmd == 'reset':
|
||||
pkg.openai.session.get_session(session_name).reset(explicit=True)
|
||||
reply = ["[bot]会话已重置"]
|
||||
elif cmd == 'last':
|
||||
result = pkg.openai.session.get_session(session_name).last_session()
|
||||
if result is None:
|
||||
reply = ["[bot]没有前一次的对话"]
|
||||
else:
|
||||
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(
|
||||
datetime_str) + result.prompt[
|
||||
:min(100,
|
||||
len(result.prompt))] + \
|
||||
("..." if len(result.prompt) > 100 else "#END#")]
|
||||
elif cmd == 'next':
|
||||
result = pkg.openai.session.get_session(session_name).next_session()
|
||||
if result is None:
|
||||
reply = ["[bot]没有后一次的对话"]
|
||||
else:
|
||||
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(
|
||||
datetime_str) + result.prompt[
|
||||
:min(100,
|
||||
len(result.prompt))] + \
|
||||
("..." if len(result.prompt) > 100 else "#END#")]
|
||||
elif cmd == 'prompt':
|
||||
reply = ["[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt]
|
||||
elif cmd == 'list':
|
||||
pkg.openai.session.get_session(session_name).persistence()
|
||||
page = 0
|
||||
|
||||
if len(params) > 0:
|
||||
try:
|
||||
page = int(params[0])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
results = pkg.openai.session.get_session(session_name).list_history(page=page)
|
||||
if len(results) == 0:
|
||||
reply = ["[bot]第{}页没有历史会话".format(page)]
|
||||
else:
|
||||
reply_str = "[bot]历史会话 第{}页:\n".format(page)
|
||||
current = -1
|
||||
for i in range(len(results)):
|
||||
# 时间(使用create_timestamp转换) 序号 部分内容
|
||||
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
|
||||
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
|
||||
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
results[i]['prompt'][
|
||||
:min(20, len(results[i]['prompt']))])
|
||||
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
|
||||
session_name).create_timestamp:
|
||||
current = i + page * 10
|
||||
|
||||
reply_str += "\n以上信息倒序排列"
|
||||
if current != -1:
|
||||
reply_str += ",当前会话是 #{}\n".format(current)
|
||||
else:
|
||||
reply_str += ",当前处于全新会话或不在此页"
|
||||
|
||||
reply = [reply_str]
|
||||
elif cmd == 'fee':
|
||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
||||
reply_str = "[bot]api-key费用情况(估算):(阈值:{})\n\n".format(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.api_key_fee_threshold)
|
||||
|
||||
using_key_name = ""
|
||||
for api_key in api_keys:
|
||||
reply_str += "{}:\n - {}美元 {}%\n".format(api_key,
|
||||
round(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
||||
api_keys[api_key]), 6),
|
||||
round(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
||||
api_keys[
|
||||
api_key]) / pkg.utils.context.get_openai_manager().key_mgr.api_key_fee_threshold * 100,
|
||||
3))
|
||||
if api_keys[api_key] == pkg.utils.context.get_openai_manager().key_mgr.using_key:
|
||||
using_key_name = api_key
|
||||
reply_str += "\n当前使用:{}".format(using_key_name)
|
||||
|
||||
reply = [reply_str]
|
||||
elif cmd == 'usage':
|
||||
reply_str = "[bot]各api-key使用情况:\n\n"
|
||||
|
||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
||||
for key_name in api_keys:
|
||||
text_length = pkg.utils.context.get_openai_manager().audit_mgr\
|
||||
.get_text_length_of_key(api_keys[key_name])
|
||||
image_count = pkg.utils.context.get_openai_manager().audit_mgr\
|
||||
.get_image_count_of_key(api_keys[key_name])
|
||||
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length), int(image_count))
|
||||
|
||||
reply = [reply_str]
|
||||
elif cmd == 'draw':
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入图片描述文字"]
|
||||
else:
|
||||
session = pkg.openai.session.get_session(session_name)
|
||||
|
||||
res = session.draw_image(" ".join(params))
|
||||
|
||||
logging.debug("draw_image result:{}".format(res))
|
||||
reply = [Image(url=res['data'][0]['url'])]
|
||||
if not (hasattr(config, 'include_image_description')
|
||||
and not config.include_image_description):
|
||||
reply.append(" ".join(params))
|
||||
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def reload_task():
|
||||
pkg.utils.reloader.reload_all()
|
||||
|
||||
threading.Thread(target=reload_task, daemon=True).start()
|
||||
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
def update_task():
|
||||
try:
|
||||
pkg.utils.updater.update_all()
|
||||
except Exception as e0:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
|
||||
return
|
||||
pkg.utils.reloader.reload_all(notify=False)
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
|
||||
|
||||
threading.Thread(target=update_task, daemon=True).start()
|
||||
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
reply = config_operation(cmd, params)
|
||||
else:
|
||||
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
|
||||
config_item = cmd[1:]
|
||||
params = [config_item] + params
|
||||
reply = config_operation("cfg", params)
|
||||
else:
|
||||
reply = ["[bot]err:未知的指令或权限不足: "+cmd]
|
||||
except Exception as e:
|
||||
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
|
||||
logging.exception(e)
|
||||
reply = ["[bot]err:{}".format(e)]
|
||||
else: # 消息
|
||||
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
||||
"..." if len(text_message) > 20 else "")))
|
||||
|
||||
session = pkg.openai.session.get_session(session_name)
|
||||
|
||||
while True:
|
||||
try:
|
||||
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
|
||||
reply = [prefix + session.append(text_message)]
|
||||
except openai.error.APIConnectionError as e:
|
||||
mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e))
|
||||
reply = ["[bot]err:调用API失败,请重试或联系作者,或等待修复"]
|
||||
except openai.error.RateLimitError as e:
|
||||
# 尝试切换api-key
|
||||
current_tokens_amt = pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.get_using_key())
|
||||
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
|
||||
|
||||
if not switched:
|
||||
mgr.notify_admin("API调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
|
||||
current_tokens_amt))
|
||||
reply = ["[bot]err:API调用额度超额,请联系作者,或等待修复"]
|
||||
else:
|
||||
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
|
||||
mgr.notify_admin("API调用额度超限({}),接口报错,已切换到{}".format(current_tokens_amt, name))
|
||||
reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"]
|
||||
continue
|
||||
except openai.error.InvalidRequestError as e:
|
||||
mgr.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或"
|
||||
"completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format(
|
||||
session_name, e))
|
||||
reply = ["[bot]err:API调用参数错误,请联系作者,或等待修复"]
|
||||
except openai.error.ServiceUnavailableError as e:
|
||||
# mgr.notify_admin("{}API调用服务不可用:{}".format(session_name, e))
|
||||
reply = ["[bot]err:API调用服务暂不可用,请尝试重试"]
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
reply = ["[bot]err:{}".format(e)]
|
||||
break
|
||||
|
||||
if reply is not None and type(reply[0]) == str:
|
||||
logging.info(
|
||||
"回复[{}]文字消息:{}".format(session_name,
|
||||
reply[0][:min(100, len(reply[0]))] + (
|
||||
"..." if len(reply[0]) > 100 else "")))
|
||||
reply = [mgr.reply_filter.process(reply[0])]
|
||||
else:
|
||||
logging.info("回复[{}]图片消息:{}".format(session_name, reply))
|
||||
|
||||
finally:
|
||||
processing.remove(session_name)
|
||||
finally:
|
||||
pkg.openai.session.get_session(session_name).release_response_lock()
|
||||
|
||||
return MessageChain(reply)
|
||||
@@ -1,45 +0,0 @@
|
||||
import pkg.database.manager
|
||||
import pkg.openai.manager
|
||||
import pkg.qqbot.manager
|
||||
|
||||
context = {
|
||||
'inst': {
|
||||
'database.manager.DatabaseManager': None,
|
||||
'openai.manager.OpenAIInteract': None,
|
||||
'qqbot.manager.QQBotManager': None,
|
||||
},
|
||||
'logger_handler': None,
|
||||
'config': None,
|
||||
}
|
||||
|
||||
|
||||
def set_config(inst):
|
||||
context['config'] = inst
|
||||
|
||||
|
||||
def get_config():
|
||||
return context['config']
|
||||
|
||||
|
||||
def set_database_manager(inst):
|
||||
context['inst']['database.manager.DatabaseManager'] = inst
|
||||
|
||||
|
||||
def get_database_manager():
|
||||
return context['inst']['database.manager.DatabaseManager']
|
||||
|
||||
|
||||
def set_openai_manager(inst):
|
||||
context['inst']['openai.manager.OpenAIInteract'] = inst
|
||||
|
||||
|
||||
def get_openai_manager():
|
||||
return context['inst']['openai.manager.OpenAIInteract']
|
||||
|
||||
|
||||
def set_qqbot_manager(inst):
|
||||
context['inst']['qqbot.manager.QQBotManager'] = inst
|
||||
|
||||
|
||||
def get_qqbot_manager():
|
||||
return context['inst']['qqbot.manager.QQBotManager']
|
||||
@@ -1,42 +0,0 @@
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
import pkg.utils.context
|
||||
|
||||
|
||||
def walk(module, prefix=''):
|
||||
"""遍历并重载所有模块"""
|
||||
for item in pkgutil.iter_modules(module.__path__):
|
||||
if item.ispkg:
|
||||
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.')
|
||||
else:
|
||||
logging.info('reload module: {}'.format(prefix + item.name))
|
||||
importlib.reload(__import__(module.__name__ + '.' + item.name, fromlist=['']))
|
||||
|
||||
|
||||
def reload_all(notify=True):
|
||||
# 解除bot的事件注册
|
||||
import pkg
|
||||
pkg.utils.context.get_qqbot_manager().unsubscribe_all()
|
||||
# 执行关闭流程
|
||||
logging.info("执行程序关闭流程")
|
||||
import main
|
||||
main.stop()
|
||||
|
||||
# 重载所有模块
|
||||
pkg.utils.context.context['exceeded_keys'] = pkg.utils.context.get_openai_manager().key_mgr.exceeded
|
||||
context = pkg.utils.context.context
|
||||
walk(pkg)
|
||||
importlib.reload(__import__('config'))
|
||||
importlib.reload(__import__('main'))
|
||||
pkg.utils.context.context = context
|
||||
|
||||
# 执行启动流程
|
||||
logging.info("执行程序启动流程")
|
||||
threading.Thread(target=main.main, args=(False,), daemon=False).start()
|
||||
|
||||
logging.info('程序启动完成')
|
||||
if notify:
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("重载完成")
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
def update_all():
|
||||
"""使用dulwich更新源码"""
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
try:
|
||||
from dulwich import porcelain
|
||||
repo = porcelain.open_repo('.')
|
||||
porcelain.pull(repo)
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
except dulwich.porcelain.DivergedBranches:
|
||||
raise Exception("分支不一致,自动更新仅支持master分支,请手动更新(https://github.com/RockChinQ/QChatGPT/issues/76)")
|
||||
214
pyproject.toml
Normal file
214
pyproject.toml
Normal file
@@ -0,0 +1,214 @@
|
||||
[project]
|
||||
name = "langbot"
|
||||
version = "4.5.4"
|
||||
description = "Easy-to-use global IM bot platform designed for LLM era"
|
||||
readme = "README.md"
|
||||
license-files = ["LICENSE"]
|
||||
requires-python = ">=3.10.1,<4.0"
|
||||
dependencies = [
|
||||
"aiocqhttp>=1.4.4",
|
||||
"aiofiles>=24.1.0",
|
||||
"aiohttp>=3.11.18",
|
||||
"aioshutil>=1.5",
|
||||
"aiosqlite>=0.21.0",
|
||||
"anthropic>=0.51.0",
|
||||
"argon2-cffi>=23.1.0",
|
||||
"async-lru>=2.0.5",
|
||||
"certifi>=2025.4.26",
|
||||
"colorlog~=6.6.0",
|
||||
"cryptography>=44.0.3",
|
||||
"dashscope>=1.23.2",
|
||||
"dingtalk-stream>=0.24.0",
|
||||
"discord-py>=2.5.2",
|
||||
"pynacl>=1.5.0", # Required for Discord voice support
|
||||
"gewechat-client>=0.1.5",
|
||||
"lark-oapi>=1.4.15",
|
||||
"mcp>=1.8.1",
|
||||
"nakuru-project-idk>=0.0.2.1",
|
||||
"ollama>=0.4.8",
|
||||
"openai>1.0.0",
|
||||
"pillow>=11.2.1",
|
||||
"psutil>=7.0.0",
|
||||
"pycryptodome>=3.22.0",
|
||||
"pydantic>2.0",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-telegram-bot>=22.0",
|
||||
"pyyaml>=6.0.2",
|
||||
"qq-botpy-rc>=1.2.1.6",
|
||||
"quart>=0.20.0",
|
||||
"quart-cors>=0.8.0",
|
||||
"requests>=2.32.3",
|
||||
"slack-sdk>=3.35.0",
|
||||
"sqlalchemy[asyncio]>=2.0.40",
|
||||
"sqlmodel>=0.0.24",
|
||||
"telegramify-markdown>=0.5.1",
|
||||
"tiktoken>=0.9.0",
|
||||
"urllib3>=2.4.0",
|
||||
"websockets>=15.0.1",
|
||||
"python-socks>=2.7.1", # dingtalk missing dependency
|
||||
"pip>=25.1.1",
|
||||
"ruff>=0.11.9",
|
||||
"pre-commit>=4.2.0",
|
||||
"uv>=0.7.11",
|
||||
"mypy>=1.16.0",
|
||||
"PyPDF2>=3.0.1",
|
||||
"python-docx>=1.1.0",
|
||||
"pandas>=2.2.2",
|
||||
"chardet>=5.2.0",
|
||||
"markdown>=3.6",
|
||||
"beautifulsoup4>=4.12.3",
|
||||
"ebooklib>=0.18",
|
||||
"html2text>=2024.2.26",
|
||||
"langchain>=0.2.0",
|
||||
"langchain-text-splitters>=0.0.1",
|
||||
"chromadb>=0.4.24",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"langbot-plugin==0.1.13b1",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"tboxsdk>=0.0.10",
|
||||
"boto3>=1.35.0",
|
||||
]
|
||||
keywords = [
|
||||
"bot",
|
||||
"agent",
|
||||
"telegram",
|
||||
"plugins",
|
||||
"openai",
|
||||
"instant-messaging",
|
||||
"wechat",
|
||||
"qq",
|
||||
"dify",
|
||||
"llm",
|
||||
"chatgpt",
|
||||
"deepseek",
|
||||
"onebot",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: AsyncIO",
|
||||
"Framework :: Robot Framework",
|
||||
"Framework :: Robot Framework :: Library",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Chat",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://langbot.app"
|
||||
Documentation = "https://docs.langbot.app"
|
||||
Repository = "https://github.com/langbot-app/LangBot"
|
||||
|
||||
[project.scripts]
|
||||
langbot = "langbot.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
package-data = { "langbot" = ["templates/**", "pkg/provider/modelmgr/requesters/*", "pkg/platform/sources/*", "web/out/**"] }
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=4.2.0",
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.0.0",
|
||||
"pytest-cov>=7.0.0",
|
||||
"ruff>=0.11.9",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
|
||||
line-length = 120
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.12
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = [
|
||||
"E712", # Comparison to true should be 'if cond is true:' or 'if cond:' (E712)
|
||||
"F402", # Import `loader` from line 8 shadowed by loop variable
|
||||
"F403", # * used, unable to detect undefined names
|
||||
"F405", # may be undefined, or defined from star imports
|
||||
"E741", # Ambiguous variable name: `l`
|
||||
"E722", # bare-except
|
||||
"E721", # type-comparison
|
||||
"F821", # undefined-all
|
||||
"FURB113", # repeated-append
|
||||
"FURB152", # math-constant
|
||||
"UP007", # non-pep604-annotation
|
||||
"UP032", # f-string
|
||||
"UP045", # non-pep604-annotation-optional
|
||||
"B005", # strip-with-multi-characters
|
||||
"B006", # mutable-argument-default
|
||||
"B007", # unused-loop-control-variable
|
||||
"B026", # star-arg-unpacking-after-keyword-arg
|
||||
"B903", # class-as-data-structure
|
||||
"B904", # raise-without-from-inside-except
|
||||
"B905", # zip-without-explicit-strict
|
||||
"N806", # non-lowercase-variable-in-function
|
||||
"N815", # mixed-case-variable-in-class-scope
|
||||
"PT011", # pytest-raises-too-broad
|
||||
"SIM102", # collapsible-if
|
||||
"SIM103", # needless-bool
|
||||
"SIM105", # suppressible-exception
|
||||
"SIM107", # return-in-try-except-finally
|
||||
"SIM108", # if-else-block-instead-of-if-exp
|
||||
"SIM113", # enumerate-for-loop
|
||||
"SIM117", # multiple-with-statements
|
||||
"SIM210", # if-expr-with-true-false
|
||||
]
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "single"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
39
pytest.ini
Normal file
39
pytest.ini
Normal file
@@ -0,0 +1,39 @@
|
||||
[pytest]
|
||||
# Test discovery patterns
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
|
||||
# Test paths
|
||||
testpaths = tests
|
||||
|
||||
# Asyncio configuration
|
||||
asyncio_mode = auto
|
||||
|
||||
# Output options
|
||||
addopts =
|
||||
-v
|
||||
--strict-markers
|
||||
--tb=short
|
||||
--disable-warnings
|
||||
|
||||
# Markers
|
||||
markers =
|
||||
asyncio: mark test as async
|
||||
unit: mark test as unit test
|
||||
integration: mark test as integration test
|
||||
slow: mark test as slow running
|
||||
|
||||
# Coverage options (when using pytest-cov)
|
||||
[coverage:run]
|
||||
source = langbot.pkg
|
||||
omit =
|
||||
*/tests/*
|
||||
*/test_*.py
|
||||
*/__pycache__/*
|
||||
*/site-packages/*
|
||||
|
||||
[coverage:report]
|
||||
precision = 2
|
||||
show_missing = True
|
||||
skip_covered = False
|
||||
1
res/announcement.json
Normal file
1
res/announcement.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
res/announcement_saved.json
Normal file
1
res/announcement_saved.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
res/instance_id.json
Normal file
1
res/instance_id.json
Normal file
@@ -0,0 +1 @@
|
||||
{"host_id": "host_9b4a220d-3bb6-42fc-aec3-41188ce0a41c", "instance_id": "instance_61d8f262-b98a-4165-8e77-85fb6262529e", "instance_create_ts": 1736824678}
|
||||
BIN
res/logo.png
Normal file
BIN
res/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
32
res/scripts/publish_announcement.py
Normal file
32
res/scripts/publish_announcement.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# 输出工作路径
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
|
||||
print('工作路径: ' + os.getcwd())
|
||||
announcement = input('请输入公告内容: ')
|
||||
|
||||
# 读取现有的公告文件 res/announcement.json
|
||||
with open('res/announcement.json', 'r', encoding='utf-8') as f:
|
||||
announcement_json = json.load(f)
|
||||
|
||||
# 将公告内容写入公告文件
|
||||
|
||||
# 当前自然时间
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
|
||||
|
||||
# 获取最后一个公告的id
|
||||
last_id = announcement_json[-1]['id'] if len(announcement_json) > 0 else -1
|
||||
|
||||
announcement = {
|
||||
'id': last_id + 1,
|
||||
'time': now,
|
||||
'timestamp': int(time.time()),
|
||||
'content': announcement,
|
||||
}
|
||||
|
||||
announcement_json.append(announcement)
|
||||
|
||||
# 将公告写入公告文件
|
||||
with open('res/announcement.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(announcement_json, f, indent=4, ensure_ascii=False)
|
||||
BIN
res/social.png
Normal file
BIN
res/social.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
31
run_tests.sh
Executable file
31
run_tests.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to run all unit tests
|
||||
# This script helps avoid circular import issues by setting up the environment properly
|
||||
|
||||
set -e
|
||||
|
||||
echo "Setting up test environment..."
|
||||
|
||||
# Activate virtual environment if it exists
|
||||
if [ -d ".venv" ]; then
|
||||
source .venv/bin/activate
|
||||
fi
|
||||
|
||||
# Check if pytest is installed
|
||||
if ! command -v pytest &> /dev/null; then
|
||||
echo "Installing test dependencies..."
|
||||
pip install pytest pytest-asyncio pytest-cov
|
||||
fi
|
||||
|
||||
echo "Running all unit tests..."
|
||||
|
||||
# Run tests with coverage
|
||||
pytest tests/unit_tests/ -v --tb=short \
|
||||
--cov=pkg \
|
||||
--cov-report=xml \
|
||||
"$@"
|
||||
|
||||
echo ""
|
||||
echo "Test run complete!"
|
||||
echo "Coverage report saved to coverage.xml"
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"words": [
|
||||
"习近平",
|
||||
"胡锦涛",
|
||||
"江泽民",
|
||||
"温家宝",
|
||||
"李克强",
|
||||
"李长春",
|
||||
"毛泽东",
|
||||
"邓小平",
|
||||
"周恩来",
|
||||
"社会主义",
|
||||
"共产党",
|
||||
"共产主义",
|
||||
"大陆官方",
|
||||
"北京政权",
|
||||
"中华帝国",
|
||||
"中国政府",
|
||||
"共狗",
|
||||
"六四事件",
|
||||
"天安门",
|
||||
"六四",
|
||||
"政治局常委",
|
||||
"学潮",
|
||||
"八九",
|
||||
"二十大",
|
||||
"民进党",
|
||||
"台独",
|
||||
"台湾独立",
|
||||
"台湾国",
|
||||
"国民党",
|
||||
"台湾民国",
|
||||
"中华民国",
|
||||
"pornhub",
|
||||
"Pornhub",
|
||||
"[Yy]ou[Pp]orn",
|
||||
"porn",
|
||||
"Porn",
|
||||
"[Xx][Vv]ideos",
|
||||
"[Rr]ed[Tt]ube",
|
||||
"[Xx][Hh]amster",
|
||||
"[Ss]pank[Ww]ire",
|
||||
"[Ss]pank[Bb]ang",
|
||||
"[Tt]ube8",
|
||||
"[Yy]ou[Jj]izz",
|
||||
"[Bb]razzers",
|
||||
"[Nn]aughty[ ]?[Aa]merica",
|
||||
"作爱",
|
||||
"做爱",
|
||||
"性交",
|
||||
"自慰",
|
||||
"阴茎",
|
||||
"淫妇",
|
||||
"肛交",
|
||||
"交配",
|
||||
"性关系",
|
||||
"性活动",
|
||||
"色情",
|
||||
"色图",
|
||||
"涩图",
|
||||
"裸体",
|
||||
"小穴",
|
||||
"淫荡",
|
||||
"性爱",
|
||||
"翻墙",
|
||||
"VPN",
|
||||
"科学上网",
|
||||
"挂梯子",
|
||||
"GFW"
|
||||
]
|
||||
}
|
||||
3
src/langbot/__init__.py
Normal file
3
src/langbot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""LangBot - Easy-to-use global IM bot platform designed for LLM era"""
|
||||
|
||||
__version__ = '4.5.4'
|
||||
104
src/langbot/__main__.py
Normal file
104
src/langbot/__main__.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""LangBot entry point for package execution"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
|
||||
# ASCII art banner
|
||||
asciiart = r"""
|
||||
_ ___ _
|
||||
| | __ _ _ _ __ _| _ ) ___| |_
|
||||
| |__/ _` | ' \/ _` | _ \/ _ \ _|
|
||||
|____\__,_|_||_\__, |___/\___/\__|
|
||||
|___/
|
||||
|
||||
⭐️ Open Source 开源地址: https://github.com/langbot-app/LangBot
|
||||
📖 Documentation 文档地址: https://docs.langbot.app
|
||||
"""
|
||||
|
||||
|
||||
async def main_entry(loop: asyncio.AbstractEventLoop):
|
||||
"""Main entry point for LangBot"""
|
||||
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 langbot.pkg.utils import platform
|
||||
|
||||
platform.standalone_runtime = True
|
||||
|
||||
if args.debug:
|
||||
from langbot.pkg.utils import constants
|
||||
|
||||
constants.debug_mode = True
|
||||
|
||||
print(asciiart)
|
||||
|
||||
# Check dependencies
|
||||
from langbot.pkg.core.bootutils import deps
|
||||
|
||||
missing_deps = await deps.check_deps()
|
||||
|
||||
if missing_deps:
|
||||
print('以下依赖包未安装,将自动安装,请完成后重启程序:')
|
||||
print(
|
||||
'These dependencies are missing, they will be installed automatically, please restart the program after completion:'
|
||||
)
|
||||
for dep in missing_deps:
|
||||
print('-', dep)
|
||||
await deps.install_deps(missing_deps)
|
||||
print('已自动安装缺失的依赖包,请重启程序。')
|
||||
print('The missing dependencies have been installed automatically, please restart the program.')
|
||||
sys.exit(0)
|
||||
|
||||
# Check configuration files
|
||||
from langbot.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 langbot.pkg.core import boot
|
||||
|
||||
await boot.main(loop)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to be called by console script entry point"""
|
||||
# Check Python version
|
||||
if sys.version_info < (3, 10, 1):
|
||||
print('需要 Python 3.10.1 及以上版本,当前 Python 版本为:', sys.version)
|
||||
print('Your Python version is not supported.')
|
||||
print('Python 3.10.1 or higher is required. Current version:', sys.version)
|
||||
sys.exit(1)
|
||||
|
||||
# Set up the working directory
|
||||
# When installed as a package, we need to handle the working directory differently
|
||||
# We'll create data directory in current working directory if not exists
|
||||
os.makedirs('data', exist_ok=True)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
|
||||
try:
|
||||
loop.run_until_complete(main_entry(loop))
|
||||
except KeyboardInterrupt:
|
||||
print('\n正在退出...')
|
||||
print('Exiting...')
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
674
src/langbot/libs/LICENSE
Normal file
674
src/langbot/libs/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
4
src/langbot/libs/README.md
Normal file
4
src/langbot/libs/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# LangBot/libs
|
||||
|
||||
LangBot 项目下的 libs 目录下的所有代码均遵循本目录下的许可证约束。
|
||||
您在使用、修改、分发本目录下的代码时,需要遵守其中包含的条款。
|
||||
177
src/langbot/libs/coze_server_api/client.py
Normal file
177
src/langbot/libs/coze_server_api/client.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import io
|
||||
from typing import Dict, List, Any, AsyncGenerator
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AsyncCozeAPIClient:
|
||||
def __init__(self, api_key: str, api_base: str = 'https://api.coze.cn'):
|
||||
self.api_key = api_key
|
||||
self.api_base = api_base
|
||||
self.session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
"""支持异步上下文管理器"""
|
||||
await self.coze_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""退出时自动关闭会话"""
|
||||
await self.close()
|
||||
|
||||
async def coze_session(self):
|
||||
"""确保HTTP session存在"""
|
||||
if self.session is None:
|
||||
connector = aiohttp.TCPConnector(
|
||||
ssl=False if self.api_base.startswith('http://') else True,
|
||||
limit=100,
|
||||
limit_per_host=30,
|
||||
keepalive_timeout=30,
|
||||
enable_cleanup_closed=True,
|
||||
)
|
||||
timeout = aiohttp.ClientTimeout(
|
||||
total=120, # 默认超时时间
|
||||
connect=30,
|
||||
sock_read=120,
|
||||
)
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Accept': 'text/event-stream',
|
||||
}
|
||||
self.session = aiohttp.ClientSession(headers=headers, timeout=timeout, connector=connector)
|
||||
return self.session
|
||||
|
||||
async def close(self):
|
||||
"""显式关闭会话"""
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
|
||||
async def upload(
|
||||
self,
|
||||
file,
|
||||
) -> str:
|
||||
# 处理 Path 对象
|
||||
if isinstance(file, Path):
|
||||
if not file.exists():
|
||||
raise ValueError(f'File not found: {file}')
|
||||
with open(file, 'rb') as f:
|
||||
file = f.read()
|
||||
|
||||
# 处理文件路径字符串
|
||||
elif isinstance(file, str):
|
||||
if not os.path.isfile(file):
|
||||
raise ValueError(f'File not found: {file}')
|
||||
with open(file, 'rb') as f:
|
||||
file = f.read()
|
||||
|
||||
# 处理文件对象
|
||||
elif hasattr(file, 'read'):
|
||||
file = file.read()
|
||||
|
||||
session = await self.coze_session()
|
||||
url = f'{self.api_base}/v1/files/upload'
|
||||
|
||||
try:
|
||||
file_io = io.BytesIO(file)
|
||||
async with session.post(
|
||||
url,
|
||||
data={
|
||||
'file': file_io,
|
||||
},
|
||||
timeout=aiohttp.ClientTimeout(total=60),
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
raise Exception('Coze API 认证失败,请检查 API Key 是否正确')
|
||||
|
||||
response_text = await response.text()
|
||||
|
||||
if response.status != 200:
|
||||
raise Exception(f'文件上传失败,状态码: {response.status}, 响应: {response_text}')
|
||||
try:
|
||||
result = await response.json()
|
||||
except json.JSONDecodeError:
|
||||
raise Exception(f'文件上传响应解析失败: {response_text}')
|
||||
|
||||
if result.get('code') != 0:
|
||||
raise Exception(f'文件上传失败: {result.get("msg", "未知错误")}')
|
||||
|
||||
file_id = result['data']['id']
|
||||
return file_id
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception('文件上传超时')
|
||||
except Exception as e:
|
||||
raise Exception(f'文件上传失败: {str(e)}')
|
||||
|
||||
async def chat_messages(
|
||||
self,
|
||||
bot_id: str,
|
||||
user_id: str,
|
||||
additional_messages: List[Dict] | None = None,
|
||||
conversation_id: str | None = None,
|
||||
auto_save_history: bool = True,
|
||||
stream: bool = True,
|
||||
timeout: float = 120,
|
||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""发送聊天消息并返回流式响应
|
||||
|
||||
Args:
|
||||
bot_id: Bot ID
|
||||
user_id: 用户ID
|
||||
additional_messages: 额外消息列表
|
||||
conversation_id: 会话ID
|
||||
auto_save_history: 是否自动保存历史
|
||||
stream: 是否流式响应
|
||||
timeout: 超时时间
|
||||
"""
|
||||
session = await self.coze_session()
|
||||
url = f'{self.api_base}/v3/chat'
|
||||
|
||||
payload = {
|
||||
'bot_id': bot_id,
|
||||
'user_id': user_id,
|
||||
'stream': stream,
|
||||
'auto_save_history': auto_save_history,
|
||||
}
|
||||
|
||||
if additional_messages:
|
||||
payload['additional_messages'] = additional_messages
|
||||
|
||||
params = {}
|
||||
if conversation_id:
|
||||
params['conversation_id'] = conversation_id
|
||||
|
||||
try:
|
||||
async with session.post(
|
||||
url,
|
||||
json=payload,
|
||||
params=params,
|
||||
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||
) as response:
|
||||
if response.status == 401:
|
||||
raise Exception('Coze API 认证失败,请检查 API Key 是否正确')
|
||||
|
||||
if response.status != 200:
|
||||
raise Exception(f'Coze API 流式请求失败,状态码: {response.status}')
|
||||
|
||||
async for chunk in response.content:
|
||||
chunk = chunk.decode('utf-8')
|
||||
if chunk != '\n':
|
||||
if chunk.startswith('event:'):
|
||||
chunk_type = chunk.replace('event:', '', 1).strip()
|
||||
elif chunk.startswith('data:'):
|
||||
chunk_data = chunk.replace('data:', '', 1).strip()
|
||||
else:
|
||||
yield {
|
||||
'event': chunk_type,
|
||||
'data': json.loads(chunk_data) if chunk_data else {},
|
||||
} # 处理本地部署时,接口返回的data为空值
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise Exception(f'Coze API 流式请求超时 ({timeout}秒)')
|
||||
except Exception as e:
|
||||
raise Exception(f'Coze API 流式请求失败: {str(e)}')
|
||||
3
src/langbot/libs/dify_service_api/README.md
Normal file
3
src/langbot/libs/dify_service_api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dify Service API Python SDK
|
||||
|
||||
这个 SDK 尚不完全支持 Dify Service API 的所有功能。
|
||||
4
src/langbot/libs/dify_service_api/__init__.py
Normal file
4
src/langbot/libs/dify_service_api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .v1 import client as client
|
||||
from .v1 import errors as errors
|
||||
|
||||
__all__ = ['client', 'errors']
|
||||
155
src/langbot/libs/dify_service_api/v1/client.py
Normal file
155
src/langbot/libs/dify_service_api/v1/client.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import typing
|
||||
import json
|
||||
|
||||
from .errors import DifyAPIError
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
|
||||
class AsyncDifyServiceClient:
|
||||
"""Dify Service API 客户端"""
|
||||
|
||||
api_key: str
|
||||
base_url: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
base_url: str = 'https://api.dify.ai/v1',
|
||||
) -> None:
|
||||
self.api_key = api_key
|
||||
self.base_url = base_url
|
||||
|
||||
async def chat_messages(
|
||||
self,
|
||||
inputs: dict[str, typing.Any],
|
||||
query: str,
|
||||
user: str,
|
||||
response_mode: str = 'streaming', # 当前不支持 blocking
|
||||
conversation_id: str = '',
|
||||
files: list[dict[str, typing.Any]] = [],
|
||||
timeout: float = 30.0,
|
||||
model_config: dict[str, typing.Any] | None = None,
|
||||
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
|
||||
"""发送消息"""
|
||||
if response_mode != 'streaming':
|
||||
raise DifyAPIError('当前仅支持 streaming 模式')
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
trust_env=True,
|
||||
timeout=timeout,
|
||||
) as client:
|
||||
payload = {
|
||||
'inputs': inputs,
|
||||
'query': query,
|
||||
'user': user,
|
||||
'response_mode': response_mode,
|
||||
'conversation_id': conversation_id,
|
||||
'files': files,
|
||||
'model_config': model_config or {},
|
||||
}
|
||||
|
||||
async with client.stream(
|
||||
'POST',
|
||||
'/chat-messages',
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json=payload,
|
||||
) as r:
|
||||
async for chunk in r.aiter_lines():
|
||||
if r.status_code != 200:
|
||||
raise DifyAPIError(f'{r.status_code} {chunk}')
|
||||
if chunk.strip() == '':
|
||||
continue
|
||||
if chunk.startswith('data:'):
|
||||
yield json.loads(chunk[5:])
|
||||
|
||||
async def workflow_run(
|
||||
self,
|
||||
inputs: dict[str, typing.Any],
|
||||
user: str,
|
||||
response_mode: str = 'streaming', # 当前不支持 blocking
|
||||
files: list[dict[str, typing.Any]] = [],
|
||||
timeout: float = 30.0,
|
||||
) -> typing.AsyncGenerator[dict[str, typing.Any], None]:
|
||||
"""运行工作流"""
|
||||
if response_mode != 'streaming':
|
||||
raise DifyAPIError('当前仅支持 streaming 模式')
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
trust_env=True,
|
||||
timeout=timeout,
|
||||
) as client:
|
||||
async with client.stream(
|
||||
'POST',
|
||||
'/workflows/run',
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json={
|
||||
'inputs': inputs,
|
||||
'user': user,
|
||||
'response_mode': response_mode,
|
||||
'files': files,
|
||||
},
|
||||
) as r:
|
||||
async for chunk in r.aiter_lines():
|
||||
if r.status_code != 200:
|
||||
raise DifyAPIError(f'{r.status_code} {chunk}')
|
||||
if chunk.strip() == '':
|
||||
continue
|
||||
if chunk.startswith('data:'):
|
||||
yield json.loads(chunk[5:])
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
file: httpx._types.FileTypes,
|
||||
user: str,
|
||||
timeout: float = 30.0,
|
||||
) -> str:
|
||||
# 处理 Path 对象
|
||||
if isinstance(file, Path):
|
||||
if not file.exists():
|
||||
raise ValueError(f'File not found: {file}')
|
||||
with open(file, 'rb') as f:
|
||||
file = f.read()
|
||||
|
||||
# 处理文件路径字符串
|
||||
elif isinstance(file, str):
|
||||
if not os.path.isfile(file):
|
||||
raise ValueError(f'File not found: {file}')
|
||||
with open(file, 'rb') as f:
|
||||
file = f.read()
|
||||
|
||||
# 处理文件对象
|
||||
elif hasattr(file, 'read'):
|
||||
file = file.read()
|
||||
async with httpx.AsyncClient(
|
||||
base_url=self.base_url,
|
||||
trust_env=True,
|
||||
timeout=timeout,
|
||||
) as client:
|
||||
# multipart/form-data
|
||||
response = await client.post(
|
||||
'/files/upload',
|
||||
headers={'Authorization': f'Bearer {self.api_key}'},
|
||||
files={
|
||||
'file': file,
|
||||
},
|
||||
data={
|
||||
'user': (None, user),
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
raise DifyAPIError(f'{response.status_code} {response.text}')
|
||||
|
||||
return response.json()
|
||||
17
src/langbot/libs/dify_service_api/v1/client_test.py
Normal file
17
src/langbot/libs/dify_service_api/v1/client_test.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from . import client
|
||||
|
||||
import asyncio
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class TestDifyClient:
|
||||
async def test_chat_messages(self):
|
||||
cln = client.DifyClient(api_key=os.getenv('DIFY_API_KEY'))
|
||||
|
||||
resp = await cln.chat_messages(inputs={}, query='Who are you?', user_id='test')
|
||||
print(resp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(TestDifyClient().test_chat_messages())
|
||||
6
src/langbot/libs/dify_service_api/v1/errors.py
Normal file
6
src/langbot/libs/dify_service_api/v1/errors.py
Normal file
@@ -0,0 +1,6 @@
|
||||
class DifyAPIError(Exception):
|
||||
"""Dify API 请求失败"""
|
||||
|
||||
def __init__(self, message: str):
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
29
src/langbot/libs/dingtalk_api/EchoHandler.py
Normal file
29
src/langbot/libs/dingtalk_api/EchoHandler.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import dingtalk_stream # type: ignore
|
||||
from dingtalk_stream import AckMessage
|
||||
|
||||
|
||||
class EchoTextHandler(dingtalk_stream.ChatbotHandler):
|
||||
def __init__(self, client):
|
||||
super().__init__() # Call parent class initializer to set up logger
|
||||
self.msg_id = ''
|
||||
self.incoming_message = None
|
||||
self.client = client # 用于更新 DingTalkClient 中的 incoming_message
|
||||
|
||||
"""处理钉钉消息"""
|
||||
|
||||
async def process(self, callback: dingtalk_stream.CallbackMessage):
|
||||
incoming_message = dingtalk_stream.ChatbotMessage.from_dict(callback.data)
|
||||
if incoming_message.message_id != self.msg_id:
|
||||
self.msg_id = incoming_message.message_id
|
||||
|
||||
await self.client.update_incoming_message(incoming_message)
|
||||
|
||||
return AckMessage.STATUS_OK, 'OK'
|
||||
|
||||
async def get_incoming_message(self):
|
||||
"""异步等待消息的到来"""
|
||||
while self.incoming_message is None:
|
||||
await asyncio.sleep(0.1) # 异步等待,避免阻塞
|
||||
|
||||
return self.incoming_message
|
||||
381
src/langbot/libs/dingtalk_api/api.py
Normal file
381
src/langbot/libs/dingtalk_api/api.py
Normal file
@@ -0,0 +1,381 @@
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from typing import Callable
|
||||
import dingtalk_stream # type: ignore
|
||||
from .EchoHandler import EchoTextHandler
|
||||
from .dingtalkevent import DingTalkEvent
|
||||
import httpx
|
||||
import traceback
|
||||
|
||||
|
||||
class DingTalkClient:
|
||||
def __init__(
|
||||
self,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
robot_name: str,
|
||||
robot_code: str,
|
||||
markdown_card: bool,
|
||||
logger: None,
|
||||
):
|
||||
"""初始化 WebSocket 连接并自动启动"""
|
||||
self.credential = dingtalk_stream.Credential(client_id, client_secret)
|
||||
self.client = dingtalk_stream.DingTalkStreamClient(self.credential)
|
||||
self.key = client_id
|
||||
self.secret = client_secret
|
||||
# 在 DingTalkClient 中传入自己作为参数,避免循环导入
|
||||
self.EchoTextHandler = EchoTextHandler(self)
|
||||
self.client.register_callback_handler(dingtalk_stream.chatbot.ChatbotMessage.TOPIC, self.EchoTextHandler)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
self.access_token = ''
|
||||
self.robot_name = robot_name
|
||||
self.robot_code = robot_code
|
||||
self.access_token_expiry_time = ''
|
||||
self.markdown_card = markdown_card
|
||||
self.logger = logger
|
||||
|
||||
async def get_access_token(self):
|
||||
url = 'https://api.dingtalk.com/v1.0/oauth2/accessToken'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
data = {'appKey': self.key, 'appSecret': self.secret}
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.post(url, json=data, headers=headers)
|
||||
if response.status_code == 200:
|
||||
response_data = response.json()
|
||||
self.access_token = response_data.get('accessToken')
|
||||
expires_in = int(response_data.get('expireIn', 7200))
|
||||
self.access_token_expiry_time = time.time() + expires_in - 60
|
||||
except Exception:
|
||||
await self.logger.error('failed to get access token in dingtalk')
|
||||
|
||||
async def is_token_expired(self):
|
||||
"""检查token是否过期"""
|
||||
if self.access_token_expiry_time is None:
|
||||
return True
|
||||
return time.time() > self.access_token_expiry_time
|
||||
|
||||
async def check_access_token(self):
|
||||
if not self.access_token or await self.is_token_expired():
|
||||
return False
|
||||
return bool(self.access_token and self.access_token.strip())
|
||||
|
||||
async def download_image(self, download_code: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get('downloadUrl')
|
||||
else:
|
||||
await self.logger.error(f'failed to get download url: {response.json()}')
|
||||
|
||||
if download_url:
|
||||
return await self.download_url_to_base64(download_url)
|
||||
|
||||
async def download_url_to_base64(self, download_url):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(download_url)
|
||||
|
||||
if response.status_code == 200:
|
||||
file_bytes = response.content
|
||||
mime_type = response.headers.get('Content-Type', 'application/octet-stream')
|
||||
base64_str = base64.b64encode(file_bytes).decode('utf-8')
|
||||
return f'data:{mime_type};base64,{base64_str}'
|
||||
else:
|
||||
await self.logger.error(f'failed to get files: {response.json()}')
|
||||
|
||||
async def get_audio_url(self, download_code: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get('downloadUrl')
|
||||
if download_url:
|
||||
return await self.download_url_to_base64(download_url)
|
||||
else:
|
||||
await self.logger.error(f'failed to get audio: {response.json()}')
|
||||
else:
|
||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||
|
||||
async def get_file_url(self, download_code: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/messageFiles/download'
|
||||
params = {'downloadCode': download_code, 'robotCode': self.robot_code}
|
||||
headers = {'x-acs-dingtalk-access-token': self.access_token}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
download_url = result.get('downloadUrl')
|
||||
if download_url:
|
||||
return download_url
|
||||
else:
|
||||
await self.logger.error(f'failed to get file: {response.json()}')
|
||||
else:
|
||||
raise Exception(f'Error: {response.status_code}, {response.text}')
|
||||
|
||||
async def update_incoming_message(self, message):
|
||||
"""异步更新 DingTalkClient 中的 incoming_message"""
|
||||
message_data = await self.get_message(message)
|
||||
if message_data:
|
||||
event = DingTalkEvent.from_payload(message_data)
|
||||
if event:
|
||||
await self._handle_message(event)
|
||||
|
||||
async def send_message(self, content: str, incoming_message, at: bool):
|
||||
if self.markdown_card:
|
||||
if at:
|
||||
self.EchoTextHandler.reply_markdown(
|
||||
title='@' + incoming_message.sender_nick + ' ' + content,
|
||||
text='@' + incoming_message.sender_nick + ' ' + content,
|
||||
incoming_message=incoming_message,
|
||||
)
|
||||
else:
|
||||
self.EchoTextHandler.reply_markdown(
|
||||
title=content,
|
||||
text=content,
|
||||
incoming_message=incoming_message,
|
||||
)
|
||||
else:
|
||||
self.EchoTextHandler.reply_text(content, incoming_message)
|
||||
|
||||
async def get_incoming_message(self):
|
||||
"""获取收到的消息"""
|
||||
return await self.EchoTextHandler.get_incoming_message()
|
||||
|
||||
def on_message(self, msg_type: str):
|
||||
def decorator(func: Callable[[DingTalkEvent], None]):
|
||||
if msg_type not in self._message_handlers:
|
||||
self._message_handlers[msg_type] = []
|
||||
self._message_handlers[msg_type].append(func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
async def _handle_message(self, event: DingTalkEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
msg_type = event.conversation
|
||||
if msg_type in self._message_handlers:
|
||||
for handler in self._message_handlers[msg_type]:
|
||||
await handler(event)
|
||||
|
||||
async def get_message(self, incoming_message: dingtalk_stream.chatbot.ChatbotMessage):
|
||||
try:
|
||||
# print(json.dumps(incoming_message.to_dict(), indent=4, ensure_ascii=False))
|
||||
message_data = {
|
||||
'IncomingMessage': incoming_message,
|
||||
}
|
||||
if str(incoming_message.conversation_type) == '1':
|
||||
message_data['conversation_type'] = 'FriendMessage'
|
||||
elif str(incoming_message.conversation_type) == '2':
|
||||
message_data['conversation_type'] = 'GroupMessage'
|
||||
|
||||
if incoming_message.message_type == 'richText':
|
||||
data = incoming_message.rich_text_content.to_dict()
|
||||
|
||||
# 使用统一的结构化数据格式,保持顺序
|
||||
rich_content = {
|
||||
'Type': 'richText',
|
||||
'Elements': [], # 按顺序存储所有元素
|
||||
'SimpleContent': '', # 兼容字段:纯文本内容
|
||||
'SimplePicture': '', # 兼容字段:第一张图片
|
||||
}
|
||||
|
||||
# 先收集所有文本和图片占位符
|
||||
text_elements = []
|
||||
|
||||
# 解析富文本内容,保持原始顺序
|
||||
for item in data['richText']:
|
||||
# 处理文本内容
|
||||
if 'text' in item and item['text'] != '\n':
|
||||
element = {'Type': 'text', 'Content': item['text']}
|
||||
rich_content['Elements'].append(element)
|
||||
text_elements.append(item['text'])
|
||||
|
||||
# 检查是否是图片元素 - 根据钉钉API的实际结构调整
|
||||
# 钉钉富文本中的图片通常有特定标识,可能需要根据实际返回调整
|
||||
elif item.get('type') == 'picture':
|
||||
# 创建图片占位符
|
||||
element = {
|
||||
'Type': 'image_placeholder',
|
||||
}
|
||||
rich_content['Elements'].append(element)
|
||||
|
||||
# 获取并下载所有图片
|
||||
image_list = incoming_message.get_image_list()
|
||||
if image_list:
|
||||
new_elements = []
|
||||
image_index = 0
|
||||
|
||||
for element in rich_content['Elements']:
|
||||
if element['Type'] == 'image_placeholder':
|
||||
if image_index < len(image_list) and image_list[image_index]:
|
||||
image_url = await self.download_image(image_list[image_index])
|
||||
new_elements.append({'Type': 'image', 'Picture': image_url})
|
||||
image_index += 1
|
||||
else:
|
||||
# 如果没有对应的图片,保留占位符或跳过
|
||||
continue
|
||||
else:
|
||||
new_elements.append(element)
|
||||
|
||||
rich_content['Elements'] = new_elements
|
||||
|
||||
# 设置兼容字段
|
||||
all_texts = [elem['Content'] for elem in rich_content['Elements'] if elem.get('Type') == 'text']
|
||||
rich_content['SimpleContent'] = '\n'.join(all_texts) if all_texts else ''
|
||||
|
||||
all_images = [elem['Picture'] for elem in rich_content['Elements'] if elem.get('Type') == 'image']
|
||||
if all_images:
|
||||
rich_content['SimplePicture'] = all_images[0]
|
||||
rich_content['AllImages'] = all_images # 所有图片的列表
|
||||
|
||||
# 设置原始的 content 和 picture 字段以保持兼容
|
||||
message_data['Content'] = rich_content['SimpleContent']
|
||||
message_data['Rich_Content'] = rich_content
|
||||
if all_images:
|
||||
message_data['Picture'] = all_images[0]
|
||||
|
||||
elif incoming_message.message_type == 'text':
|
||||
message_data['Content'] = incoming_message.get_text_list()[0]
|
||||
|
||||
message_data['Type'] = 'text'
|
||||
elif incoming_message.message_type == 'picture':
|
||||
message_data['Picture'] = await self.download_image(incoming_message.get_image_list()[0])
|
||||
|
||||
message_data['Type'] = 'image'
|
||||
elif incoming_message.message_type == 'audio':
|
||||
message_data['Audio'] = await self.get_audio_url(incoming_message.to_dict()['content']['downloadCode'])
|
||||
|
||||
message_data['Type'] = 'audio'
|
||||
elif incoming_message.message_type == 'file':
|
||||
down_list = incoming_message.get_down_list()
|
||||
if len(down_list) >= 2:
|
||||
message_data['File'] = await self.get_file_url(down_list[0])
|
||||
message_data['Name'] = down_list[1]
|
||||
else:
|
||||
if self.logger:
|
||||
await self.logger.error(f'get_down_list() returned fewer than 2 elements: {down_list}')
|
||||
message_data['File'] = None
|
||||
message_data['Name'] = None
|
||||
message_data['Type'] = 'file'
|
||||
|
||||
copy_message_data = message_data.copy()
|
||||
del copy_message_data['IncomingMessage']
|
||||
# print("message_data:", json.dumps(copy_message_data, indent=4, ensure_ascii=False))
|
||||
except Exception:
|
||||
if self.logger:
|
||||
await self.logger.error(f'Error in get_message: {traceback.format_exc()}')
|
||||
else:
|
||||
traceback.print_exc()
|
||||
|
||||
return message_data
|
||||
|
||||
async def send_proactive_message_to_one(self, target_id: str, content: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend'
|
||||
|
||||
headers = {
|
||||
'x-acs-dingtalk-access-token': self.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
data = {
|
||||
'robotCode': self.robot_code,
|
||||
'userIds': [target_id],
|
||||
'msgKey': 'sampleText',
|
||||
'msgParam': json.dumps({'content': content}),
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=data)
|
||||
if response.status_code == 200:
|
||||
return
|
||||
except Exception:
|
||||
await self.logger.error(f'failed to send proactive massage to person: {traceback.format_exc()}')
|
||||
raise Exception(f'failed to send proactive massage to person: {traceback.format_exc()}')
|
||||
|
||||
async def send_proactive_message_to_group(self, target_id: str, content: str):
|
||||
if not await self.check_access_token():
|
||||
await self.get_access_token()
|
||||
|
||||
url = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send'
|
||||
|
||||
headers = {
|
||||
'x-acs-dingtalk-access-token': self.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
data = {
|
||||
'robotCode': self.robot_code,
|
||||
'openConversationId': target_id,
|
||||
'msgKey': 'sampleText',
|
||||
'msgParam': json.dumps({'content': content}),
|
||||
}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, headers=headers, json=data)
|
||||
if response.status_code == 200:
|
||||
return
|
||||
except Exception:
|
||||
await self.logger.error(f'failed to send proactive massage to group: {traceback.format_exc()}')
|
||||
raise Exception(f'failed to send proactive massage to group: {traceback.format_exc()}')
|
||||
|
||||
async def create_and_card(
|
||||
self, temp_card_id: str, incoming_message: dingtalk_stream.ChatbotMessage, quote_origin: bool = False
|
||||
):
|
||||
content_key = 'content'
|
||||
card_data = {content_key: ''}
|
||||
|
||||
card_instance = dingtalk_stream.AICardReplier(self.client, incoming_message)
|
||||
# print(card_instance)
|
||||
# 先投放卡片: https://open.dingtalk.com/document/orgapp/create-and-deliver-cards
|
||||
card_instance_id = await card_instance.async_create_and_deliver_card(
|
||||
temp_card_id,
|
||||
card_data,
|
||||
)
|
||||
return card_instance, card_instance_id
|
||||
|
||||
async def send_card_message(self, card_instance, card_instance_id: str, content: str, is_final: bool):
|
||||
content_key = 'content'
|
||||
try:
|
||||
await card_instance.async_streaming(
|
||||
card_instance_id,
|
||||
content_key=content_key,
|
||||
content_value=content,
|
||||
append=False,
|
||||
finished=is_final,
|
||||
failed=False,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
await card_instance.async_streaming(
|
||||
card_instance_id,
|
||||
content_key=content_key,
|
||||
content_value='',
|
||||
append=False,
|
||||
finished=is_final,
|
||||
failed=True,
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
"""启动 WebSocket 连接,监听消息"""
|
||||
await self.client.start()
|
||||
79
src/langbot/libs/dingtalk_api/dingtalkevent.py
Normal file
79
src/langbot/libs/dingtalk_api/dingtalkevent.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import Dict, Any, Optional
|
||||
import dingtalk_stream # type: ignore
|
||||
|
||||
|
||||
class DingTalkEvent(dict):
|
||||
@staticmethod
|
||||
def from_payload(payload: Dict[str, Any]) -> Optional['DingTalkEvent']:
|
||||
try:
|
||||
event = DingTalkEvent(payload)
|
||||
return event
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return self.get('Content', '')
|
||||
|
||||
@property
|
||||
def rich_content(self):
|
||||
return self.get('Rich_Content', '')
|
||||
|
||||
@property
|
||||
def incoming_message(self) -> Optional['dingtalk_stream.chatbot.ChatbotMessage']:
|
||||
return self.get('IncomingMessage')
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.get('Type', '')
|
||||
|
||||
@property
|
||||
def picture(self):
|
||||
return self.get('Picture', '')
|
||||
|
||||
@property
|
||||
def audio(self):
|
||||
return self.get('Audio', '')
|
||||
|
||||
@property
|
||||
def file(self):
|
||||
return self.get('File', '')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get('Name', '')
|
||||
|
||||
@property
|
||||
def conversation(self):
|
||||
return self.get('conversation_type', '')
|
||||
|
||||
def __getattr__(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
允许通过属性访问数据中的任意字段。
|
||||
|
||||
Args:
|
||||
key (str): 字段名。
|
||||
|
||||
Returns:
|
||||
Optional[Any]: 字段值。
|
||||
"""
|
||||
return self.get(key)
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
允许通过属性设置数据中的任意字段。
|
||||
|
||||
Args:
|
||||
key (str): 字段名。
|
||||
value (Any): 字段值。
|
||||
"""
|
||||
self[key] = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
生成事件对象的字符串表示。
|
||||
|
||||
Returns:
|
||||
str: 字符串表示。
|
||||
"""
|
||||
return f'<DingTalkEvent {super().__repr__()}>'
|
||||
337
src/langbot/libs/official_account_api/api.py
Normal file
337
src/langbot/libs/official_account_api/api.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# 微信公众号的加解密算法与企业微信一样,所以直接使用企业微信的加解密算法文件
|
||||
import time
|
||||
import traceback
|
||||
from langbot.libs.wecom_api.WXBizMsgCrypt3 import WXBizMsgCrypt
|
||||
import xml.etree.ElementTree as ET
|
||||
from quart import Quart, request
|
||||
import hashlib
|
||||
from typing import Callable
|
||||
from langbot.libs.official_account_api.oaevent import OAEvent
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
xml_template = """
|
||||
<xml>
|
||||
<ToUserName><![CDATA[{to_user}]]></ToUserName>
|
||||
<FromUserName><![CDATA[{from_user}]]></FromUserName>
|
||||
<CreateTime>{create_time}</CreateTime>
|
||||
<MsgType><![CDATA[text]]></MsgType>
|
||||
<Content><![CDATA[{content}]]></Content>
|
||||
</xml>
|
||||
"""
|
||||
|
||||
|
||||
class OAClient:
|
||||
def __init__(self, token: str, EncodingAESKey: str, AppID: str, Appsecret: str, logger: None):
|
||||
self.token = token
|
||||
self.aes = EncodingAESKey
|
||||
self.appid = AppID
|
||||
self.appsecret = Appsecret
|
||||
self.base_url = 'https://api.weixin.qq.com'
|
||||
self.access_token = ''
|
||||
self.app = Quart(__name__)
|
||||
self.app.add_url_rule(
|
||||
'/callback/command',
|
||||
'handle_callback',
|
||||
self.handle_callback_request,
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
self.access_token_expiry_time = None
|
||||
self.msg_id_map = {}
|
||||
self.generated_content = {}
|
||||
self.logger = logger
|
||||
|
||||
async def handle_callback_request(self):
|
||||
try:
|
||||
# 每隔100毫秒查询是否生成ai回答
|
||||
start_time = time.time()
|
||||
signature = request.args.get('signature', '')
|
||||
timestamp = request.args.get('timestamp', '')
|
||||
nonce = request.args.get('nonce', '')
|
||||
echostr = request.args.get('echostr', '')
|
||||
msg_signature = request.args.get('msg_signature', '')
|
||||
if msg_signature is None:
|
||||
await self.logger.error('msg_signature不在请求体中')
|
||||
raise Exception('msg_signature不在请求体中')
|
||||
|
||||
if request.method == 'GET':
|
||||
# 校验签名
|
||||
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
||||
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
||||
|
||||
if check_signature == signature:
|
||||
return echostr # 验证成功返回echostr
|
||||
else:
|
||||
await self.logger.error('拒绝请求')
|
||||
raise Exception('拒绝请求')
|
||||
elif request.method == 'POST':
|
||||
encryt_msg = await request.data
|
||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
||||
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
||||
xml_msg = xml_msg.decode('utf-8')
|
||||
|
||||
if ret != 0:
|
||||
await self.logger.error('消息解密失败')
|
||||
raise Exception('消息解密失败')
|
||||
|
||||
message_data = await self.get_message(xml_msg)
|
||||
if message_data:
|
||||
event = OAEvent.from_payload(message_data)
|
||||
if event:
|
||||
await self._handle_message(event)
|
||||
|
||||
root = ET.fromstring(xml_msg)
|
||||
from_user = root.find('FromUserName').text # 发送者
|
||||
to_user = root.find('ToUserName').text # 机器人
|
||||
|
||||
timeout = 4.80
|
||||
interval = 0.1
|
||||
while True:
|
||||
content = self.generated_content.pop(message_data['MsgId'], None)
|
||||
if content:
|
||||
response_xml = xml_template.format(
|
||||
to_user=from_user,
|
||||
from_user=to_user,
|
||||
create_time=int(time.time()),
|
||||
content=content,
|
||||
)
|
||||
|
||||
return response_xml
|
||||
|
||||
if time.time() - start_time >= timeout:
|
||||
break
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
if self.msg_id_map.get(message_data['MsgId'], 1) == 3:
|
||||
# response_xml = xml_template.format(
|
||||
# to_user=from_user,
|
||||
# from_user=to_user,
|
||||
# create_time=int(time.time()),
|
||||
# content = "请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。"
|
||||
# )
|
||||
print('请求失效:暂不支持公众号超过15秒的请求,如有需求,请联系 LangBot 团队。')
|
||||
return ''
|
||||
|
||||
except Exception:
|
||||
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
async def get_message(self, xml_msg: str):
|
||||
root = ET.fromstring(xml_msg)
|
||||
|
||||
message_data = {
|
||||
'ToUserName': root.find('ToUserName').text,
|
||||
'FromUserName': root.find('FromUserName').text,
|
||||
'CreateTime': int(root.find('CreateTime').text),
|
||||
'MsgType': root.find('MsgType').text,
|
||||
'Content': root.find('Content').text if root.find('Content') is not None else None,
|
||||
'MsgId': int(root.find('MsgId').text) if root.find('MsgId') is not None else None,
|
||||
}
|
||||
|
||||
return message_data
|
||||
|
||||
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[[OAEvent], 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: OAEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
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)
|
||||
|
||||
async def set_message(self, msg_id: int, content: str):
|
||||
self.generated_content[msg_id] = content
|
||||
|
||||
|
||||
class OAClientForLongerResponse:
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
EncodingAESKey: str,
|
||||
AppID: str,
|
||||
Appsecret: str,
|
||||
LoadingMessage: str,
|
||||
logger: None,
|
||||
):
|
||||
self.token = token
|
||||
self.aes = EncodingAESKey
|
||||
self.appid = AppID
|
||||
self.appsecret = Appsecret
|
||||
self.base_url = 'https://api.weixin.qq.com'
|
||||
self.access_token = ''
|
||||
self.app = Quart(__name__)
|
||||
self.app.add_url_rule(
|
||||
'/callback/command',
|
||||
'handle_callback',
|
||||
self.handle_callback_request,
|
||||
methods=['GET', 'POST'],
|
||||
)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
self.access_token_expiry_time = None
|
||||
self.loading_message = LoadingMessage
|
||||
self.msg_queue = {}
|
||||
self.user_msg_queue = {}
|
||||
self.logger = logger
|
||||
|
||||
async def handle_callback_request(self):
|
||||
try:
|
||||
signature = request.args.get('signature', '')
|
||||
timestamp = request.args.get('timestamp', '')
|
||||
nonce = request.args.get('nonce', '')
|
||||
echostr = request.args.get('echostr', '')
|
||||
msg_signature = request.args.get('msg_signature', '')
|
||||
|
||||
if msg_signature is None:
|
||||
await self.logger.error('msg_signature不在请求体中')
|
||||
raise Exception('msg_signature不在请求体中')
|
||||
|
||||
if request.method == 'GET':
|
||||
check_str = ''.join(sorted([self.token, timestamp, nonce]))
|
||||
check_signature = hashlib.sha1(check_str.encode('utf-8')).hexdigest()
|
||||
return echostr if check_signature == signature else '拒绝请求'
|
||||
|
||||
elif request.method == 'POST':
|
||||
encryt_msg = await request.data
|
||||
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.appid)
|
||||
ret, xml_msg = wxcpt.DecryptMsg(encryt_msg, msg_signature, timestamp, nonce)
|
||||
xml_msg = xml_msg.decode('utf-8')
|
||||
|
||||
if ret != 0:
|
||||
await self.logger.error('消息解密失败')
|
||||
raise Exception('消息解密失败')
|
||||
|
||||
# 解析 XML
|
||||
root = ET.fromstring(xml_msg)
|
||||
from_user = root.find('FromUserName').text
|
||||
to_user = root.find('ToUserName').text
|
||||
|
||||
if self.msg_queue.get(from_user) and self.msg_queue[from_user][0]['content']:
|
||||
queue_top = self.msg_queue[from_user].pop(0)
|
||||
queue_content = queue_top['content']
|
||||
|
||||
# 弹出用户消息
|
||||
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user]:
|
||||
self.user_msg_queue[from_user].pop(0)
|
||||
|
||||
response_xml = xml_template.format(
|
||||
to_user=from_user,
|
||||
from_user=to_user,
|
||||
create_time=int(time.time()),
|
||||
content=queue_content,
|
||||
)
|
||||
return response_xml
|
||||
|
||||
else:
|
||||
response_xml = xml_template.format(
|
||||
to_user=from_user,
|
||||
from_user=to_user,
|
||||
create_time=int(time.time()),
|
||||
content=self.loading_message,
|
||||
)
|
||||
|
||||
if self.user_msg_queue.get(from_user) and self.user_msg_queue[from_user][0]['content']:
|
||||
return response_xml
|
||||
else:
|
||||
message_data = await self.get_message(xml_msg)
|
||||
|
||||
if message_data:
|
||||
event = OAEvent.from_payload(message_data)
|
||||
if event:
|
||||
self.user_msg_queue.setdefault(from_user, []).append(
|
||||
{
|
||||
'content': event.message,
|
||||
}
|
||||
)
|
||||
await self._handle_message(event)
|
||||
|
||||
return response_xml
|
||||
|
||||
except Exception:
|
||||
await self.logger.error(f'handle_callback_request失败: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
async def get_message(self, xml_msg: str):
|
||||
root = ET.fromstring(xml_msg)
|
||||
|
||||
message_data = {
|
||||
'ToUserName': root.find('ToUserName').text,
|
||||
'FromUserName': root.find('FromUserName').text,
|
||||
'CreateTime': int(root.find('CreateTime').text),
|
||||
'MsgType': root.find('MsgType').text,
|
||||
'Content': root.find('Content').text if root.find('Content') is not None else None,
|
||||
'MsgId': int(root.find('MsgId').text) if root.find('MsgId') is not None else None,
|
||||
}
|
||||
|
||||
return message_data
|
||||
|
||||
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[[OAEvent], 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: OAEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
|
||||
msg_type = event.type
|
||||
if msg_type in self._message_handlers:
|
||||
for handler in self._message_handlers[msg_type]:
|
||||
await handler(event)
|
||||
|
||||
async def set_message(self, from_user: int, message_id: int, content: str):
|
||||
if from_user not in self.msg_queue:
|
||||
self.msg_queue[from_user] = []
|
||||
|
||||
self.msg_queue[from_user].append(
|
||||
{
|
||||
'msg_id': message_id,
|
||||
'content': content,
|
||||
}
|
||||
)
|
||||
166
src/langbot/libs/official_account_api/oaevent.py
Normal file
166
src/langbot/libs/official_account_api/oaevent.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class OAEvent(dict):
|
||||
"""
|
||||
封装从微信公众号收到的事件数据对象(字典),提供属性以获取其中的字段。
|
||||
|
||||
除 `type` 和 `detail_type` 属性对于任何事件都有效外,其它属性是否存在(若不存在则返回 `None`)依事件类型不同而不同。
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_payload(payload: Dict[str, Any]) -> Optional['OAEvent']:
|
||||
"""
|
||||
从微信公众号事件数据构造 `WecomEvent` 对象。
|
||||
|
||||
Args:
|
||||
payload (Dict[str, Any]): 解密后的微信事件数据。
|
||||
|
||||
Returns:
|
||||
Optional[OAEvent]: 如果事件数据合法,则返回 OAEvent 对象;否则返回 None。
|
||||
"""
|
||||
try:
|
||||
event = OAEvent(payload)
|
||||
_ = event.type, event.detail_type # 确保必须字段存在
|
||||
return event
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""
|
||||
事件类型,例如 "message"、"event"、"text" 等。
|
||||
|
||||
Returns:
|
||||
str: 事件类型。
|
||||
"""
|
||||
return self.get('MsgType', '')
|
||||
|
||||
@property
|
||||
def picurl(self) -> str:
|
||||
"""
|
||||
图片链接
|
||||
"""
|
||||
return self.get('PicUrl', '')
|
||||
|
||||
@property
|
||||
def detail_type(self) -> str:
|
||||
"""
|
||||
事件详细类型,依 `type` 的不同而不同。例如:
|
||||
- 消息事件: "text", "image", "voice", 等
|
||||
- 事件通知: "subscribe", "unsubscribe", "click", 等
|
||||
|
||||
Returns:
|
||||
str: 事件详细类型。
|
||||
"""
|
||||
if self.type == 'event':
|
||||
return self.get('Event', '')
|
||||
return self.type
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""
|
||||
事件名,对于消息事件是 `type.detail_type`,对于其他事件是 `event_type`。
|
||||
|
||||
Returns:
|
||||
str: 事件名。
|
||||
"""
|
||||
return f'{self.type}.{self.detail_type}'
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[str]:
|
||||
"""
|
||||
发送方账号
|
||||
"""
|
||||
return self.get('FromUserName')
|
||||
|
||||
@property
|
||||
def receiver_id(self) -> Optional[str]:
|
||||
"""
|
||||
接收者 ID,例如机器人自身的公众号微信 ID。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 接收者 ID。
|
||||
"""
|
||||
return self.get('ToUserName')
|
||||
|
||||
@property
|
||||
def message_id(self) -> Optional[str]:
|
||||
"""
|
||||
消息 ID,仅在消息类型事件中存在。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 消息 ID。
|
||||
"""
|
||||
return self.get('MsgId')
|
||||
|
||||
@property
|
||||
def message(self) -> Optional[str]:
|
||||
"""
|
||||
消息内容,仅在消息类型事件中存在。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 消息内容。
|
||||
"""
|
||||
return self.get('Content')
|
||||
|
||||
@property
|
||||
def media_id(self) -> Optional[str]:
|
||||
"""
|
||||
媒体文件 ID,仅在图片、语音等消息类型中存在。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 媒体文件 ID。
|
||||
"""
|
||||
return self.get('MediaId')
|
||||
|
||||
@property
|
||||
def timestamp(self) -> Optional[int]:
|
||||
"""
|
||||
事件发生的时间戳。
|
||||
|
||||
Returns:
|
||||
Optional[int]: 时间戳。
|
||||
"""
|
||||
return self.get('CreateTime')
|
||||
|
||||
@property
|
||||
def event_key(self) -> Optional[str]:
|
||||
"""
|
||||
事件的 Key 值,例如点击菜单时的 `EventKey`。
|
||||
|
||||
Returns:
|
||||
Optional[str]: 事件 Key。
|
||||
"""
|
||||
return self.get('EventKey')
|
||||
|
||||
def __getattr__(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
允许通过属性访问数据中的任意字段。
|
||||
|
||||
Args:
|
||||
key (str): 字段名。
|
||||
|
||||
Returns:
|
||||
Optional[Any]: 字段值。
|
||||
"""
|
||||
return self.get(key)
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
允许通过属性设置数据中的任意字段。
|
||||
|
||||
Args:
|
||||
key (str): 字段名。
|
||||
value (Any): 字段值。
|
||||
"""
|
||||
self[key] = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
生成事件对象的字符串表示。
|
||||
|
||||
Returns:
|
||||
str: 字符串表示。
|
||||
"""
|
||||
return f'<WecomEvent {super().__repr__()}>'
|
||||
263
src/langbot/libs/qq_official_api/api.py
Normal file
263
src/langbot/libs/qq_official_api/api.py
Normal file
@@ -0,0 +1,263 @@
|
||||
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
|
||||
112
src/langbot/libs/qq_official_api/qqofficialevent.py
Normal file
112
src/langbot/libs/qq_official_api/qqofficialevent.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class QQOfficialEvent(dict):
|
||||
@staticmethod
|
||||
def from_payload(payload: Dict[str, Any]) -> Optional['QQOfficialEvent']:
|
||||
try:
|
||||
event = QQOfficialEvent(payload)
|
||||
return event
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def t(self) -> str:
|
||||
"""
|
||||
事件类型
|
||||
"""
|
||||
return self.get('t', '')
|
||||
|
||||
@property
|
||||
def user_openid(self) -> str:
|
||||
"""
|
||||
用户openid
|
||||
"""
|
||||
return self.get('user_openid', {})
|
||||
|
||||
@property
|
||||
def timestamp(self) -> str:
|
||||
"""
|
||||
时间戳
|
||||
"""
|
||||
return self.get('timestamp', {})
|
||||
|
||||
@property
|
||||
def d_author_id(self) -> str:
|
||||
"""
|
||||
作者id
|
||||
"""
|
||||
return self.get('id', {})
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
"""
|
||||
内容
|
||||
"""
|
||||
return self.get('content', '')
|
||||
|
||||
@property
|
||||
def d_id(self) -> str:
|
||||
"""
|
||||
d_id
|
||||
"""
|
||||
return self.get('d_id', {})
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""
|
||||
消息id,msg_id
|
||||
"""
|
||||
return self.get('id', {})
|
||||
|
||||
@property
|
||||
def channel_id(self) -> str:
|
||||
"""
|
||||
频道id
|
||||
"""
|
||||
return self.get('channel_id', {})
|
||||
|
||||
@property
|
||||
def username(self) -> str:
|
||||
"""
|
||||
用户名
|
||||
"""
|
||||
return self.get('username', {})
|
||||
|
||||
@property
|
||||
def guild_id(self) -> str:
|
||||
"""
|
||||
频道id
|
||||
"""
|
||||
return self.get('guild_id', {})
|
||||
|
||||
@property
|
||||
def member_openid(self) -> str:
|
||||
"""
|
||||
成员openid
|
||||
"""
|
||||
return self.get('openid', {})
|
||||
|
||||
@property
|
||||
def attachments(self) -> str:
|
||||
"""
|
||||
附件url
|
||||
"""
|
||||
url = self.get('image_attachments', '')
|
||||
if url and not url.startswith('https://'):
|
||||
url = 'https://' + url
|
||||
return url
|
||||
|
||||
@property
|
||||
def group_openid(self) -> str:
|
||||
"""
|
||||
群组id
|
||||
"""
|
||||
return self.get('group_openid', {})
|
||||
|
||||
@property
|
||||
def content_type(self) -> str:
|
||||
"""
|
||||
文件类型
|
||||
"""
|
||||
return self.get('content_type', '')
|
||||
102
src/langbot/libs/slack_api/api.py
Normal file
102
src/langbot/libs/slack_api/api.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
import traceback
|
||||
from quart import Quart, jsonify, request
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
from .slackevent import SlackEvent
|
||||
from typing import Callable
|
||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||
|
||||
|
||||
class SlackClient:
|
||||
def __init__(self, bot_token: str, signing_secret: str, logger: None):
|
||||
self.bot_token = bot_token
|
||||
self.signing_secret = signing_secret
|
||||
self.app = Quart(__name__)
|
||||
self.client = AsyncWebClient(self.bot_token)
|
||||
self.app.add_url_rule(
|
||||
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
|
||||
)
|
||||
self._message_handlers = {
|
||||
'example': [],
|
||||
}
|
||||
self.bot_user_id = None # 避免机器人回复自己的消息
|
||||
self.logger = logger
|
||||
|
||||
async def handle_callback_request(self):
|
||||
try:
|
||||
body = await request.get_data()
|
||||
data = json.loads(body)
|
||||
if 'type' in data:
|
||||
if data['type'] == 'url_verification':
|
||||
return data['challenge']
|
||||
|
||||
bot_user_id = data.get('event', {}).get('bot_id', '')
|
||||
|
||||
if self.bot_user_id and bot_user_id == self.bot_user_id:
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
# 处理私信
|
||||
if data and data.get('event', {}).get('channel_type') in ['im']:
|
||||
event = SlackEvent.from_payload(data)
|
||||
await self._handle_message(event)
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
# 处理群聊
|
||||
if data.get('event', {}).get('type') == 'app_mention':
|
||||
data.setdefault('event', {})['channel_type'] = 'channel'
|
||||
event = SlackEvent.from_payload(data)
|
||||
await self._handle_message(event)
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
except Exception as e:
|
||||
await self.logger.error(f'Error in handle_callback_request: {traceback.format_exc()}')
|
||||
raise (e)
|
||||
|
||||
async def _handle_message(self, event: SlackEvent):
|
||||
"""
|
||||
处理消息事件。
|
||||
"""
|
||||
msg_type = event.type
|
||||
if msg_type in self._message_handlers:
|
||||
for handler in self._message_handlers[msg_type]:
|
||||
await handler(event)
|
||||
|
||||
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 send_message_to_channel(self, text: str, channel_id: str):
|
||||
try:
|
||||
response = await self.client.chat_postMessage(channel=channel_id, text=text)
|
||||
if self.bot_user_id is None and response.get('ok'):
|
||||
self.bot_user_id = response['message']['bot_id']
|
||||
return
|
||||
except Exception as e:
|
||||
await self.logger.error(f'Error in send_message: {e}')
|
||||
raise e
|
||||
|
||||
async def send_message_to_one(self, text: str, user_id: str):
|
||||
try:
|
||||
response = await self.client.chat_postMessage(channel='@' + user_id, text=text)
|
||||
if self.bot_user_id is None and response.get('ok'):
|
||||
self.bot_user_id = response['message']['bot_id']
|
||||
|
||||
return
|
||||
except Exception as e:
|
||||
await self.logger.error(f'Error in send_message: {traceback.format_exc()}')
|
||||
raise e
|
||||
|
||||
async def run_task(self, host: str, port: int, *args, **kwargs):
|
||||
"""
|
||||
启动 Quart 应用。
|
||||
"""
|
||||
await self.app.run_task(host=host, port=port, *args, **kwargs)
|
||||
87
src/langbot/libs/slack_api/slackevent.py
Normal file
87
src/langbot/libs/slack_api/slackevent.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class SlackEvent(dict):
|
||||
@staticmethod
|
||||
def from_payload(payload: Dict[str, Any]) -> Optional['SlackEvent']:
|
||||
try:
|
||||
event = SlackEvent(payload)
|
||||
return event
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
if self.get('event', {}).get('channel_type') == 'im':
|
||||
blocks = self.get('event', {}).get('blocks', [])
|
||||
if not blocks:
|
||||
return ''
|
||||
|
||||
elements = blocks[0].get('elements', [])
|
||||
if not elements:
|
||||
return ''
|
||||
|
||||
elements = elements[0].get('elements', [])
|
||||
text = ''
|
||||
|
||||
for el in elements:
|
||||
if el.get('type') == 'text':
|
||||
text += el.get('text', '')
|
||||
elif el.get('type') == 'link':
|
||||
text += el.get('url', '')
|
||||
|
||||
return text
|
||||
|
||||
if self.get('event', {}).get('channel_type') == 'channel':
|
||||
message_text = ''
|
||||
for block in self.get('event', {}).get('blocks', []):
|
||||
if block.get('type') == 'rich_text':
|
||||
for element in block.get('elements', []):
|
||||
if element.get('type') == 'rich_text_section':
|
||||
parts = []
|
||||
for el in element.get('elements', []):
|
||||
if el.get('type') == 'text':
|
||||
parts.append(el['text'])
|
||||
elif el.get('type') == 'link':
|
||||
parts.append(el['url'])
|
||||
message_text = ''.join(parts)
|
||||
|
||||
return message_text
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[str]:
|
||||
return self.get('event', {}).get('user', '')
|
||||
|
||||
@property
|
||||
def channel_id(self) -> Optional[str]:
|
||||
return self.get('event', {}).get('channel', '')
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""message对应私聊,app_mention对应频道at"""
|
||||
return self.get('event', {}).get('channel_type', '')
|
||||
|
||||
@property
|
||||
def message_id(self) -> str:
|
||||
return self.get('event_id', '')
|
||||
|
||||
@property
|
||||
def pic_url(self) -> str:
|
||||
"""提取 Slack 事件中的图片 URL"""
|
||||
files = self.get('event', {}).get('files', [])
|
||||
if files:
|
||||
return files[0].get('url_private', '')
|
||||
return None
|
||||
|
||||
@property
|
||||
def sender_name(self) -> str:
|
||||
return self.get('event', {}).get('user', '')
|
||||
|
||||
def __getattr__(self, key: str) -> Optional[Any]:
|
||||
return self.get(key)
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
self[key] = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<SlackEvent {super().__repr__()}>'
|
||||
201
src/langbot/libs/wechatpad_api/LICENSE
Normal file
201
src/langbot/libs/wechatpad_api/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
38
src/langbot/libs/wechatpad_api/README.md
Normal file
38
src/langbot/libs/wechatpad_api/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# wechatpad-python
|
||||
|
||||
|
||||
## 此项目时准备对接wechatpadpro 的pythonsdk
|
||||
|
||||
## 未完工接口
|
||||
|
||||
* 关于好友的接口
|
||||
* 关于群管理的接口
|
||||
* 关于下载的接口
|
||||
* 关于用户的部分接口
|
||||
* 关于消息的部分接口
|
||||
* 关于支付的
|
||||
* 关于朋友圈的
|
||||
* 关于标签的
|
||||
* 关于收藏的
|
||||
|
||||
* 暂时只写了一部分接口
|
||||
|
||||
|
||||
## 已完工接口
|
||||
|
||||
1. 获取普通token
|
||||
2. 登录二维码(只是返回数据,暂时还未打印二维码)
|
||||
3. 获取登录状态
|
||||
4. 唤醒登录
|
||||
5. 退出登录
|
||||
6. 获取用户信息
|
||||
7. 获取用户二维码
|
||||
8. 上传用户头像
|
||||
9. 获取设备信息
|
||||
10. 发送文本消息
|
||||
11. 发送图片消息
|
||||
12. 发送语音消息
|
||||
13. 发送app消息
|
||||
14. 发送emoji消息
|
||||
15. 发送名片消息
|
||||
16. 撤回消息
|
||||
1
src/langbot/libs/wechatpad_api/__init__.py
Normal file
1
src/langbot/libs/wechatpad_api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .client import WeChatPadClient as WeChatPadClient
|
||||
0
src/langbot/libs/wechatpad_api/api/__init__.py
Normal file
0
src/langbot/libs/wechatpad_api/api/__init__.py
Normal file
12
src/langbot/libs/wechatpad_api/api/chatroom.py
Normal file
12
src/langbot/libs/wechatpad_api/api/chatroom.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from langbot.libs.wechatpad_api.util.http_util import post_json
|
||||
|
||||
|
||||
class ChatRoomApi:
|
||||
def __init__(self, base_url, token):
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
|
||||
def get_chatroom_member_detail(self, chatroom_name):
|
||||
params = {'ChatRoomName': chatroom_name}
|
||||
url = self.base_url + '/group/GetChatroomMemberDetail'
|
||||
return post_json(url, token=self.token, data=params)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user