Compare commits
2272 Commits
v3.2.0
...
features/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38486bc5aa | ||
|
|
7b8800c4eb | ||
|
|
8f4625f53b | ||
|
|
1e5f243edb | ||
|
|
e5eab2af34 | ||
|
|
c10973e160 | ||
|
|
b1e4bff3ec | ||
|
|
c1202cda63 | ||
|
|
32d6cd7776 | ||
|
|
2f78d30e93 | ||
|
|
33407c9f0d | ||
|
|
d2d5ef1c5c | ||
|
|
98d8eaee02 | ||
|
|
10b9228060 | ||
|
|
5872f1e017 | ||
|
|
5073f21002 | ||
|
|
69aaf09ac8 | ||
|
|
6e61ee81d8 | ||
|
|
cfd05a8d17 | ||
|
|
29845fcc4c | ||
|
|
e204b180a8 | ||
|
|
563972fd29 | ||
|
|
cbe94b84fc | ||
|
|
aa6f73574d | ||
|
|
94f0419ef7 | ||
|
|
cefd2d7f49 | ||
|
|
81e1e545fb | ||
|
|
d516920e72 | ||
|
|
2171372246 | ||
|
|
d2df4d0cce | ||
|
|
6ab90fc123 | ||
|
|
1a84ebbb1e | ||
|
|
c9c0352369 | ||
|
|
9903b028a3 | ||
|
|
49def5d883 | ||
|
|
6975525b70 | ||
|
|
fbc4f8527b | ||
|
|
90cb5a1951 | ||
|
|
ac71d9f034 | ||
|
|
64bcbc9fc0 | ||
|
|
9e7d46f956 | ||
|
|
e911896cfb | ||
|
|
9c6d66093f | ||
|
|
b2e39b9701 | ||
|
|
e95ad4049b | ||
|
|
1df49d1d6f | ||
|
|
b71000e2f3 | ||
|
|
47e6ed455e | ||
|
|
92592fb9d9 | ||
|
|
02a9769b35 | ||
|
|
7640f11bfc | ||
|
|
be8a0991ed | ||
|
|
9fa44dbcfa | ||
|
|
61aac9c80c | ||
|
|
60af83cfee | ||
|
|
cf64e6c231 | ||
|
|
2cae941bae | ||
|
|
bc0784f41d | ||
|
|
b711140f26 | ||
|
|
c57d75e01a | ||
|
|
1d766001bb | ||
|
|
0759a11a85 | ||
|
|
cb749a38ab | ||
|
|
369eab18ab | ||
|
|
73edeae013 | ||
|
|
7d46314dc8 | ||
|
|
d5a53a89eb | ||
|
|
a85bc510dd | ||
|
|
2beea7d218 | ||
|
|
a93cd3dd5f | ||
|
|
6c1f540170 | ||
|
|
d026a9f009 | ||
|
|
a8e7dadd39 | ||
|
|
2f8d921adf | ||
|
|
0c6e526f94 | ||
|
|
b1e3018b6b | ||
|
|
87f05fce66 | ||
|
|
1b37530c96 | ||
|
|
db4d02c2e2 | ||
|
|
fd7811402b | ||
|
|
eb0325e627 | ||
|
|
842c3c8ea9 | ||
|
|
8b4b04ec09 | ||
|
|
9f32c9280f | ||
|
|
4fcd09cfa8 | ||
|
|
7a8d65d37d | ||
|
|
23129a9ba2 | ||
|
|
7f791e730b | ||
|
|
f7e296b349 | ||
|
|
712d4acaaa | ||
|
|
74a5c01f21 | ||
|
|
3ba8724d77 | ||
|
|
6313a7d8a9 | ||
|
|
432a3f520c | ||
|
|
191b3e42d4 | ||
|
|
a27f05fcb4 | ||
|
|
2f33e0b873 | ||
|
|
f0359467f1 | ||
|
|
d1db8cf2c8 | ||
|
|
b1985ed2ce | ||
|
|
140ddc70e6 | ||
|
|
d7fd616470 | ||
|
|
3ccbef141e | ||
|
|
e92fbb0443 | ||
|
|
bd270aed68 | ||
|
|
28d7864393 | ||
|
|
b5d8173ee3 | ||
|
|
17d62a9af7 | ||
|
|
d89fb863ed | ||
|
|
a21ad77820 | ||
|
|
f86c8e8cab | ||
|
|
cb12cbdd3d | ||
|
|
6661fa996c | ||
|
|
c19bca798b | ||
|
|
8f98b411db | ||
|
|
a8aa03847e | ||
|
|
1bfd747cc6 | ||
|
|
ae06d945a7 | ||
|
|
9f41d5f34d | ||
|
|
ef61c52908 | ||
|
|
d8842ef274 | ||
|
|
c88fdaf353 | ||
|
|
af295da871 | ||
|
|
083235a2fe | ||
|
|
2a3a5f7eb2 | ||
|
|
77c48f280f | ||
|
|
0ee1eb2f9f | ||
|
|
c2b20365bb | ||
|
|
cfdc7e4452 | ||
|
|
2363f61aa9 | ||
|
|
557ac6f9fa | ||
|
|
a49b871cf9 | ||
|
|
a0d6b3efba | ||
|
|
6cabf07bc0 | ||
|
|
a15444ee8c | ||
|
|
ceb5f5669e | ||
|
|
25b75e05e4 | ||
|
|
4d214bb5c1 | ||
|
|
7cbaed8c6c | ||
|
|
2915fdf665 | ||
|
|
a66c385b08 | ||
|
|
4dace7c5d8 | ||
|
|
8ebf087dbf | ||
|
|
2fa8bda5bb | ||
|
|
a5ae833945 | ||
|
|
d21d42b312 | ||
|
|
78575f0f0a | ||
|
|
8ccd292d16 | ||
|
|
2534f59398 | ||
|
|
5c60dbe2b1 | ||
|
|
c99ecde15f | ||
|
|
219f3403d9 | ||
|
|
00f417bad6 | ||
|
|
81649f053b | ||
|
|
e5bde50f2d | ||
|
|
0321e00b0d | ||
|
|
09528e3292 | ||
|
|
e7412a9cbf | ||
|
|
01efe5f869 | ||
|
|
28a178a55c | ||
|
|
88f130014c | ||
|
|
af258c590c | ||
|
|
b0eb5733be | ||
|
|
fe35bfba37 | ||
|
|
7cfbc4ab8f | ||
|
|
7a9d4f0abd | ||
|
|
6f6a5b565c | ||
|
|
e57deb873c | ||
|
|
0f692b1608 | ||
|
|
8c03e79f99 | ||
|
|
71290f0929 | ||
|
|
22364ef7de | ||
|
|
2cc1eb1abc | ||
|
|
90dbcbb4e2 | ||
|
|
66503d58be | ||
|
|
8e10f0ce2b | ||
|
|
f51f510f2e | ||
|
|
c44f085b47 | ||
|
|
a35f36eeaf | ||
|
|
14564c392a | ||
|
|
76e05ea749 | ||
|
|
ab599dceed | ||
|
|
4c37604445 | ||
|
|
bb74018d19 | ||
|
|
575289e5bc | ||
|
|
e89da2a7b4 | ||
|
|
bd34959f68 | ||
|
|
622dcf8fd5 | ||
|
|
9e315739b7 | ||
|
|
7b01adc5df | ||
|
|
432fc47443 | ||
|
|
d8fba44c5e | ||
|
|
e29d3d8c01 | ||
|
|
e678413214 | ||
|
|
eaa9d9d087 | ||
|
|
9e3cc076b7 | ||
|
|
3bb01fa52c | ||
|
|
008e49d144 | ||
|
|
4e275384b0 | ||
|
|
63ec99f67a | ||
|
|
14a8bb57df | ||
|
|
7512bfc710 | ||
|
|
3c3b6dadc3 | ||
|
|
cd722a0e39 | ||
|
|
a1b5d0a100 | ||
|
|
69d3ae709c | ||
|
|
67ef993d61 | ||
|
|
20f49890ad | ||
|
|
3e4917f0a1 | ||
|
|
99ee75aec6 | ||
|
|
1674653a42 | ||
|
|
d2f7e55bf5 | ||
|
|
9f31df7f3a | ||
|
|
b8c1b53d67 | ||
|
|
2495837791 | ||
|
|
b6562e3c47 | ||
|
|
c57da046ee | ||
|
|
ff63134c14 | ||
|
|
3f5210c587 | ||
|
|
3df5e7b9b9 | ||
|
|
225db66738 | ||
|
|
383ebb8f57 | ||
|
|
e1bed60f1f | ||
|
|
edbb856023 | ||
|
|
98d3ab646f | ||
|
|
81be556f1b | ||
|
|
f45a085469 | ||
|
|
210cc58cc3 | ||
|
|
1063b11ef6 | ||
|
|
a4e999c47f | ||
|
|
543e01c301 | ||
|
|
14e0aa3ec5 | ||
|
|
1a8a171f8b | ||
|
|
f1954f9a43 | ||
|
|
441b148501 | ||
|
|
bd0f30b81c | ||
|
|
ad14e9bf40 | ||
|
|
6f71301aaf | ||
|
|
5f0d601baa | ||
|
|
f234a5bcc2 | ||
|
|
ab677ea100 | ||
|
|
f3ad53e949 | ||
|
|
d324cfa84d | ||
|
|
dd4319d72a | ||
|
|
1f2de3d3d8 | ||
|
|
72702beb0b | ||
|
|
adb0cbc5dd | ||
|
|
6a503b82c3 | ||
|
|
28a87351f1 | ||
|
|
bcc97378b0 | ||
|
|
eb8a138713 | ||
|
|
dcd7dcbbdf | ||
|
|
1538759ba7 | ||
|
|
30e8ea7fd8 | ||
|
|
879b7b582c | ||
|
|
8ba4236402 | ||
|
|
5eef8fa9b9 | ||
|
|
d03d035437 | ||
|
|
68e8e1f70b | ||
|
|
7acb45b157 | ||
|
|
c36142deaf | ||
|
|
5fd6e316fa | ||
|
|
39a9d7765a | ||
|
|
7cfcba29a6 | ||
|
|
9bf8aadca9 | ||
|
|
714d4af63d | ||
|
|
8203fdb4f0 | ||
|
|
5e1e2d1a4f | ||
|
|
2f941de65b | ||
|
|
777c503002 | ||
|
|
e9b23f68fd | ||
|
|
efa45e6203 | ||
|
|
638f55f83c | ||
|
|
8b2fc29d5b | ||
|
|
b516fb0550 | ||
|
|
efef34c01e | ||
|
|
5f1dfa7599 | ||
|
|
8e9c7544cf | ||
|
|
4e3d5641c8 | ||
|
|
20b760529e | ||
|
|
a55a07c5ff | ||
|
|
94ee8ea297 | ||
|
|
ec5d71d0e1 | ||
|
|
d121d08d05 | ||
|
|
be08f4a558 | ||
|
|
010f082fbb | ||
|
|
073cdf6d51 | ||
|
|
4df8606ab6 | ||
|
|
71442d26ec | ||
|
|
4f5528869c | ||
|
|
f16feff17b | ||
|
|
71b233fe5f | ||
|
|
770dec9ed6 | ||
|
|
2ca95a988e | ||
|
|
d8aae538cd | ||
|
|
cf1e7ee08a | ||
|
|
d14513ddfd | ||
|
|
9a9017bc6c | ||
|
|
3c9b654713 | ||
|
|
80d2ad40bc | ||
|
|
31670e75e5 | ||
|
|
ed6011a2be | ||
|
|
cdded38ade | ||
|
|
f536f24833 | ||
|
|
f5bff00b1f | ||
|
|
27c9717445 | ||
|
|
863a1ba8ef | ||
|
|
cb04dd2b83 | ||
|
|
8c7cf51958 | ||
|
|
244fb1fed6 | ||
|
|
25f7a68a13 | ||
|
|
62d8cf79ef | ||
|
|
646b18d910 | ||
|
|
2f81b2e381 | ||
|
|
1f5a7e7885 | ||
|
|
80fca470f2 | ||
|
|
6e9d9ac856 | ||
|
|
8d6fada1eb | ||
|
|
3e715399a1 | ||
|
|
81cc8831f9 | ||
|
|
f7370044a7 | ||
|
|
51b015a629 | ||
|
|
392af7a553 | ||
|
|
d2dd07bad7 | ||
|
|
cebcd6925a | ||
|
|
e7b4357fc7 | ||
|
|
dc279dde4a | ||
|
|
c0810a674f | ||
|
|
0760cabbbe | ||
|
|
3b149c520b | ||
|
|
3d19fc89ff | ||
|
|
cd1b1919f4 | ||
|
|
0ed646eb27 | ||
|
|
c0c5859c99 | ||
|
|
a47121b849 | ||
|
|
d9dd20e89a | ||
|
|
ed4609ebe5 | ||
|
|
e24225c828 | ||
|
|
01ef86d658 | ||
|
|
cd4802da04 | ||
|
|
2aca65780f | ||
|
|
2c435f7387 | ||
|
|
cc1afd1a9c | ||
|
|
6f098cdba6 | ||
|
|
d03e9fb90a | ||
|
|
9f2966abe9 | ||
|
|
4e28ea1883 | ||
|
|
289214e85c | ||
|
|
a20d98bf93 | ||
|
|
7c3d98acbe | ||
|
|
7311786f48 | ||
|
|
82de9c926e | ||
|
|
7fd86d4de3 | ||
|
|
724da29e2a | ||
|
|
54113d7b94 | ||
|
|
66396e8290 | ||
|
|
72be76215f | ||
|
|
ace86703a9 | ||
|
|
7b25495463 | ||
|
|
3d4b651c1f | ||
|
|
d305ae064d | ||
|
|
ac4f3d8907 | ||
|
|
af2687771b | ||
|
|
a67b7f909a | ||
|
|
f9c3e4cdb0 | ||
|
|
dc62c1f8d4 | ||
|
|
0441b51a68 | ||
|
|
5c0c9f687e | ||
|
|
e049c54043 | ||
|
|
99e47540d5 | ||
|
|
8e1885ffeb | ||
|
|
8501a0c205 | ||
|
|
797f2a3173 | ||
|
|
1057b4bc35 | ||
|
|
efc0116595 | ||
|
|
cdc560fad0 | ||
|
|
75a2803710 | ||
|
|
fb3169faa4 | ||
|
|
d587bd837e | ||
|
|
b9fab74edc | ||
|
|
50c22bbadb | ||
|
|
d0b10b9195 | ||
|
|
50a296de20 | ||
|
|
c8fe4f4a3c | ||
|
|
a8ba0720af | ||
|
|
745a01246c | ||
|
|
bee5d3550f | ||
|
|
1789393151 | ||
|
|
345afe1338 | ||
|
|
65428aa49f | ||
|
|
b251ee9322 | ||
|
|
04f00682a0 | ||
|
|
90dcda1475 | ||
|
|
f1ee4eb89f | ||
|
|
343fc22168 | ||
|
|
00ef0d7e3d | ||
|
|
f2deaf6199 | ||
|
|
617a2c010e | ||
|
|
c79e38e044 | ||
|
|
38eae1d1ee | ||
|
|
7e4c89b0cb | ||
|
|
14c29f07bd | ||
|
|
825e3dbcf5 | ||
|
|
8275130f04 | ||
|
|
2c47abea95 | ||
|
|
85aa28d724 | ||
|
|
53a3736b04 | ||
|
|
86ba3c230e | ||
|
|
8d21126bd6 | ||
|
|
74ded91976 | ||
|
|
7c27520d57 | ||
|
|
b54bbc4c5a | ||
|
|
3e09a4ddd4 | ||
|
|
f93f04a536 | ||
|
|
b93f30b809 | ||
|
|
95bd2f26a5 | ||
|
|
7cfcf056f9 | ||
|
|
96b565e1e8 | ||
|
|
9d7ad7a18f | ||
|
|
9838c2758b | ||
|
|
1b1f5f5a5e | ||
|
|
0f95f62aa1 | ||
|
|
9405ba7871 | ||
|
|
ccb95f803c | ||
|
|
dae745d925 | ||
|
|
791db65526 | ||
|
|
60b2ff0a7a | ||
|
|
e6c8507379 | ||
|
|
420db5416e | ||
|
|
6e03218d54 | ||
|
|
5e4bd36b26 | ||
|
|
bbc039366e | ||
|
|
e1ec7dbbba | ||
|
|
075b008740 | ||
|
|
b2c382fa01 | ||
|
|
02e2e617f5 | ||
|
|
c5f9b5861f | ||
|
|
2dace4c697 | ||
|
|
c7891385ca | ||
|
|
2059ddcadf | ||
|
|
ba1b68df20 | ||
|
|
bfc8024119 | ||
|
|
f26cf6ed6f | ||
|
|
403b61836d | ||
|
|
b5af7d1eb9 | ||
|
|
f453af6e4c | ||
|
|
f2be55bd8e | ||
|
|
d241dd17ca | ||
|
|
cecafdfe6c | ||
|
|
6fecfd1a0e | ||
|
|
64245d001c | ||
|
|
7d92965cae | ||
|
|
b4fa08c4e2 | ||
|
|
d4e9566851 | ||
|
|
a26b494f7f | ||
|
|
b84e22e41f | ||
|
|
cee6efab19 | ||
|
|
30f71cb550 | ||
|
|
771e755a78 | ||
|
|
16ec462abd | ||
|
|
ca55465d3c | ||
|
|
7098c98dde | ||
|
|
f56355da89 | ||
|
|
422160debd | ||
|
|
8062cf406a | ||
|
|
0e802232ec | ||
|
|
f650a9205d | ||
|
|
c85dbb2347 | ||
|
|
a6a79128c8 | ||
|
|
42839627e8 | ||
|
|
e7f35098e4 | ||
|
|
267e68a894 | ||
|
|
b32b444438 | ||
|
|
522d0f8313 | ||
|
|
5715e5de67 | ||
|
|
cc6b05e8b3 | ||
|
|
417747d5d0 | ||
|
|
a34f439226 | ||
|
|
b7ca014fd0 | ||
|
|
fa098d585a | ||
|
|
c35a14e3ec | ||
|
|
60651736a5 | ||
|
|
581f9b7bd3 | ||
|
|
124eb04807 | ||
|
|
1d561da7fb | ||
|
|
16e3cd0784 | ||
|
|
a6d91933dc | ||
|
|
445c40f758 | ||
|
|
725a841a3b | ||
|
|
f77c453843 | ||
|
|
ba6718d5bc | ||
|
|
cdb7a1b3fa | ||
|
|
a03c79b89d | ||
|
|
98800d3426 | ||
|
|
a616adaac4 | ||
|
|
ffb5605c99 | ||
|
|
621b556856 | ||
|
|
a3ffecbb2a | ||
|
|
ea64cebe2a | ||
|
|
e79487dd5f | ||
|
|
7fe1c1ec89 | ||
|
|
ab2bbff369 | ||
|
|
ec32825309 | ||
|
|
fd0c182087 | ||
|
|
49fcff1daf | ||
|
|
33b64ddf39 | ||
|
|
4c447aa648 | ||
|
|
ccbfc3d274 | ||
|
|
f83fe43bbb | ||
|
|
19022d67f8 | ||
|
|
58a815dd6b | ||
|
|
1ce95c473d | ||
|
|
eb365e398d | ||
|
|
bc9fe82860 | ||
|
|
b3cd9bf2b9 | ||
|
|
c5c2b829ec | ||
|
|
9713f96401 | ||
|
|
11f35ebf96 | ||
|
|
7d403aa181 | ||
|
|
64af810a4a | ||
|
|
30821905af | ||
|
|
a9dbff756b | ||
|
|
a6aba10d3d | ||
|
|
9c276c37fe | ||
|
|
6ab6c0fd4c | ||
|
|
b6b0fe3fff | ||
|
|
0d5825bda9 | ||
|
|
cdfb64631a | ||
|
|
d161c281c8 | ||
|
|
8fed5bf2a1 | ||
|
|
98d2e9bd27 | ||
|
|
a03af55edd | ||
|
|
86e2fd9aee | ||
|
|
97bd0e5e58 | ||
|
|
ceaba21986 | ||
|
|
172a77d942 | ||
|
|
4f9d2d2a7d | ||
|
|
8c929f6e05 | ||
|
|
3319b71f5b | ||
|
|
46ec028a5b | ||
|
|
0ce0ef3e5c | ||
|
|
375b071cb2 | ||
|
|
29e1417ff2 | ||
|
|
75db2bd366 | ||
|
|
60ca1efbda | ||
|
|
2692e4978b | ||
|
|
91982eb002 | ||
|
|
bb1dec76fa | ||
|
|
f618b8fcdc | ||
|
|
9147cab75b | ||
|
|
5f07bcc8e6 | ||
|
|
705cf2ea1b | ||
|
|
42c4394484 | ||
|
|
221221a3c1 | ||
|
|
9564166297 | ||
|
|
f5cf3c3c8e | ||
|
|
18f919fb6b | ||
|
|
0924835253 | ||
|
|
20d2e5c578 | ||
|
|
907801605c | ||
|
|
93bc684e8c | ||
|
|
a76c98d57e | ||
|
|
d937a800d0 | ||
|
|
d16f3a227f | ||
|
|
80c9a3eeda | ||
|
|
e68173b451 | ||
|
|
40c27d87f5 | ||
|
|
3c13b5049d | ||
|
|
8288d5e51f | ||
|
|
6e1449900a | ||
|
|
4ffbb18ab4 | ||
|
|
b27271b7a3 | ||
|
|
ebb6665f64 | ||
|
|
e4e5731ffd | ||
|
|
2ab5810f13 | ||
|
|
af934c5d09 | ||
|
|
1e0cf7c112 | ||
|
|
46859c93c9 | ||
|
|
ea1f9cb3b2 | ||
|
|
1641549016 | ||
|
|
716a5dbb8a | ||
|
|
af98cb11c5 | ||
|
|
9a4c2cf341 | ||
|
|
2bc3bcd102 | ||
|
|
d6c663f79d | ||
|
|
9ed86e5f53 | ||
|
|
303e0bc037 | ||
|
|
2cc24019f9 | ||
|
|
83ce774d19 | ||
|
|
2b4ee13b5e | ||
|
|
3a964561f0 | ||
|
|
6959f86632 | ||
|
|
537d373e10 | ||
|
|
cceadf222c | ||
|
|
cf5a4af623 | ||
|
|
39aea11c22 | ||
|
|
c2f1227700 | ||
|
|
900f14d37c | ||
|
|
598249b1d6 | ||
|
|
7ed15bdf04 | ||
|
|
2fc0ec0f72 | ||
|
|
5e9c2a669b | ||
|
|
b310521884 | ||
|
|
288945bf7e | ||
|
|
4fc07cff36 | ||
|
|
b884fe0e86 | ||
|
|
855858c236 | ||
|
|
c11a2a5419 | ||
|
|
773a6572af | ||
|
|
88ad373c9b | ||
|
|
51666464b9 | ||
|
|
5af9cf2f52 | ||
|
|
12c4ae4b10 | ||
|
|
4e1bef414a | ||
|
|
e896c18644 | ||
|
|
c852685e74 | ||
|
|
1e99797df8 | ||
|
|
52a4c986a8 | ||
|
|
c501728204 | ||
|
|
6b067fa6a7 | ||
|
|
a1cd5c53a9 | ||
|
|
a46d487e03 | ||
|
|
3deb6d3ab3 | ||
|
|
af34cdd5d2 | ||
|
|
6e1393235a | ||
|
|
343e0b54b9 | ||
|
|
ecb70cb6f7 | ||
|
|
ca50618af6 | ||
|
|
29c07ba83e | ||
|
|
45fbb83a9f | ||
|
|
ae7ba2df25 | ||
|
|
c3ef57cc32 | ||
|
|
7bb4ca5a14 | ||
|
|
063783d81d | ||
|
|
42116c9b65 | ||
|
|
a36e11973d | ||
|
|
5125568ea2 | ||
|
|
0fa164e50d | ||
|
|
cf814e81ee | ||
|
|
43a45f18ce | ||
|
|
ad51381063 | ||
|
|
0b0e4ce904 | ||
|
|
6a3e04d688 | ||
|
|
4107a17370 | ||
|
|
06b4d8f169 | ||
|
|
1c0c820746 | ||
|
|
d061403a28 | ||
|
|
5c092321a6 | ||
|
|
bdd3f61c1f | ||
|
|
8023557d6e | ||
|
|
074b0ced7a | ||
|
|
3864b1ac9b | ||
|
|
6e9b43457d | ||
|
|
ca1aec8920 | ||
|
|
acac580862 | ||
|
|
673e1b2980 | ||
|
|
f62157be72 | ||
|
|
f894ecf3b6 | ||
|
|
66dd4e28ad | ||
|
|
939dc1b0fb | ||
|
|
56bf5d38a1 | ||
|
|
d09b70b295 | ||
|
|
205180387a | ||
|
|
39c8cfeda5 | ||
|
|
f38a329be5 | ||
|
|
a0cd069539 | ||
|
|
bf306a2f01 | ||
|
|
c31f93a8d1 | ||
|
|
4730ab6309 | ||
|
|
1ae78ca98c | ||
|
|
d2379da478 | ||
|
|
0f64981b20 | ||
|
|
0002e49bb5 | ||
|
|
db13a60274 | ||
|
|
db0f11a359 | ||
|
|
ac7f43520b | ||
|
|
f67b9f5f6e | ||
|
|
c75156c4ce | ||
|
|
10270b5595 | ||
|
|
f7458572ed | ||
|
|
d57b7222b2 | ||
|
|
62e70a673a | ||
|
|
5e9eba6478 | ||
|
|
cb02dfe1a4 | ||
|
|
b50739e1af | ||
|
|
8da1b0212d | ||
|
|
ca1f2acb33 | ||
|
|
c15f966669 | ||
|
|
7705b8781a | ||
|
|
b2502746f0 | ||
|
|
ab68094386 | ||
|
|
bbec701223 | ||
|
|
b29d14e600 | ||
|
|
86e51c5cd1 | ||
|
|
cb8267be3f | ||
|
|
eaed43915c | ||
|
|
bd91fd2c38 | ||
|
|
1203b214cd | ||
|
|
c3fec15f11 | ||
|
|
0545653494 | ||
|
|
db2989bdb4 | ||
|
|
587bd00a19 | ||
|
|
960ff438e8 | ||
|
|
98e7ea85d3 | ||
|
|
2549e44710 | ||
|
|
4d32b563ca | ||
|
|
3a4b732977 | ||
|
|
500909a28e | ||
|
|
07753eb25b | ||
|
|
c6eaf3d010 | ||
|
|
6723fe8271 | ||
|
|
3348b70435 | ||
|
|
35a8527c16 | ||
|
|
7afc475290 | ||
|
|
789bceaa3a | ||
|
|
abbc043969 | ||
|
|
654e5762f1 | ||
|
|
507c3e3629 | ||
|
|
991dfeb2f2 | ||
|
|
26482fc2d3 | ||
|
|
e0ce6d9688 | ||
|
|
946595216a | ||
|
|
864b6bc56d | ||
|
|
6ea5b7581f | ||
|
|
f70b8f0c10 | ||
|
|
1593bcb537 | ||
|
|
bf7fc02c8d | ||
|
|
143702b92b | ||
|
|
c5ccc1a084 | ||
|
|
2ecb52a9b2 | ||
|
|
6439917cbe | ||
|
|
d21c18f657 | ||
|
|
25ef0039e4 | ||
|
|
e6981290bc | ||
|
|
75c3d8abbd | ||
|
|
d88683f498 | ||
|
|
40b9aa3a4c | ||
|
|
b6d1515d58 | ||
|
|
e01d4264e3 | ||
|
|
2117b65487 | ||
|
|
a7823b352f | ||
|
|
c543b62a08 | ||
|
|
3923b87f08 | ||
|
|
b7ecdadb83 | ||
|
|
5ff121e1ed | ||
|
|
f486e5448f | ||
|
|
c5aae98558 | ||
|
|
6d8a3b9897 | ||
|
|
6d98780e19 | ||
|
|
3ad2c46f3f | ||
|
|
a730cee7fd | ||
|
|
77c823c100 | ||
|
|
124f21c67a | ||
|
|
e46cf20dd3 | ||
|
|
4bef5e8313 | ||
|
|
22e93b0af4 | ||
|
|
5aeca9662b | ||
|
|
b996cf1f05 | ||
|
|
878a106877 | ||
|
|
45d36f86fd | ||
|
|
b108ae403a | ||
|
|
887ed66768 | ||
|
|
dac840a887 | ||
|
|
238de4ba8c | ||
|
|
9a7bdade43 | ||
|
|
aa84556204 | ||
|
|
6b68069fcd | ||
|
|
42c7034fb2 | ||
|
|
060c7e0145 | ||
|
|
b5b085dfb1 | ||
|
|
fc06ce9d7f | ||
|
|
d8d81b05a7 | ||
|
|
a60f42b1f2 | ||
|
|
6e18be88d0 | ||
|
|
b45e439c48 | ||
|
|
b87061c18c | ||
|
|
f78aca7752 | ||
|
|
3ccca2aa10 | ||
|
|
6d7c40eb76 | ||
|
|
da4cd7fb65 | ||
|
|
c97cda6b84 | ||
|
|
7a7fd4167a | ||
|
|
dffc1a43d5 | ||
|
|
36897fea1e | ||
|
|
c7b34735f0 | ||
|
|
5b07176c88 | ||
|
|
474b40d660 | ||
|
|
a62901b948 | ||
|
|
25d8746327 | ||
|
|
aff1698223 | ||
|
|
7f8941745f | ||
|
|
b858401098 | ||
|
|
d5a158b80f | ||
|
|
f315f284aa | ||
|
|
c367f5009d | ||
|
|
6db1e63bda | ||
|
|
e22ab2ede6 | ||
|
|
b7d7e0b682 | ||
|
|
96bba15f2f | ||
|
|
fcf965a595 | ||
|
|
e1a20d3c22 | ||
|
|
2abd7d8c5d | ||
|
|
5b8f73cdd7 | ||
|
|
7fd765421f | ||
|
|
d9d94af022 | ||
|
|
790b924e57 | ||
|
|
4a62f877df | ||
|
|
ac47c57bb7 | ||
|
|
3ace4199a1 | ||
|
|
e6bd7524c1 | ||
|
|
699c86e8c1 | ||
|
|
f40fa0ecea | ||
|
|
626f94686b | ||
|
|
752d13b1b1 | ||
|
|
54c0dc1b2b | ||
|
|
c5bc709898 | ||
|
|
ccdbb01513 | ||
|
|
5206d750ac | ||
|
|
a800e3df67 | ||
|
|
ccb1f87a20 | ||
|
|
c111da4681 | ||
|
|
9cc4e97a53 | ||
|
|
dca1c0b0f3 | ||
|
|
f06be6ed21 | ||
|
|
3c8ec2f42e | ||
|
|
7e193f7f52 | ||
|
|
7069b02929 | ||
|
|
66995db927 | ||
|
|
c36054ca1b | ||
|
|
3e07fbf3dc | ||
|
|
bf3fbe3e96 | ||
|
|
0a93d22bc8 | ||
|
|
f5b3d94d16 | ||
|
|
4d1a6994aa | ||
|
|
05c686782c | ||
|
|
85609ea742 | ||
|
|
20dabc0615 | ||
|
|
356dd9bc2b | ||
|
|
cd5d7534c4 | ||
|
|
b4f12fc933 | ||
|
|
cbea387ce0 | ||
|
|
345b155374 | ||
|
|
29d216950e | ||
|
|
321b04772c | ||
|
|
5b924aee98 | ||
|
|
46d44e3405 | ||
|
|
4d5332fe25 | ||
|
|
18bd4c54f4 | ||
|
|
31c7768ca0 | ||
|
|
6ec643e9d1 | ||
|
|
2b39f6f61c | ||
|
|
bf3ca13961 | ||
|
|
82026370ec | ||
|
|
6d49bf5346 | ||
|
|
67431d87fb | ||
|
|
fdf55221e6 | ||
|
|
07f277dd3b | ||
|
|
cf8f0603ca | ||
|
|
5592408ab8 | ||
|
|
a01617b45c | ||
|
|
7abb4087b3 | ||
|
|
dff15cf27a | ||
|
|
aa858137e5 | ||
|
|
45cb143202 | ||
|
|
7a9c6ab8c4 | ||
|
|
e2c26c292d | ||
|
|
be7c3fd00e | ||
|
|
7e5461a2cf | ||
|
|
6ee9010645 | ||
|
|
a23d5be056 | ||
|
|
97a6a1fdc2 | ||
|
|
c8f567347b | ||
|
|
74c1e7f69e | ||
|
|
15a5fc0cae | ||
|
|
f07c54d47c | ||
|
|
70446be108 | ||
|
|
d6d21fca56 | ||
|
|
8d7273924f | ||
|
|
ea64afbaa7 | ||
|
|
45da9837ec | ||
|
|
8c19b7d163 | ||
|
|
ab227a08d0 | ||
|
|
40d6e77964 | ||
|
|
9326e3f1b0 | ||
|
|
0e1eb3daf6 | ||
|
|
05daac12ed | ||
|
|
c5b24b4764 | ||
|
|
cc16548e5f | ||
|
|
291d65bb3e | ||
|
|
bd3ad03da6 | ||
|
|
5fa6788357 | ||
|
|
c5c5a98ac4 | ||
|
|
a1151143cf | ||
|
|
f5024984f7 | ||
|
|
f4880fd90d | ||
|
|
0ae61d5865 | ||
|
|
d3bd775a79 | ||
|
|
da546cfe7f | ||
|
|
a211933e83 | ||
|
|
1d40b5a821 | ||
|
|
33836daeb7 | ||
|
|
d921b0f6bd | ||
|
|
0607b95df6 | ||
|
|
0de6d0e046 | ||
|
|
98427345cf | ||
|
|
9fedaa9f77 | ||
|
|
bf4c2ecd33 | ||
|
|
f8c18cc1e0 | ||
|
|
458b900412 | ||
|
|
192c776e0b | ||
|
|
5cdec18863 | ||
|
|
15f856f951 | ||
|
|
01d52cef74 | ||
|
|
95563c8659 | ||
|
|
31d8c40eca | ||
|
|
56001ed272 | ||
|
|
d916fda04c | ||
|
|
cfae655068 | ||
|
|
5596565ec4 | ||
|
|
afa1aa5d93 | ||
|
|
e98c3d8393 | ||
|
|
6687b816f0 | ||
|
|
ea8035e854 | ||
|
|
54b0171d49 | ||
|
|
676d4277b9 | ||
|
|
a4b1da3ca2 | ||
|
|
9e9c16e770 | ||
|
|
dc87006fed | ||
|
|
b9b260f26a | ||
|
|
33fd6a5016 | ||
|
|
97cbccc2ba | ||
|
|
1ee4685d5d | ||
|
|
aba18232b1 | ||
|
|
0a02441b75 | ||
|
|
1be5b4c7ff | ||
|
|
a0ce0cf18a | ||
|
|
7c54e5d093 | ||
|
|
b825e51dab | ||
|
|
589855c393 | ||
|
|
4c546f2f53 | ||
|
|
3753fce912 | ||
|
|
4c02857ec5 | ||
|
|
33f87ff7d7 | ||
|
|
784dcf2a9a | ||
|
|
43ee943acb | ||
|
|
a769fd7d13 | ||
|
|
2c4fd00b16 | ||
|
|
264771fe98 | ||
|
|
ecd92dafef | ||
|
|
c8b6e4bea3 | ||
|
|
3756cb766e | ||
|
|
068d9ca60b | ||
|
|
93f632d8b8 | ||
|
|
bb44ce7e74 | ||
|
|
6986c8d8f7 | ||
|
|
fe95506db4 | ||
|
|
310ed76b18 | ||
|
|
98830d147f | ||
|
|
19c9177d7b | ||
|
|
f41c5f97f6 | ||
|
|
648c125697 | ||
|
|
0dc2b89897 | ||
|
|
83745f83a5 | ||
|
|
2f91fe4535 | ||
|
|
739f09059e | ||
|
|
c86f9f0f5f | ||
|
|
9470ca6bc5 | ||
|
|
2a92c4d5de | ||
|
|
bb6e892657 | ||
|
|
c9079b9299 | ||
|
|
b6963c1bf9 | ||
|
|
9c29df47bb | ||
|
|
fc146d3d00 | ||
|
|
1bf5a21678 | ||
|
|
011542dc2b | ||
|
|
489784104e | ||
|
|
3860634fd2 | ||
|
|
709c324e18 | ||
|
|
b75d24d92c | ||
|
|
ed80e9424c | ||
|
|
2fe1f2060a | ||
|
|
c6df820164 | ||
|
|
d6239822db | ||
|
|
bced9ffff9 | ||
|
|
d7d1c1544a | ||
|
|
7c1e8ce48c | ||
|
|
e3b0ca8ef6 | ||
|
|
9e266eb6d5 | ||
|
|
7231403e16 | ||
|
|
344a486fd7 | ||
|
|
4fd831875d | ||
|
|
0988d067ea | ||
|
|
44dbe475af | ||
|
|
bd24cf3ea4 | ||
|
|
b493a808fe | ||
|
|
54035d108d | ||
|
|
c5e8bc7e20 | ||
|
|
3bbb4779a3 | ||
|
|
1b3963ebea | ||
|
|
3b6dd7e15a | ||
|
|
757d2a3947 | ||
|
|
61b71143f2 | ||
|
|
1b343a36c9 | ||
|
|
8e94937060 | ||
|
|
e8ffebc006 | ||
|
|
2ca95eaa9f | ||
|
|
0dc5b4cdfc | ||
|
|
cc6cd96d8e | ||
|
|
4244d37625 | ||
|
|
0b766095d4 | ||
|
|
a4f212a18f | ||
|
|
caafb73190 | ||
|
|
09482799c9 | ||
|
|
37f93d1760 | ||
|
|
725f2e5204 | ||
|
|
967198fae0 | ||
|
|
43d57f6dcb | ||
|
|
6afa4db577 | ||
|
|
3b8c3fb29a | ||
|
|
921c3b0627 | ||
|
|
c0fadb45ab | ||
|
|
a1481fb179 | ||
|
|
987cd972d3 | ||
|
|
bdf25976a3 | ||
|
|
87c3aff4ce | ||
|
|
99350a957a | ||
|
|
319068dc7e | ||
|
|
cd18806c39 | ||
|
|
95b08b2023 | ||
|
|
0e70f76c86 | ||
|
|
4d414a2994 | ||
|
|
3d22772d4e | ||
|
|
0b381e2570 | ||
|
|
f2cc4311c5 | ||
|
|
e349671fdf | ||
|
|
01c02d5efa | ||
|
|
b62b1f3870 | ||
|
|
8844830859 | ||
|
|
0c51ee4b64 | ||
|
|
11920d5e31 | ||
|
|
848ea1eb63 | ||
|
|
a216519486 | ||
|
|
b04606c38e | ||
|
|
38072beea7 | ||
|
|
b843f1fa03 | ||
|
|
560d40e571 | ||
|
|
5f0b8161b7 | ||
|
|
062d482917 | ||
|
|
39693a27e3 | ||
|
|
7cd1eeac30 | ||
|
|
bafa473c8e | ||
|
|
750cf46b2e | ||
|
|
68885a4bbc | ||
|
|
bcc99a8904 | ||
|
|
59fbd98db3 | ||
|
|
b70ed425f1 | ||
|
|
45ef5811c8 | ||
|
|
3b137ac762 | ||
|
|
1ddb0caf73 | ||
|
|
ae4c6fe2dd | ||
|
|
b03fe438d0 | ||
|
|
db257af58e | ||
|
|
735368c71b | ||
|
|
9e04e3679b | ||
|
|
43b8414727 | ||
|
|
5a00187147 | ||
|
|
cb525c7c84 | ||
|
|
d88420dd03 | ||
|
|
b9a983f8e0 | ||
|
|
42431ea7db | ||
|
|
f9459e4abb | ||
|
|
72f917d611 | ||
|
|
9fd1d19e93 | ||
|
|
062af1ac08 | ||
|
|
41bd76e091 | ||
|
|
cfd3f4b199 | ||
|
|
79d38f9597 | ||
|
|
b3866559e1 | ||
|
|
4d186baa35 | ||
|
|
8ed3d5f3db | ||
|
|
f0c8f39b6d | ||
|
|
431db8fc9b | ||
|
|
ba252c5356 | ||
|
|
a2812c39c0 | ||
|
|
0490758820 | ||
|
|
7f56824b42 | ||
|
|
627da3a2bc | ||
|
|
9b36a5c8a6 | ||
|
|
c1cf2be533 | ||
|
|
e6b69042de | ||
|
|
109650faf3 | ||
|
|
e54eaab842 | ||
|
|
43b6297b5d | ||
|
|
c20f4f5adf | ||
|
|
dc1f222cd2 | ||
|
|
c2b687212c | ||
|
|
849913276d | ||
|
|
23579c1e4a | ||
|
|
e031161fd4 | ||
|
|
4800ee6c0a | ||
|
|
d3a7fef9b0 | ||
|
|
40822fe77a | ||
|
|
837b670213 | ||
|
|
57ce69f3fb | ||
|
|
be022c4894 | ||
|
|
8a366964bb | ||
|
|
ee86b68470 | ||
|
|
60352307aa | ||
|
|
3ebd2f746f | ||
|
|
1c1a65b637 | ||
|
|
010e60d029 | ||
|
|
7a25568861 | ||
|
|
5f4f913661 | ||
|
|
ccd0e34a53 | ||
|
|
72f1ffccd3 | ||
|
|
ea7a52945f | ||
|
|
89d4d1351a | ||
|
|
b757c91d93 | ||
|
|
27203d7a4d | ||
|
|
9ad4e18ac5 | ||
|
|
fcdc8f3ce7 | ||
|
|
78b994b84a | ||
|
|
58bfc677e2 | ||
|
|
7d17285a0c | ||
|
|
e9eb00a0d4 | ||
|
|
48d07af574 | ||
|
|
2fc62efd88 | ||
|
|
be516d75bd | ||
|
|
951d5fde85 | ||
|
|
1389abc052 | ||
|
|
19ad67a77f | ||
|
|
641f308344 | ||
|
|
9f097fa4d5 | ||
|
|
5ad362c52b | ||
|
|
614f238a61 | ||
|
|
dec91950bc | ||
|
|
6cef9c23f0 | ||
|
|
3f568bf136 | ||
|
|
5484b421ce | ||
|
|
02f21e07d3 | ||
|
|
fff1f23a83 | ||
|
|
a056ec0d38 | ||
|
|
2eb9e5dde3 | ||
|
|
627d2a4701 | ||
|
|
76895fe86d | ||
|
|
64c3c85780 | ||
|
|
7288348857 | ||
|
|
62e73299b1 | ||
|
|
fe76c41ed8 | ||
|
|
1a92edf8be | ||
|
|
b63b606a4e | ||
|
|
8e2ef3d22b | ||
|
|
c6c4a32283 | ||
|
|
b70b3b158e | ||
|
|
3d59ab8108 | ||
|
|
b6c3089510 | ||
|
|
bd92aac280 | ||
|
|
5299e802e9 | ||
|
|
8e5a57d7dd | ||
|
|
beaa324fb6 | ||
|
|
79e64fe206 | ||
|
|
93f525e3fe | ||
|
|
aacb803c64 | ||
|
|
8a0665b222 | ||
|
|
20e41a7f73 | ||
|
|
93a1699a35 | ||
|
|
c33c07e4af | ||
|
|
c7484d0cc9 | ||
|
|
fb85a7bb35 | ||
|
|
42ff9a4d34 | ||
|
|
005e9eae7c | ||
|
|
3e325debcc | ||
|
|
a221de9a2b | ||
|
|
32b0cc1865 | ||
|
|
bbf85f8a12 | ||
|
|
67a0172b28 | ||
|
|
fb19d4d45b | ||
|
|
a156b1af14 | ||
|
|
a604b4943c | ||
|
|
3f0b6435d9 | ||
|
|
e0f029e2cb | ||
|
|
89d3fd5fab | ||
|
|
a38b00be6b | ||
|
|
0e8d52b591 | ||
|
|
298c77740d | ||
|
|
c681aae8ee | ||
|
|
faef98b089 | ||
|
|
84a3e0a30b | ||
|
|
69bd553ce0 | ||
|
|
fd0c0f8975 | ||
|
|
860ceb06b4 | ||
|
|
ecf501bf72 | ||
|
|
81a2ed1e25 | ||
|
|
76ab28338a | ||
|
|
9a56c9630f | ||
|
|
53b9497c18 | ||
|
|
750b16b6ee | ||
|
|
0ee3e0779a | ||
|
|
333c2d9299 | ||
|
|
ad37ff5048 | ||
|
|
33f86f3bde | ||
|
|
8acb969a49 | ||
|
|
b74b5933b8 | ||
|
|
681c556b7e | ||
|
|
1746684e52 | ||
|
|
0b93d06555 | ||
|
|
8a8b8c7c27 | ||
|
|
6b6577006d | ||
|
|
23ee5e81c9 | ||
|
|
483f55e4b1 | ||
|
|
1bb1bc2553 | ||
|
|
a4e4e36f94 | ||
|
|
6849415812 | ||
|
|
86f6cb038e | ||
|
|
7480a1d6ce | ||
|
|
3cd10117dd | ||
|
|
0caf19d390 | ||
|
|
5c14ebb049 | ||
|
|
9717a736b1 | ||
|
|
9c9ab50d1a | ||
|
|
d4bcb8174e | ||
|
|
9e7fe773bd | ||
|
|
aca18fab0f | ||
|
|
691de01b79 | ||
|
|
3383f15142 | ||
|
|
84c1593889 | ||
|
|
3c80fa1e33 | ||
|
|
06b16a1deb | ||
|
|
4c4246fb09 | ||
|
|
364be1e9f6 | ||
|
|
f959ed71aa | ||
|
|
5c4326c302 | ||
|
|
125fc3a622 | ||
|
|
6b9e785db3 | ||
|
|
25d34e9a43 | ||
|
|
457d4aa1dc | ||
|
|
ff0c0992ff | ||
|
|
d379e012c4 | ||
|
|
151fff26fd | ||
|
|
3d0d561215 | ||
|
|
22d586ed7b | ||
|
|
6dc19b29e8 | ||
|
|
50975a87d4 | ||
|
|
ce721d9f0f | ||
|
|
20510a33f7 | ||
|
|
3abd9c8763 | ||
|
|
e9eff7420b | ||
|
|
64c250c9d8 | ||
|
|
8047f82bfd | ||
|
|
af6467fb3d | ||
|
|
3ff1664aec | ||
|
|
34ea2b44b8 | ||
|
|
6c8d851109 | ||
|
|
d678299a74 | ||
|
|
7aed0db2b6 | ||
|
|
0355524345 | ||
|
|
0a43e4672e | ||
|
|
71e0ccdfec | ||
|
|
1df33ac3c8 | ||
|
|
7334090ac1 | ||
|
|
6b0f044198 | ||
|
|
ddf54c9cf8 | ||
|
|
7c64e184e2 | ||
|
|
a904db033c | ||
|
|
b234856b02 | ||
|
|
89d51d2afc | ||
|
|
37cb9678e9 | ||
|
|
0500ff333a | ||
|
|
08528510ef | ||
|
|
ddbd03dc1e | ||
|
|
ade87f378a | ||
|
|
4db14b905f | ||
|
|
b669b31451 | ||
|
|
1cb2b62f81 | ||
|
|
e5828713cf | ||
|
|
d10cb84068 | ||
|
|
4222f8516f | ||
|
|
7f998c7611 | ||
|
|
db46000337 | ||
|
|
1aac8d8041 | ||
|
|
c59c8e05f7 | ||
|
|
4942d0a629 | ||
|
|
873b7715f4 | ||
|
|
98e7ed6920 | ||
|
|
046f5e645e | ||
|
|
f5e5a7094c | ||
|
|
154125fee6 | ||
|
|
9f8e960ebe | ||
|
|
4179b0be0a | ||
|
|
28bafa38db | ||
|
|
b07552565e | ||
|
|
c4427471d2 | ||
|
|
08f81c6784 | ||
|
|
a471e98aca | ||
|
|
75a8fcc8a0 | ||
|
|
46ef76c168 | ||
|
|
66637446c9 | ||
|
|
21efeb888a | ||
|
|
a4ee8b5322 | ||
|
|
36519ac47e | ||
|
|
3f514fceca | ||
|
|
c2249fdfac | ||
|
|
c610719a44 | ||
|
|
36a6c2461a | ||
|
|
c29f22c39e | ||
|
|
30d3062944 | ||
|
|
69ba75abf4 | ||
|
|
e4d486fec5 | ||
|
|
f242144dcf | ||
|
|
02dee2d664 | ||
|
|
a3dd2c3069 | ||
|
|
a23425e8aa | ||
|
|
be79ddc9a3 | ||
|
|
7d71015e8c | ||
|
|
ad54549b51 | ||
|
|
6cf032a164 | ||
|
|
6390d796ac | ||
|
|
98b8411905 | ||
|
|
ddf1029afa | ||
|
|
1effbc5cc9 | ||
|
|
414b645e9f | ||
|
|
398c76f496 | ||
|
|
1bc456dd95 | ||
|
|
2e8421884e | ||
|
|
70d9b193ac | ||
|
|
b49c11004a | ||
|
|
34843eea90 | ||
|
|
2d6d7f31e8 | ||
|
|
7a24cbff1c | ||
|
|
1e7eb2cf1c | ||
|
|
361256e016 | ||
|
|
8838dbd003 | ||
|
|
13a95e1f2b | ||
|
|
1aaa451a3e | ||
|
|
cbba81e54d | ||
|
|
370868dfac | ||
|
|
77f692aae2 | ||
|
|
9318e205ea | ||
|
|
ebcc717c19 | ||
|
|
4c16b564ee | ||
|
|
e2283d1453 | ||
|
|
d891801c5a | ||
|
|
de75386944 | ||
|
|
82dc37de50 | ||
|
|
b6fa7f62dc | ||
|
|
f9e0a95c5e | ||
|
|
b2c6e12647 | ||
|
|
caffb83780 | ||
|
|
8882cb5479 | ||
|
|
75dace2dee | ||
|
|
ad6487d042 | ||
|
|
a91604e8ab | ||
|
|
c364f7c643 | ||
|
|
53435ba184 | ||
|
|
25f8d5519b | ||
|
|
2e4fef6c66 | ||
|
|
80b2b7dc00 | ||
|
|
8585cd8e21 | ||
|
|
9fa2a7eeea | ||
|
|
2d1f74228d | ||
|
|
3d6f7aa0e1 | ||
|
|
3dea60366a | ||
|
|
d4d9a1df4c | ||
|
|
7d6975fd31 | ||
|
|
08be52ed17 | ||
|
|
682a7700c2 | ||
|
|
9d87009216 | ||
|
|
ef86838f62 | ||
|
|
35468233f8 | ||
|
|
26e229867d | ||
|
|
3a1578b3c6 | ||
|
|
d5e3d2cbbc | ||
|
|
c095248176 | ||
|
|
44601c8954 | ||
|
|
135dbb8f07 | ||
|
|
c95682a0c7 | ||
|
|
d177b9f7fa | ||
|
|
9b57615d94 | ||
|
|
c03f3eacd1 | ||
|
|
a26e395932 | ||
|
|
0870b87c96 | ||
|
|
b52a44a7dd | ||
|
|
0a290aafef | ||
|
|
9014d4c410 | ||
|
|
60e58b4f5f | ||
|
|
620e74a6aa | ||
|
|
efa287ed35 | ||
|
|
a24eb9d9b0 | ||
|
|
bd3dab8aae | ||
|
|
4fe1ebaa5b | ||
|
|
c5e944744b | ||
|
|
0c396181f7 | ||
|
|
0034474219 | ||
|
|
8136ad8287 | ||
|
|
681940d466 | ||
|
|
16488506e8 | ||
|
|
122fccc041 | ||
|
|
9d0ad35403 | ||
|
|
f9ec97e026 | ||
|
|
95495a2647 | ||
|
|
e3310a605c | ||
|
|
b55719bf28 | ||
|
|
b957b51279 | ||
|
|
90bcfab369 | ||
|
|
f8a8e30641 | ||
|
|
25cb98e7a7 | ||
|
|
03e1bb7cf9 | ||
|
|
85dbb24f3a | ||
|
|
d817635782 | ||
|
|
2f4f237810 | ||
|
|
5ac94d810f | ||
|
|
39dc46dc25 | ||
|
|
0d9cf725f7 | ||
|
|
e55dbead5b | ||
|
|
7d046e5b30 | ||
|
|
8b4693cf66 | ||
|
|
a1172c9a82 | ||
|
|
1ed2bd33f0 | ||
|
|
4c159bd0ba | ||
|
|
050654b2a9 | ||
|
|
61b261e1b2 | ||
|
|
017b010206 | ||
|
|
00f5189f58 | ||
|
|
4a8309ed1f | ||
|
|
76cfc31a1d | ||
|
|
d9ec434699 | ||
|
|
239f3c40be | ||
|
|
09c8c6e670 | ||
|
|
7e4ad01c94 | ||
|
|
ed98e269ef | ||
|
|
b47d63334f | ||
|
|
5e2a3a5aea | ||
|
|
1a7eb21fc7 | ||
|
|
834a51cdc9 | ||
|
|
1b69d99c06 | ||
|
|
ad189933c6 | ||
|
|
9d86ff32de | ||
|
|
278bb57a58 | ||
|
|
0ba494e0ba | ||
|
|
8b247054bb | ||
|
|
7c5c8e4e0d | ||
|
|
ad106a27f3 | ||
|
|
9d6f61b49e | ||
|
|
02368954a0 | ||
|
|
b477a35a01 | ||
|
|
16622887de | ||
|
|
9059d1fb17 | ||
|
|
df2b008d82 | ||
|
|
0da871efd0 | ||
|
|
1c55349f81 | ||
|
|
9309fa1e81 | ||
|
|
5996189f91 | ||
|
|
bd2b984bfb | ||
|
|
194409a117 | ||
|
|
27978b216d | ||
|
|
c38fa77ce6 | ||
|
|
3eb49f7422 | ||
|
|
1989d615d2 | ||
|
|
239412d265 | ||
|
|
375a419a9e | ||
|
|
875c8ab424 | ||
|
|
c9bfc810ce | ||
|
|
46ecb16949 | ||
|
|
f6dc16f17b | ||
|
|
4eef42f730 | ||
|
|
8612d9a771 | ||
|
|
0caff054f5 | ||
|
|
4aa91ad599 | ||
|
|
7a0864f5c2 | ||
|
|
73dc0dfcf6 | ||
|
|
1ff9a69339 | ||
|
|
179eb5d847 | ||
|
|
52c868828c | ||
|
|
7eea4615b6 | ||
|
|
d9b351df1a | ||
|
|
d6a785b645 | ||
|
|
79db828a01 | ||
|
|
a5ffb0f8dc | ||
|
|
9492fcde74 | ||
|
|
d2456ce4cd | ||
|
|
7de27abc8d | ||
|
|
d8155bc8eb | ||
|
|
cf08e52a92 | ||
|
|
768398b991 | ||
|
|
24c20a19f1 | ||
|
|
8fbcbcd4c0 | ||
|
|
e0da5bb943 | ||
|
|
36fbc4fb82 | ||
|
|
cb11051f42 | ||
|
|
a824781d14 | ||
|
|
600a2c6748 | ||
|
|
77df64bfb5 | ||
|
|
2d6e54903c | ||
|
|
baa2b83df9 | ||
|
|
1ff02446af | ||
|
|
b58c6ba762 | ||
|
|
611a902000 | ||
|
|
c1b3f9dd29 | ||
|
|
7c5a88a6a6 | ||
|
|
be9abfef58 | ||
|
|
b549c9377e | ||
|
|
a5b00dbf74 | ||
|
|
90e2e14cd7 | ||
|
|
14bb245424 | ||
|
|
b63a0f3a45 | ||
|
|
e1f8842d7f | ||
|
|
3dda5fb268 | ||
|
|
248e0c5240 | ||
|
|
0297a43de6 | ||
|
|
2b4f66e0cf | ||
|
|
e622af2cc3 | ||
|
|
f527b1b5a6 | ||
|
|
c15b13a107 | ||
|
|
bc06acdd25 | ||
|
|
5252870733 | ||
|
|
3cac6a47a5 | ||
|
|
49bba9bf98 | ||
|
|
f4d12e4e5e | ||
|
|
d305211a36 | ||
|
|
9ec44d6f97 | ||
|
|
175bb3ee01 | ||
|
|
036c78750f | ||
|
|
a18de9de7d | ||
|
|
59fbbd5987 | ||
|
|
7e89fbc907 | ||
|
|
0956f240b3 | ||
|
|
f9db97c6b0 | ||
|
|
a2443c4ac1 | ||
|
|
095bd95044 | ||
|
|
b569209647 | ||
|
|
9057cac2b9 | ||
|
|
f9a6c685df | ||
|
|
208eb4f454 | ||
|
|
b3cb9e6714 | ||
|
|
5f9233f9b7 | ||
|
|
16447ae597 | ||
|
|
103edd5260 | ||
|
|
928089bf0f | ||
|
|
e5bd74695a | ||
|
|
f796969465 | ||
|
|
10756175b7 | ||
|
|
5637a71486 | ||
|
|
bcebd0fb62 | ||
|
|
3817d3ca87 | ||
|
|
4dd714e814 | ||
|
|
61e8bb49ec | ||
|
|
103dcd3761 | ||
|
|
54ac135fc8 | ||
|
|
86582809fc | ||
|
|
974d648f19 | ||
|
|
a79afc9597 | ||
|
|
e4883241d9 | ||
|
|
babf223745 | ||
|
|
c7d91730b6 | ||
|
|
71246b65c9 | ||
|
|
50076b647e | ||
|
|
a1a788dce8 | ||
|
|
a611b4f346 | ||
|
|
7f6ed674b4 | ||
|
|
aa3cfd887a | ||
|
|
2649d46d8d | ||
|
|
e23ffe6f02 | ||
|
|
96f3c3729a | ||
|
|
11e9d47ce2 | ||
|
|
efbc8e4383 | ||
|
|
bc7404409f | ||
|
|
8677d70baf | ||
|
|
f39253f0e1 | ||
|
|
68c1957267 | ||
|
|
a275aa2e4d | ||
|
|
cadbac9948 | ||
|
|
82673e8ddd | ||
|
|
bee51024b3 | ||
|
|
3437cb73ec | ||
|
|
d01d1a8520 | ||
|
|
5aa842cf66 | ||
|
|
03282dee0f | ||
|
|
98e8ecb8e2 | ||
|
|
9451dc3fd4 | ||
|
|
e1d3759f55 | ||
|
|
0ec382c86b | ||
|
|
756087c9f1 | ||
|
|
3e7c47e873 | ||
|
|
e3ffdbc308 | ||
|
|
645cace4d6 | ||
|
|
0959d5986b | ||
|
|
89605c29a7 | ||
|
|
e527f31213 | ||
|
|
a0dbd99928 | ||
|
|
17d39c7a4a | ||
|
|
54edaebbd9 | ||
|
|
d587a6f64c | ||
|
|
2371c32be5 | ||
|
|
c9abb8352c | ||
|
|
8995e62e73 | ||
|
|
316147a8db | ||
|
|
1fdcfc7a30 | ||
|
|
8e2c633cd4 | ||
|
|
786b0e4a54 | ||
|
|
c38c1c3c35 | ||
|
|
7d856756f4 | ||
|
|
f0d1d365e0 | ||
|
|
8e2d666ff8 | ||
|
|
38d7be1d5f | ||
|
|
431e2fad72 | ||
|
|
b3b63be8fc | ||
|
|
071fc7d6ef | ||
|
|
2a37f7edac | ||
|
|
c656ad5e2c | ||
|
|
da14a89490 | ||
|
|
cf22eae467 | ||
|
|
b199bddb0b | ||
|
|
2188ea82de | ||
|
|
1fa13d0177 | ||
|
|
ed508af424 | ||
|
|
5df26864d5 | ||
|
|
837111b17e | ||
|
|
a6b363b433 | ||
|
|
2807e1e892 | ||
|
|
0a2abd8214 | ||
|
|
8beb7acdb1 | ||
|
|
466c80b94d | ||
|
|
36c0cfc9a9 | ||
|
|
35ba1b3345 | ||
|
|
d00821d1c7 | ||
|
|
6c1b3f242b | ||
|
|
9f9da1e0c9 | ||
|
|
14fb4b70bd | ||
|
|
b1049540a4 | ||
|
|
5e2909df33 | ||
|
|
c122dad21f | ||
|
|
48ae686602 | ||
|
|
bf2c3a1a81 | ||
|
|
96e7a93886 | ||
|
|
dba1ed1e19 | ||
|
|
a24514876b | ||
|
|
466a1c1c41 | ||
|
|
a2d5e9f40f | ||
|
|
1bbff1d161 | ||
|
|
0948bae99b | ||
|
|
850db41596 | ||
|
|
7bafc87e2b | ||
|
|
1a0de02a15 | ||
|
|
6d5d278624 | ||
|
|
3b4cc48fa0 | ||
|
|
c908461088 | ||
|
|
53d1398d30 | ||
|
|
782c0367d0 | ||
|
|
4678222e9b | ||
|
|
f71dc3e4be | ||
|
|
f6233893bd | ||
|
|
6427bcf130 | ||
|
|
8fa41b706c | ||
|
|
4706c4438d | ||
|
|
0c8ebc2b06 | ||
|
|
b3b5ebc2ca | ||
|
|
b8aa23ccc5 | ||
|
|
364843db29 | ||
|
|
aa56c8f7e6 | ||
|
|
8e9fd27058 | ||
|
|
b75908cb2a | ||
|
|
af6df49ce1 | ||
|
|
bd3bdb5769 | ||
|
|
98fe193b21 | ||
|
|
26cbc9e8b1 | ||
|
|
ebb8c43fd0 | ||
|
|
8c7344f1c4 | ||
|
|
5c32a17787 | ||
|
|
aff520e69a | ||
|
|
45e627c33c | ||
|
|
7a1b158f83 | ||
|
|
6374c5d49d | ||
|
|
fd460b19d4 | ||
|
|
dff7cc4ca5 | ||
|
|
d013320bec | ||
|
|
fc6dcfaf21 | ||
|
|
a001270bd2 | ||
|
|
9e67883fbd | ||
|
|
f1a448708c | ||
|
|
a4bfa96502 | ||
|
|
595b83a256 | ||
|
|
8d34f77321 | ||
|
|
67095f97b1 | ||
|
|
50740c94ab | ||
|
|
4db4cfeda2 | ||
|
|
ad13cef89c | ||
|
|
855fc6fcd1 | ||
|
|
8f12244e51 | ||
|
|
fe0213465c | ||
|
|
f984047004 | ||
|
|
19e9e2d090 | ||
|
|
7fe3b97d00 | ||
|
|
9cd243da47 | ||
|
|
e43208c2e9 | ||
|
|
dc016fc22f | ||
|
|
c6f037cae2 | ||
|
|
f049830e28 | ||
|
|
dd1995ae0b | ||
|
|
23dc233569 | ||
|
|
0977aa7d0d | ||
|
|
24862b0672 | ||
|
|
f05a57efc3 | ||
|
|
65331a9d7c | ||
|
|
f7ae287e40 | ||
|
|
45f380b1f6 | ||
|
|
9e6b329df4 | ||
|
|
43cd34d94c | ||
|
|
9fa00aff9a | ||
|
|
9a56dcb1be | ||
|
|
fdfe7bbe59 | ||
|
|
3a99a60792 | ||
|
|
fa2b4e14df | ||
|
|
35322a6900 | ||
|
|
2ccf29d61e | ||
|
|
b068013343 | ||
|
|
d839e72998 | ||
|
|
d7c9a8ed29 | ||
|
|
6837d4d692 | ||
|
|
8aba83735b | ||
|
|
aa51187747 | ||
|
|
5f07a9ae95 | ||
|
|
a2ca767bf4 | ||
|
|
5806c74e7c | ||
|
|
0481e1d45e | ||
|
|
3177b61421 | ||
|
|
6009cf5dfa | ||
|
|
0a970e8c31 | ||
|
|
aa276ca6af | ||
|
|
9f02dd13ff | ||
|
|
609e723322 | ||
|
|
c564a1d53e | ||
|
|
a7fe31f28b | ||
|
|
a84dc599d6 | ||
|
|
8da029add9 | ||
|
|
ba45a2d270 | ||
|
|
cb56b22aea | ||
|
|
23cc5b31ba | ||
|
|
e8d99f0460 | ||
|
|
6bcd10cd5c | ||
|
|
619fb20c5f | ||
|
|
386a312e96 | ||
|
|
2759d347e6 | ||
|
|
b6ec327b49 | ||
|
|
ee02d622ba | ||
|
|
5c4a6083f5 | ||
|
|
49e63a3d3d | ||
|
|
6bae9dc9ed | ||
|
|
5fa1979a46 | ||
|
|
b40d4fa315 | ||
|
|
4d2ff7cd5b | ||
|
|
d8ec0e64d0 | ||
|
|
82e979cc07 | ||
|
|
8c132a51f5 | ||
|
|
40bd372cc1 | ||
|
|
212e114270 | ||
|
|
b0e9de6951 | ||
|
|
3489522bbb | ||
|
|
96237abc03 | ||
|
|
7155b4f0ac | ||
|
|
a8b2b09e0f | ||
|
|
6858b8c555 | ||
|
|
0e493b1a0e | ||
|
|
37d478f970 | ||
|
|
7d0d42a49f | ||
|
|
0eb1684ef1 | ||
|
|
9b0b723143 | ||
|
|
532bc6e1e6 | ||
|
|
fe3ed4c454 | ||
|
|
b5ec89e586 | ||
|
|
895e7397c2 | ||
|
|
59b767957a | ||
|
|
17d4bf8f22 | ||
|
|
836be3b097 | ||
|
|
310415bea9 | ||
|
|
aafc1276a9 | ||
|
|
2993e794cc | ||
|
|
58cb9cfb2d | ||
|
|
fbdf0901d5 | ||
|
|
af8c81b621 | ||
|
|
06b5275e48 | ||
|
|
ad95572d5f | ||
|
|
0021cfc4bc | ||
|
|
aebc7850f4 | ||
|
|
1b7efbc607 | ||
|
|
3800e96d14 | ||
|
|
461f1bb07c | ||
|
|
7d4c07e4f6 | ||
|
|
31b788f463 | ||
|
|
96ab761f73 | ||
|
|
2b3f05c039 | ||
|
|
f2e8303b66 | ||
|
|
2a614b545b | ||
|
|
5c0ab21f68 | ||
|
|
689d109438 | ||
|
|
2a6934b283 | ||
|
|
760cb94e9a | ||
|
|
2a6cff0013 | ||
|
|
ce578f0417 | ||
|
|
1745bdb9e2 | ||
|
|
3f90b89c3c | ||
|
|
f343e40d15 | ||
|
|
5cc4be9e65 | ||
|
|
da5aada002 | ||
|
|
07f2ee9ad9 | ||
|
|
12f4e1146f | ||
|
|
92c57e5476 | ||
|
|
a923baacd8 | ||
|
|
999b094d55 | ||
|
|
d4213f2352 | ||
|
|
3f65c9a066 | ||
|
|
1d427e2645 | ||
|
|
36414c4b00 | ||
|
|
47e253d76c | ||
|
|
b73cf84df0 | ||
|
|
a5b885a774 | ||
|
|
0c785413da | ||
|
|
482d7ef5f7 | ||
|
|
9f9073c0ff | ||
|
|
ef05ff4abd | ||
|
|
5848aae435 | ||
|
|
fb06f33de0 | ||
|
|
0d7ddb149e | ||
|
|
4f2d7b9c4e | ||
|
|
c02ed96f6f | ||
|
|
3b2ac891b2 | ||
|
|
ef0108881b | ||
|
|
af48975a6b | ||
|
|
6441b149ab | ||
|
|
f8892881f8 | ||
|
|
228aec5401 | ||
|
|
68ad48ff55 | ||
|
|
541ba64032 | ||
|
|
2d870b798c | ||
|
|
0f1fe1ab63 | ||
|
|
73cc86ddb1 | ||
|
|
23128f4be2 | ||
|
|
92200d0e82 | ||
|
|
d6e8655792 | ||
|
|
37076d7920 | ||
|
|
78347ec91b | ||
|
|
9ded102a0a | ||
|
|
59b7d8b8cb | ||
|
|
f5b97f6762 | ||
|
|
d47da241af | ||
|
|
4611ce15eb | ||
|
|
aa8c56a688 | ||
|
|
ef44d4471a | ||
|
|
5581eae957 | ||
|
|
ec46dfaac9 | ||
|
|
6042a047bd | ||
|
|
6ca9e2a753 | ||
|
|
618eabfe5c | ||
|
|
bb5db2e9d0 | ||
|
|
97e4d169b3 | ||
|
|
50e44b1473 | ||
|
|
38588dd3fa | ||
|
|
d183388347 | ||
|
|
1e69d59384 | ||
|
|
00f008f94d | ||
|
|
3c28001a74 | ||
|
|
76a6218be6 | ||
|
|
6c1de1bbd6 | ||
|
|
d7678081da | ||
|
|
5e4ba563cb | ||
|
|
8afbe77b0a | ||
|
|
2ef139b59a | ||
|
|
1f0d2d9b89 | ||
|
|
37a1f144ab | ||
|
|
9a7a654596 | ||
|
|
9abccd63cf | ||
|
|
93fea77182 | ||
|
|
19797243f6 | ||
|
|
c9c733d925 | ||
|
|
a7d7678c78 | ||
|
|
c0911921c7 | ||
|
|
4a4241d57a | ||
|
|
c9426bb6eb | ||
|
|
db4abd169a | ||
|
|
80b6958599 | ||
|
|
80058c781a | ||
|
|
44bd2e36f3 | ||
|
|
3589a5e5be | ||
|
|
13ef033f0e | ||
|
|
3f8c68bbca | ||
|
|
4275cea82b | ||
|
|
a0bcb5339a | ||
|
|
43deec4a4b | ||
|
|
2bc433a30b | ||
|
|
eb2b395932 | ||
|
|
2bfd1c0bf2 | ||
|
|
7228c4b13f | ||
|
|
9351d7471f | ||
|
|
1cf49998bc | ||
|
|
6ae86597e8 | ||
|
|
c578ff25bd | ||
|
|
2934a3e3be | ||
|
|
ceaa69da75 | ||
|
|
fa8e731576 | ||
|
|
685c0a106a | ||
|
|
7f539090dd | ||
|
|
2089273f95 | ||
|
|
838bb4c7ad | ||
|
|
637acd1a12 | ||
|
|
03fa9a847f | ||
|
|
d488c88e78 | ||
|
|
baae842210 | ||
|
|
ec1fb838b6 | ||
|
|
13281179df | ||
|
|
276a42c9a1 | ||
|
|
7a70a730ba | ||
|
|
d0fe59631c | ||
|
|
106892e933 | ||
|
|
19543a41b3 | ||
|
|
b172b760ab | ||
|
|
4b5d49cb41 | ||
|
|
3fd35b6058 | ||
|
|
5f86c4ab99 | ||
|
|
c94a7f6629 | ||
|
|
7d6beb4141 | ||
|
|
e2117e690a | ||
|
|
fb791290e2 | ||
|
|
5dd1488b5d | ||
|
|
529cd64d82 | ||
|
|
d2bd3e8da8 | ||
|
|
e42ce7dd86 | ||
|
|
40709462ee | ||
|
|
2ad6c01a4d | ||
|
|
70c12e788e | ||
|
|
1713791c90 | ||
|
|
9aa23fd412 | ||
|
|
e4ba09cd93 | ||
|
|
171fdf1fbc | ||
|
|
01f4e0b961 | ||
|
|
be2d5a91c7 | ||
|
|
a1d89d9478 | ||
|
|
98d1dc3b65 | ||
|
|
b80eb3acc0 | ||
|
|
05ccc1995b | ||
|
|
0de244889e | ||
|
|
e6c5c3a493 | ||
|
|
164aa2ccd2 | ||
|
|
f1599e26b3 | ||
|
|
ed64a4d32d | ||
|
|
2ee4b431d4 | ||
|
|
cd8a73ed19 | ||
|
|
e6c985ce4e | ||
|
|
a20446aeb9 | ||
|
|
7b23d76559 | ||
|
|
8315cf5818 | ||
|
|
ed16265bde | ||
|
|
dff205faf6 | ||
|
|
9aae8aee0c | ||
|
|
7c818ced2b | ||
|
|
218e887558 | ||
|
|
a68860b35a | ||
|
|
82d4d43383 | ||
|
|
94618e8feb | ||
|
|
55de7d4494 | ||
|
|
7ed639f741 | ||
|
|
41f2870c29 | ||
|
|
ba198490fa | ||
|
|
0f9ab082ab | ||
|
|
97b58965f2 | ||
|
|
f2566c68e3 | ||
|
|
a456bf5449 | ||
|
|
a09998f910 | ||
|
|
be662b913c | ||
|
|
e7ddc8448d | ||
|
|
29374f8d8a | ||
|
|
359b971103 | ||
|
|
fbdb1ae208 | ||
|
|
22c13c1eff | ||
|
|
5fc63aeaf1 | ||
|
|
d4f32673ab | ||
|
|
480dffb51b | ||
|
|
966df00124 | ||
|
|
3e2b4bc727 | ||
|
|
5929a8d42b | ||
|
|
f8ab40eb39 | ||
|
|
55e9233b93 | ||
|
|
b7277b51fd | ||
|
|
1fa9111b2b | ||
|
|
90a9e496d9 | ||
|
|
2a7dce1eb0 | ||
|
|
0c0841cc03 | ||
|
|
4c9fe016bf | ||
|
|
acc90f140c | ||
|
|
68a7bc3930 | ||
|
|
12ea64be0e | ||
|
|
7f30a673f7 | ||
|
|
897e100c32 | ||
|
|
0d4ad5cb31 | ||
|
|
b124bd0d0e | ||
|
|
6bc2f84602 | ||
|
|
d787a28c40 | ||
|
|
6b078a5731 | ||
|
|
17dddbfe21 | ||
|
|
3ff3c9e144 | ||
|
|
f5a37d82cc | ||
|
|
d3d428dc9d | ||
|
|
8dc8c5b5dc | ||
|
|
e6b06f914b | ||
|
|
4dc502a8b6 | ||
|
|
b1d1a13d5f | ||
|
|
75cc4cac5a | ||
|
|
1b7e4fbbdc | ||
|
|
9789e2f6c1 | ||
|
|
b8fb0bee24 | ||
|
|
419f77e245 | ||
|
|
59b1c3473b | ||
|
|
6db58ca375 | ||
|
|
4832b342b0 | ||
|
|
6cec542402 | ||
|
|
9644791783 | ||
|
|
5031c307d1 | ||
|
|
aa49539e3e | ||
|
|
7b4118493b | ||
|
|
d1cc9ba4ce | ||
|
|
e0e92139d7 | ||
|
|
62039392bb | ||
|
|
b72c69892e | ||
|
|
e6205e9aad | ||
|
|
b8a6fb1720 | ||
|
|
7c06d82f27 | ||
|
|
d92cb0f500 | ||
|
|
7fa72f2fe9 | ||
|
|
21d480a3b5 | ||
|
|
771c045844 | ||
|
|
e6ce484c15 | ||
|
|
102a92f62d | ||
|
|
6c7ac70701 | ||
|
|
9d8372289f | ||
|
|
766f6a1ba2 | ||
|
|
193ff24f4c | ||
|
|
c675017374 | ||
|
|
86cb852507 | ||
|
|
73494e0d7d | ||
|
|
ec61aa1b6f | ||
|
|
6df0e78b22 | ||
|
|
63c604359b | ||
|
|
08212588a0 | ||
|
|
c8518ce827 | ||
|
|
94434e3fc0 | ||
|
|
9f3af95198 | ||
|
|
acb3af8ab8 | ||
|
|
9c50889371 | ||
|
|
8c03c90708 | ||
|
|
91cc21e729 | ||
|
|
dd29199c9b | ||
|
|
9156629d72 | ||
|
|
002aa61dd9 | ||
|
|
401747a7a3 | ||
|
|
990390218c | ||
|
|
69a4d6ac83 | ||
|
|
3a67492680 | ||
|
|
d58b9edf78 | ||
|
|
5144dd09f1 | ||
|
|
6a5f3720a2 | ||
|
|
d814d3537c | ||
|
|
85380ade6a | ||
|
|
86f53deade | ||
|
|
c3357dc0e2 | ||
|
|
97e14dd294 | ||
|
|
e45c48b998 | ||
|
|
0b53eae4ad | ||
|
|
92aa3123ec | ||
|
|
e9e789da20 | ||
|
|
c6bdac8835 | ||
|
|
90df679a77 | ||
|
|
b25a422fd6 | ||
|
|
47e70bd086 | ||
|
|
f963194124 | ||
|
|
bdfc77d349 | ||
|
|
7abe90f2ac | ||
|
|
4a52779d09 | ||
|
|
a01e865042 | ||
|
|
446c50da80 | ||
|
|
750a93a1aa | ||
|
|
ba12d65792 | ||
|
|
bd40404f58 | ||
|
|
4d8d9ecfc2 | ||
|
|
f2efa022b4 | ||
|
|
fc28f34ec6 | ||
|
|
b740cc467d | ||
|
|
6ab8114eee | ||
|
|
cd3f90917f | ||
|
|
2219547a8b | ||
|
|
017426501c | ||
|
|
ca19754a30 | ||
|
|
4623f2f12a | ||
|
|
c14813c0b2 | ||
|
|
9d8308ace0 | ||
|
|
4976e81ea4 | ||
|
|
f59de87a31 | ||
|
|
53dbebb503 | ||
|
|
52df91eb60 | ||
|
|
a9a758d715 | ||
|
|
0226fa7a25 | ||
|
|
a4f47da35c | ||
|
|
29364000e2 | ||
|
|
ceecca44a4 | ||
|
|
50f62e66b0 | ||
|
|
ab39dfd254 | ||
|
|
708fad18b6 | ||
|
|
526ba34d87 | ||
|
|
5d4882dee9 | ||
|
|
48c4361d37 | ||
|
|
c1d070186e | ||
|
|
1a39fd9172 | ||
|
|
0c1ab4158e | ||
|
|
5221566335 | ||
|
|
2291c2d9ba | ||
|
|
0de14c4c8b | ||
|
|
51de0159fb | ||
|
|
37a756aeb3 | ||
|
|
353b6ed761 | ||
|
|
90815b1ac5 | ||
|
|
8a50786e61 | ||
|
|
3b77df0556 | ||
|
|
1fa11062de | ||
|
|
6883de0f1c | ||
|
|
bdde0fe094 | ||
|
|
ab22b8103e | ||
|
|
641d5cd67b | ||
|
|
9fe941e457 | ||
|
|
78060c9985 | ||
|
|
5bd6af3400 | ||
|
|
4ecd78d6a8 | ||
|
|
7e9f54ed2c | ||
|
|
7dd29c707f | ||
|
|
a1489fb1f9 | ||
|
|
5f0f5398e8 | ||
|
|
e3b2396f32 | ||
|
|
6fd70ed26a | ||
|
|
a93e6ff01a | ||
|
|
6db8c38c58 | ||
|
|
d3d3ff7970 | ||
|
|
c5b2b30f79 | ||
|
|
ac2144d65b | ||
|
|
c620b4f919 | ||
|
|
292a3a43ba | ||
|
|
5fc4693b9c | ||
|
|
6dfbaf1b88 | ||
|
|
14c6e56287 | ||
|
|
7e48514f67 | ||
|
|
d8e70c4d7f | ||
|
|
fb52989d62 | ||
|
|
5b72ebaad5 | ||
|
|
98863ab901 | ||
|
|
b5cb5eb969 | ||
|
|
7f4f96f77b | ||
|
|
3b3f75f03e | ||
|
|
a5db4d4e47 | ||
|
|
d3b0f25cfe | ||
|
|
a9c6a68c5f | ||
|
|
c27f172452 | ||
|
|
2eeb5822c1 | ||
|
|
743046d48f | ||
|
|
d3a5205bde | ||
|
|
ae6dd8929a | ||
|
|
dcf96896ef | ||
|
|
67792100bb | ||
|
|
48c1263417 | ||
|
|
12d37381fe | ||
|
|
dcec3f5f84 | ||
|
|
32e2a7830a | ||
|
|
6992249e53 | ||
|
|
107214ac53 | ||
|
|
8a58772911 | ||
|
|
e21736b470 | ||
|
|
e8679f8984 | ||
|
|
970fe02027 | ||
|
|
12216853c5 | ||
|
|
33ec92258d | ||
|
|
a578edf137 | ||
|
|
f8949ebead | ||
|
|
141c91301f | ||
|
|
8d95e67b5a | ||
|
|
0633e7f25f | ||
|
|
266da0a9d8 | ||
|
|
121c40f273 | ||
|
|
a876efb95f | ||
|
|
95a8cc9498 | ||
|
|
f02731055e | ||
|
|
1df83addfc | ||
|
|
9db43ac5e6 | ||
|
|
0f470cf96f | ||
|
|
da3fcb7b86 | ||
|
|
73dd4703b9 | ||
|
|
0c679a0151 | ||
|
|
1d6ea2dbe6 | ||
|
|
933df57654 | ||
|
|
a7c87642b4 | ||
|
|
cbe761fc33 | ||
|
|
f8aef78d25 | ||
|
|
14dbdb2d83 | ||
|
|
abda226d63 | ||
|
|
a2dc6f0a49 | ||
|
|
7a94c26333 | ||
|
|
9b1ffb384b | ||
|
|
9566bfe122 | ||
|
|
89ff103bda | ||
|
|
6c788db53a | ||
|
|
344b5fa419 | ||
|
|
c6d161b837 | ||
|
|
2065ba0c60 | ||
|
|
a481fd1a3e | ||
|
|
c50bcdbdb9 | ||
|
|
36a2a7632c | ||
|
|
e77b7014e6 | ||
|
|
d57fd0f827 | ||
|
|
6a83d2a62a | ||
|
|
2d29726c18 | ||
|
|
b241b0f954 | ||
|
|
171dd1dc02 | ||
|
|
af62d969d7 | ||
|
|
c4fd9a66c6 | ||
|
|
d191997a39 | ||
|
|
853ac4c104 | ||
|
|
ed053acad6 | ||
|
|
f147634e51 | ||
|
|
e3b2a68341 | ||
|
|
84c450aef9 | ||
|
|
f52a0eb43a | ||
|
|
6ed7559518 | ||
|
|
d977dbe9a7 | ||
|
|
17fc761c61 | ||
|
|
af878f2ed3 | ||
|
|
bb2164c324 | ||
|
|
0496becc50 | ||
|
|
618f8aa7d2 | ||
|
|
c57f711c48 | ||
|
|
4edd11f2f7 | ||
|
|
a2cf058951 | ||
|
|
d52eb10ddd | ||
|
|
4b6dae71fc | ||
|
|
ddad30c22e | ||
|
|
77067c545c | ||
|
|
465d283cad | ||
|
|
05071144fb | ||
|
|
a4e7904953 | ||
|
|
986a8c7554 | ||
|
|
9272843b77 | ||
|
|
542d4bc703 | ||
|
|
e3640fdac9 | ||
|
|
f64ab4b190 | ||
|
|
bd571e1577 | ||
|
|
e4a5cbd893 | ||
|
|
7a9fd7fd1e | ||
|
|
d9b60108db | ||
|
|
8455c8b4ed | ||
|
|
5c2e7099fc | ||
|
|
1fd1d55895 | ||
|
|
5ce4137e75 | ||
|
|
d49179541e | ||
|
|
676f258981 | ||
|
|
fa44749240 | ||
|
|
6c856f9da2 | ||
|
|
e8773cea7f | ||
|
|
4d36ffcb08 | ||
|
|
c653e492c4 | ||
|
|
f08de1f404 | ||
|
|
1218691b61 | ||
|
|
61fc27ff79 | ||
|
|
123ee24f7e | ||
|
|
52c9045a28 | ||
|
|
f00f1e8933 | ||
|
|
8da4433e57 | ||
|
|
7babb87934 | ||
|
|
f5857aaa0c | ||
|
|
f4222e0923 | ||
|
|
f0caea9026 |
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
# github acions
|
||||||
|
.github/
|
||||||
|
.*ignore
|
||||||
|
.git/
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv*/
|
||||||
|
ENV/
|
||||||
|
.conda/
|
||||||
|
README*.md
|
||||||
|
dashboard/
|
||||||
|
data/
|
||||||
|
changelogs/
|
||||||
|
tests/
|
||||||
|
.ruff_cache/
|
||||||
|
.astrbot
|
||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: astrbot
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: ['https://afdian.com/a/astrbot_team']
|
||||||
56
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: 🥳 发布插件
|
||||||
|
description: 提交插件到插件市场
|
||||||
|
title: "[Plugin] 插件名"
|
||||||
|
labels: ["plugin-publish"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
欢迎发布插件到插件市场!
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 插件基本信息
|
||||||
|
|
||||||
|
请将插件信息填写到下方的 JSON 代码块中。其中 `tags`(插件标签)和 `social_link`(社交链接)选填。
|
||||||
|
|
||||||
|
不熟悉 JSON ?现在可以从 [这里](https://plugins.astrbot.app/#/submit) 获取你的 JSON 啦!获取到了记得复制粘贴过来哦!
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: plugin-info
|
||||||
|
attributes:
|
||||||
|
label: 插件信息
|
||||||
|
description: 请在下方代码块中填写您的插件信息,确保反引号包裹了JSON
|
||||||
|
value: |
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "插件名",
|
||||||
|
"desc": "插件介绍",
|
||||||
|
"author": "作者名",
|
||||||
|
"repo": "插件仓库链接",
|
||||||
|
"tags": [],
|
||||||
|
"social_link": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## 检查
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checks
|
||||||
|
attributes:
|
||||||
|
label: 插件检查清单
|
||||||
|
description: 请确认以下所有项目
|
||||||
|
options:
|
||||||
|
- label: 我的插件经过完整的测试
|
||||||
|
required: true
|
||||||
|
- label: 我的插件不包含恶意代码
|
||||||
|
required: true
|
||||||
|
- label: 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||||
|
required: true
|
||||||
82
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: '🐛 报告 Bug'
|
||||||
|
title: '[Bug]'
|
||||||
|
description: 提交报告帮助我们改进。
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 发生了什么
|
||||||
|
description: 描述你遇到的异常
|
||||||
|
placeholder: >
|
||||||
|
一个清晰且具体的描述这个异常是什么。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 如何复现?
|
||||||
|
description: >
|
||||||
|
复现该问题的步骤
|
||||||
|
placeholder: >
|
||||||
|
如: 1. 打开 '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: AstrBot 版本、部署方式(如 Windows Docker Desktop 部署)、使用的提供商、使用的消息平台适配器
|
||||||
|
description: >
|
||||||
|
请提供您的 AstrBot 版本和部署方式。
|
||||||
|
placeholder: >
|
||||||
|
如: 3.1.8 Docker, 3.1.7 Windows启动器
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 操作系统
|
||||||
|
description: |
|
||||||
|
你在哪个操作系统上遇到了这个问题?
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- 'Windows'
|
||||||
|
- 'macOS'
|
||||||
|
- 'Linux'
|
||||||
|
- 'Other'
|
||||||
|
- 'Not sure'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 报错日志
|
||||||
|
description: >
|
||||||
|
如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长!
|
||||||
|
placeholder: >
|
||||||
|
请提供完整的报错日志或截图。
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: 你愿意提交 PR 吗?
|
||||||
|
description: >
|
||||||
|
这不是必需的,但我们很乐意在贡献过程中为您提供指导特别是如果你已经很好地理解了如何实现修复。
|
||||||
|
options:
|
||||||
|
- label: 是的,我愿意提交 PR!
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
options:
|
||||||
|
- label: >
|
||||||
|
我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "感谢您填写我们的表单!"
|
||||||
42
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
name: '🎉 功能建议'
|
||||||
|
title: "[Feature]"
|
||||||
|
description: 提交建议帮助我们改进。
|
||||||
|
labels: [ "enhancement" ]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢您抽出时间提出新功能建议,请准确解释您的想法。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 描述
|
||||||
|
description: 简短描述您的功能建议。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 使用场景
|
||||||
|
description: 你想要发生什么?
|
||||||
|
placeholder: >
|
||||||
|
一个清晰且具体的描述这个功能的使用场景。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: 你愿意提交PR吗?
|
||||||
|
description: >
|
||||||
|
这不是必须的,但我们欢迎您的贡献。
|
||||||
|
options:
|
||||||
|
- label: 是的, 我愿意提交PR!
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Code of Conduct
|
||||||
|
options:
|
||||||
|
- label: >
|
||||||
|
我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "感谢您填写我们的表单!"
|
||||||
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!-- 如果有的话,指定这个 PR 要解决的 ISSUE -->
|
||||||
|
解决了 #XYZ
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
|
||||||
|
<!--解释为什么要改动-->
|
||||||
|
|
||||||
|
### Modifications
|
||||||
|
|
||||||
|
<!--简单解释你的改动-->
|
||||||
|
|
||||||
|
### Check
|
||||||
|
|
||||||
|
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容-->
|
||||||
|
|
||||||
|
- [ ] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
||||||
|
- [ ] 👀 我的更改经过良好的测试
|
||||||
|
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。
|
||||||
|
- [ ] 😮 我的更改没有引入恶意代码
|
||||||
63
.github/copilot-instructions.md
vendored
Normal file
63
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# AstrBot Development Instructions
|
||||||
|
|
||||||
|
AstrBot is a multi-platform LLM chatbot and development framework written in Python with a Vue.js dashboard. It supports multiple messaging platforms (QQ, Telegram, Discord, etc.) and various LLM providers (OpenAI, Anthropic, Google Gemini, etc.).
|
||||||
|
|
||||||
|
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||||
|
|
||||||
|
## Working Effectively
|
||||||
|
|
||||||
|
### Bootstrap and Install Dependencies
|
||||||
|
- **Python 3.10+ required** - Check `.python-version` file
|
||||||
|
- Install UV package manager: `pip install uv`
|
||||||
|
- Install project dependencies: `uv sync` -- takes 6-7 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
|
||||||
|
- Create required directories: `mkdir -p data/plugins data/config data/temp`
|
||||||
|
|
||||||
|
### Running the Application
|
||||||
|
- Run main application: `uv run main.py` -- starts in ~3 seconds
|
||||||
|
- Application creates WebUI on http://localhost:6185 (default credentials: `astrbot`/`astrbot`)
|
||||||
|
- Application loads plugins automatically from `packages/` and `data/plugins/` directories
|
||||||
|
|
||||||
|
### Dashboard Build (Vue.js/Node.js)
|
||||||
|
- **Prerequisites**: Node.js 20+ and npm 10+ required
|
||||||
|
- Navigate to dashboard: `cd dashboard`
|
||||||
|
- Install dashboard dependencies: `npm install` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
|
||||||
|
- Build dashboard: `npm run build` -- takes 25-30 seconds. NEVER CANCEL.
|
||||||
|
- Dashboard creates optimized production build in `dashboard/dist/`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Do not generate test files for now.
|
||||||
|
|
||||||
|
### Code Quality and Linting
|
||||||
|
- Install ruff linter: `uv add --dev ruff`
|
||||||
|
- Check code style: `uv run ruff check .` -- takes <1 second
|
||||||
|
- Check formatting: `uv run ruff format --check .` -- takes <1 second
|
||||||
|
- Fix formatting: `uv run ruff format .`
|
||||||
|
- **ALWAYS** run `uv run ruff check .` and `uv run ruff format .` before committing changes
|
||||||
|
|
||||||
|
### Plugin Development
|
||||||
|
- Plugins load from `packages/` (built-in) and `data/plugins/` (user-installed)
|
||||||
|
- Plugin system supports function tools and message handlers
|
||||||
|
- Key plugins: python_interpreter, web_searcher, astrbot, reminder, session_controller
|
||||||
|
|
||||||
|
### Common Issues and Workarounds
|
||||||
|
- **Dashboard download fails**: Known issue with "division by zero" error - application still works
|
||||||
|
- **Import errors in tests**: Ensure `uv run` is used to run tests in proper environment
|
||||||
|
=- **Build timeouts**: Always set appropriate timeouts (10+ minutes for uv sync, 5+ minutes for npm install)
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
- GitHub Actions workflows in `.github/workflows/`
|
||||||
|
- Docker builds supported via `Dockerfile`
|
||||||
|
- Pre-commit hooks enforce ruff formatting and linting
|
||||||
|
|
||||||
|
## Docker Support
|
||||||
|
- Primary deployment method: `docker run soulter/astrbot:latest`
|
||||||
|
- Compose file available: `compose.yml`
|
||||||
|
- Exposes ports: 6185 (WebUI), 6195 (WeChat), 6199 (QQ), etc.
|
||||||
|
- Volume mount required: `./data:/AstrBot/data`
|
||||||
|
|
||||||
|
## Multi-language Support
|
||||||
|
- Documentation in Chinese (README.md), English (README_en.md), Japanese (README_ja.md)
|
||||||
|
- UI supports internationalization
|
||||||
|
- Default language is Chinese
|
||||||
|
|
||||||
|
Remember: This is a production chatbot framework with real users. Always test thoroughly and ensure changes don't break existing functionality.
|
||||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Actions updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
92
.github/workflows/auto_release.yml
vendored
Normal file
92
.github/workflows/auto_release.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
name: Auto Release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish-to-github-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Dashboard Build
|
||||||
|
run: |
|
||||||
|
cd dashboard
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
|
echo ${{ github.ref_name }} > dist/assets/version
|
||||||
|
zip -r dist.zip dist
|
||||||
|
|
||||||
|
- name: Upload to Cloudflare R2
|
||||||
|
env:
|
||||||
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_BUCKET_NAME: "astrbot"
|
||||||
|
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||||
|
VERSION_TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
echo "Installing rclone..."
|
||||||
|
curl https://rclone.org/install.sh | sudo bash
|
||||||
|
|
||||||
|
echo "Configuring rclone remote..."
|
||||||
|
mkdir -p ~/.config/rclone
|
||||||
|
cat <<EOF > ~/.config/rclone/rclone.conf
|
||||||
|
[r2]
|
||||||
|
type = s3
|
||||||
|
provider = Cloudflare
|
||||||
|
access_key_id = $R2_ACCESS_KEY_ID
|
||||||
|
secret_access_key = $R2_SECRET_ACCESS_KEY
|
||||||
|
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Uploading dist.zip to R2 bucket: $R2_BUCKET_NAME/$R2_OBJECT_NAME"
|
||||||
|
mv dashboard/dist.zip dashboard/$R2_OBJECT_NAME
|
||||||
|
rclone copy dashboard/$R2_OBJECT_NAME r2:$R2_BUCKET_NAME --progress
|
||||||
|
mv dashboard/$R2_OBJECT_NAME dashboard/astrbot-webui-${VERSION_TAG}.zip
|
||||||
|
rclone copy dashboard/astrbot-webui-${VERSION_TAG}.zip r2:$R2_BUCKET_NAME --progress
|
||||||
|
mv dashboard/astrbot-webui-${VERSION_TAG}.zip dashboard/dist.zip
|
||||||
|
|
||||||
|
- name: Fetch Changelog
|
||||||
|
run: |
|
||||||
|
echo "changelog=changelogs/${{github.ref_name}}.md" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
bodyFile: ${{ env.changelog }}
|
||||||
|
artifacts: "dashboard/dist.zip"
|
||||||
|
|
||||||
|
build-and-publish-to-pypi:
|
||||||
|
# 构建并发布到 PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-and-publish-to-github-release
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: |
|
||||||
|
python -m pip install uv
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: |
|
||||||
|
uv build
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
run: |
|
||||||
|
uv publish
|
||||||
93
.github/workflows/codeql.yml
vendored
Normal file
93
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '21 15 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||||
|
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||||
|
# - https://gh.io/supported-runners-and-hardware-resources
|
||||||
|
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||||
|
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||||
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
|
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
# required to fetch internal or private CodeQL packs
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
# only required for workflows in private repositories
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: python
|
||||||
|
build-mode: none
|
||||||
|
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||||
|
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||||
|
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||||
|
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||||
|
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||||
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
# If the analyze step fails for one of the languages you are analyzing with
|
||||||
|
# "We were unable to automatically build your code", modify the matrix above
|
||||||
|
# to set the build mode to "manual" for that language. Then modify this step
|
||||||
|
# to build your code.
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
- if: matrix.build-mode == 'manual'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||||
|
'languages you are analyzing, replace this with the commands to build' \
|
||||||
|
'your code, for example:'
|
||||||
|
echo ' make bootstrap'
|
||||||
|
echo ' make release'
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
45
.github/workflows/coverage_test.yml
vendored
Normal file
45
.github/workflows/coverage_test.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Run tests and upload coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- 'README.md'
|
||||||
|
- 'changelogs/**'
|
||||||
|
- 'dashboard/**'
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run tests and collect coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest pytest-asyncio pytest-cov
|
||||||
|
pip install --editable .
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
mkdir -p data/plugins
|
||||||
|
mkdir -p data/config
|
||||||
|
mkdir -p data/temp
|
||||||
|
export TESTING=true
|
||||||
|
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
||||||
|
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
|
||||||
|
|
||||||
|
- name: Upload results to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
47
.github/workflows/dashboard_ci.yml
vendored
Normal file
47
.github/workflows/dashboard_ci.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: AstrBot Dashboard CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: npm install, build
|
||||||
|
run: |
|
||||||
|
cd dashboard
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Inject Commit SHA
|
||||||
|
id: get_sha
|
||||||
|
run: |
|
||||||
|
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
|
mkdir -p dashboard/dist/assets
|
||||||
|
echo $COMMIT_SHA > dashboard/dist/assets/version
|
||||||
|
cd dashboard
|
||||||
|
zip -r dist.zip dist
|
||||||
|
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-without-markdown
|
||||||
|
path: |
|
||||||
|
dashboard/dist
|
||||||
|
!dist/**/*.md
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
tag: release-${{ github.sha }}
|
||||||
|
owner: AstrBotDevs
|
||||||
|
repo: astrbot-release-harbour
|
||||||
|
body: "Automated release from commit ${{ github.sha }}"
|
||||||
|
token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }}
|
||||||
|
artifacts: "dashboard/dist.zip"
|
||||||
67
.github/workflows/docker-image.yml
vendored
67
.github/workflows/docker-image.yml
vendored
@@ -1,23 +1,62 @@
|
|||||||
name: Docker Image CI/CD
|
name: Docker Image CI/CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-latest-docker-image:
|
publish-docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Build and publish docker image
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Pull The Codes
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v5
|
||||||
- name: Build image
|
with:
|
||||||
|
fetch-depth: 0 # Must be 0 so we can fetch tags
|
||||||
|
|
||||||
|
- name: Get latest tag (only on manual trigger)
|
||||||
|
id: get-latest-tag
|
||||||
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
git clone https://github.com/Soulter/AstrBot
|
tag=$(git describe --tags --abbrev=0)
|
||||||
cd AstrBot
|
echo "latest_tag=$tag" >> $GITHUB_OUTPUT
|
||||||
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest .
|
|
||||||
- name: Publish image
|
- name: Checkout to latest tag (only on manual trigger)
|
||||||
run: |
|
if: github.event_name == 'workflow_dispatch'
|
||||||
docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }}
|
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
|
||||||
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
|
||||||
|
- name: Set QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: Soulter
|
||||||
|
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker to DockerHub and Github GHCR
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:latest
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
||||||
|
ghcr.io/soulter/astrbot:latest
|
||||||
|
ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
|
||||||
|
|
||||||
|
- name: Post build notifications
|
||||||
|
run: echo "Docker image has been built and pushed successfully"
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: 'Stale issue message'
|
stale-issue-message: 'Stale issue message'
|
||||||
|
|||||||
26
.gitignore
vendored
26
.gitignore
vendored
@@ -1,13 +1,33 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
botpy.log
|
botpy.log
|
||||||
.vscode
|
.vscode
|
||||||
data.db
|
.venv*
|
||||||
|
.idea
|
||||||
|
data_v2.db
|
||||||
|
data_v3.db
|
||||||
configs/session
|
configs/session
|
||||||
configs/config.yaml
|
configs/config.yaml
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
temp
|
temp
|
||||||
cmd_config.json
|
cmd_config.json
|
||||||
addons/plugins/
|
data
|
||||||
data/*
|
|
||||||
cookies.json
|
cookies.json
|
||||||
logs/
|
logs/
|
||||||
|
addons/plugins
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
|
||||||
|
tests/astrbot_plugin_openai
|
||||||
|
chroma
|
||||||
|
dashboard/node_modules/
|
||||||
|
dashboard/dist/
|
||||||
|
.DS_Store
|
||||||
|
package-lock.json
|
||||||
|
package.json
|
||||||
|
venv/*
|
||||||
|
packages/python_interpreter/workplace
|
||||||
|
.venv/*
|
||||||
|
.conda/
|
||||||
|
.idea
|
||||||
|
pytest.ini
|
||||||
|
.astrbot
|
||||||
13
.pre-commit-config.yaml
Normal file
13
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
default_install_hook_types: [pre-commit, prepare-commit-msg]
|
||||||
|
ci:
|
||||||
|
autofix_commit_msg: ":balloon: auto fixes by pre-commit hooks"
|
||||||
|
autofix_prs: true
|
||||||
|
autoupdate_branch: master
|
||||||
|
autoupdate_schedule: weekly
|
||||||
|
autoupdate_commit_msg: ":balloon: pre-commit autoupdate"
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.11.2
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
- id: ruff-format
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.10
|
||||||
31
Dockerfile
31
Dockerfile
@@ -1,8 +1,35 @@
|
|||||||
FROM python:3.10.13-bullseye
|
FROM python:3.11-slim
|
||||||
WORKDIR /AstrBot
|
WORKDIR /AstrBot
|
||||||
|
|
||||||
COPY . /AstrBot/
|
COPY . /AstrBot/
|
||||||
|
|
||||||
RUN python -m pip install -r requirements.txt
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
gcc \
|
||||||
|
build-essential \
|
||||||
|
python3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
ca-certificates \
|
||||||
|
bash \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN python -m pip install uv
|
||||||
|
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||||
|
RUN uv pip install socksio uv pyffmpeg pilk --no-cache-dir --system
|
||||||
|
|
||||||
|
# 释出 ffmpeg
|
||||||
|
RUN python -c "from pyffmpeg import FFmpeg; ff = FFmpeg();"
|
||||||
|
|
||||||
|
# add /root/.pyffmpeg/bin/ffmpeg to PATH, inorder to use ffmpeg
|
||||||
|
RUN echo 'export PATH=$PATH:/root/.pyffmpeg/bin' >> ~/.bashrc
|
||||||
|
|
||||||
|
EXPOSE 6185
|
||||||
|
EXPOSE 6186
|
||||||
|
|
||||||
CMD [ "python", "main.py" ]
|
CMD [ "python", "main.py" ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
35
Dockerfile_with_node
Normal file
35
Dockerfile_with_node
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
WORKDIR /AstrBot
|
||||||
|
|
||||||
|
COPY . /AstrBot/
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
build-essential \
|
||||||
|
python3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libssl-dev \
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
ca-certificates \
|
||||||
|
bash \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Installation of Node.js
|
||||||
|
ENV NVM_DIR="/root/.nvm"
|
||||||
|
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
|
||||||
|
. "$NVM_DIR/nvm.sh" && \
|
||||||
|
nvm install 22 && \
|
||||||
|
nvm use 22
|
||||||
|
RUN /bin/bash -c ". \"$NVM_DIR/nvm.sh\" && node -v && npm -v"
|
||||||
|
|
||||||
|
RUN python -m pip install uv
|
||||||
|
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||||
|
RUN uv pip install socksio uv pyffmpeg --no-cache-dir --system
|
||||||
|
|
||||||
|
EXPOSE 6185
|
||||||
|
EXPOSE 6186
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
4
LICENSE
4
LICENSE
@@ -629,8 +629,8 @@ 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
|
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.
|
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.>
|
AstrBot is a llm-powered chatbot and develop framework.
|
||||||
Copyright (C) <year> <name of author>
|
Copyright (C) 2022-2099 Soulter
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
|||||||
334
README.md
334
README.md
@@ -1,180 +1,226 @@
|
|||||||
<p align="center">
|
<img width="430" height="31" alt="image" src="https://github.com/user-attachments/assets/474c822c-fab7-41be-8c23-6dae252823ed" /><p align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
<img src="https://github.com/Soulter/AstrBot/assets/37870767/b1686114-f3aa-4963-b07f-28bf83dc0a10" alt="QQChannelChatGPT" width="200" />
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# AstrBot
|
_✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
||||||
|
|
||||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
<img src="https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/34412545-2e37-400f-bedc-42348713ac1f.svg" alt="wakatime">
|
|
||||||
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
|
||||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
|
||||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft">
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-322154837-purple">
|
|
||||||
</a>
|
|
||||||
<img alt="Static Badge" src="https://img.shields.io/badge/频道-x42d56aki2-purple">
|
|
||||||
|
|
||||||
<a href="https://astrbot.soulter.top/center">项目部署</a> |
|
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||||
<a href="https://github.com/Soulter/QQChannelChatGPT/issues">问题提交</a> |
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
|
||||||
<a href="https://astrbot.soulter.top/center/docs/%E5%BC%80%E5%8F%91/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91">插件开发(最少只需 25 行)</a>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
|
||||||
|
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
|
||||||
|
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
||||||
|
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a> |
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
|
<a href="https://astrbot.app/">查看文档</a> |
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 🤔您可能想了解的
|
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
|
||||||
- **如何部署?** [帮助文档](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2) (部署不成功欢迎进群捞人解决<3)
|
|
||||||
- **go-cqhttp启动不成功、报登录失败?** [在这里搜索解决方法](https://github.com/Mrs4s/go-cqhttp/issues)
|
|
||||||
- **程序闪退/机器人启动不成功?** [提交issue或加群反馈](https://github.com/Soulter/QQChannelChatGPT/issues)
|
|
||||||
- **如何开启 ChatGPT、Claude、HuggingChat 等语言模型?** [查看帮助](https://astrbot.soulter.top/center/docs/%E4%BD%BF%E7%94%A8/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B)
|
|
||||||
|
|
||||||
## 🧩功能:
|
## ✨ 主要功能
|
||||||
|
|
||||||
✨ 最近功能:
|
1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
|
||||||
1. 可视化面板
|
2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
|
||||||
2. Docker 一键部署项目:[链接](https://astrbot.soulter.top/center/docs/%E9%83%A8%E7%BD%B2/%E9%80%9A%E8%BF%87Docker%E9%83%A8%E7%BD%B2)
|
3. **Agent**。完善适配的 Agentic 能力。支持多轮工具调用、内置沙盒代码执行器、网页搜索等功能。
|
||||||
|
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富。
|
||||||
|
5. **WebUI**。可视化配置和管理机器人,功能齐全。
|
||||||
|
|
||||||
🌍支持的消息平台/接口
|
## ✨ 使用方式
|
||||||
- go-cqhttp(QQ、QQ频道)
|
|
||||||
- QQ 官方机器人接口
|
|
||||||
- Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持)
|
|
||||||
|
|
||||||
🌍支持的AI语言模型一览:
|
#### Docker 部署
|
||||||
|
|
||||||
**文字模型/图片理解**
|
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
|
||||||
|
|
||||||
- OpenAI GPT-3(原生支持)
|
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
|
||||||
- OpenAI GPT-3.5(原生支持)
|
|
||||||
- OpenAI GPT-4(原生支持)
|
|
||||||
- Claude(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
|
||||||
- HuggingChat(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
|
||||||
- Gemini(免费,由[LLMs插件](https://github.com/Soulter/llms)支持)
|
|
||||||
|
|
||||||
**图片生成**
|
#### 宝塔面板部署
|
||||||
- OpenAI Dalle 接口
|
|
||||||
- NovelAI/Naifu (免费,由[AIDraw插件](https://github.com/Soulter/aidraw)支持)
|
|
||||||
|
|
||||||
🌍机器人支持的能力一览:
|
AstrBot 与宝塔面板合作,已上架至宝塔面板。
|
||||||
- 可视化面板(beta)
|
|
||||||
- 同时部署机器人到 QQ 和 QQ 频道
|
|
||||||
- 大模型对话
|
|
||||||
- 大模型网页搜索能力 **(目前仅支持OpenAI系模型,最新版本下使用 web on 指令打开)**
|
|
||||||
- 插件(在QQ或QQ频道聊天框内输入 `plugin` 了解详情)
|
|
||||||
- 回复文字图片渲染(以图片markdown格式回复,**大幅度降低被风控概率**,需手动在`cmd_config.json`内开启qq_pic_mode)
|
|
||||||
- 人格设置
|
|
||||||
- 关键词回复
|
|
||||||
- 热更新(更新本项目时**仅需**在QQ或QQ频道聊天框内输入`update latest r`)
|
|
||||||
- Windows一键部署 https://github.com/Soulter/QQChatGPTLauncher/releases/latest
|
|
||||||
|
|
||||||
<!--
|
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
|
||||||
### 基本功能
|
|
||||||
<details>
|
|
||||||
<summary>✅ 回复符合上下文</summary>
|
|
||||||
|
|
||||||
- 程序向API发送近多次对话内容,模型根据上下文生成回复
|
#### 1Panel 部署
|
||||||
|
|
||||||
- 你可在`configs/config.yaml`中修改`total_token_limit`来近似控制缓存大小。
|
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
|
||||||
<summary>✅ 超额自动切换</summary>
|
|
||||||
|
#### 在 雨云 上部署
|
||||||
|
|
||||||
|
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
|
||||||
|
|
||||||
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
|
|
||||||
|
#### 在 Replit 上部署
|
||||||
|
|
||||||
|
社区贡献的部署方式。
|
||||||
|
|
||||||
|
[](https://repl.it/github/Soulter/AstrBot)
|
||||||
|
|
||||||
|
#### Windows 一键安装器部署
|
||||||
|
|
||||||
|
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
|
||||||
|
|
||||||
|
#### CasaOS 部署
|
||||||
|
|
||||||
|
社区贡献的部署方式。
|
||||||
|
|
||||||
|
请参阅官方文档 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html) 。
|
||||||
|
|
||||||
|
#### 手动部署
|
||||||
|
|
||||||
|
> 推荐使用 `uv`。
|
||||||
|
|
||||||
|
首先,安装 uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install uv
|
||||||
|
```
|
||||||
|
|
||||||
|
通过 Git Clone 安装 AstrBot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||||
|
uv run main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
||||||
|
|
||||||
|
## ⚡ 消息平台支持情况
|
||||||
|
|
||||||
|
| 平台 | 支持性 |
|
||||||
|
| -------- | ------- |
|
||||||
|
| QQ(官方机器人接口) | ✔ |
|
||||||
|
| QQ(OneBot) | ✔ |
|
||||||
|
| Telegram | ✔ |
|
||||||
|
| 企业微信 | ✔ |
|
||||||
|
| 微信客服 | ✔ |
|
||||||
|
| 微信公众号 | ✔ |
|
||||||
|
| 飞书 | ✔ |
|
||||||
|
| 钉钉 | ✔ |
|
||||||
|
| Slack | ✔ |
|
||||||
|
| Discord | ✔ |
|
||||||
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
|
||||||
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
|
||||||
|
| 微信对话开放平台 | 🚧 |
|
||||||
|
| WhatsApp | 🚧 |
|
||||||
|
| 小爱音响 | 🚧 |
|
||||||
|
|
||||||
|
## ⚡ 提供商支持情况
|
||||||
|
|
||||||
|
| 名称 | 支持性 | 类型 | 备注 |
|
||||||
|
| -------- | ------- | ------- | ------- |
|
||||||
|
| OpenAI API | ✔ | 文本生成 | 也支持 DeepSeek、Gemini、Kimi、xAI 等兼容 OpenAI API 的服务 |
|
||||||
|
| Claude API | ✔ | 文本生成 | |
|
||||||
|
| Google Gemini API | ✔ | 文本生成 | |
|
||||||
|
| Dify | ✔ | LLMOps | |
|
||||||
|
| 阿里云百炼应用 | ✔ | LLMOps | |
|
||||||
|
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||||
|
| LM Studio | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
|
||||||
|
| LLMTuner | ✔ | 模型加载器 | 本地加载 lora 等微调模型 |
|
||||||
|
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | 模型 API 及算力服务平台 | |
|
||||||
|
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | 模型 API 服务平台 | |
|
||||||
|
| 硅基流动 | ✔ | 模型 API 服务平台 | |
|
||||||
|
| PPIO 派欧云 | ✔ | 模型 API 服务平台 | |
|
||||||
|
| OneAPI | ✔ | LLM 分发系统 | |
|
||||||
|
| Whisper | ✔ | 语音转文本 | 支持 API、本地部署 |
|
||||||
|
| SenseVoice | ✔ | 语音转文本 | 本地部署 |
|
||||||
|
| OpenAI TTS API | ✔ | 文本转语音 | |
|
||||||
|
| GSVI | ✔ | 文本转语音 | GPT-Sovits-Inference |
|
||||||
|
| GPT-SoVITs | ✔ | 文本转语音 | GPT-Sovits-Inference |
|
||||||
|
| FishAudio | ✔ | 文本转语音 | GPT-Sovits 作者参与的项目 |
|
||||||
|
| Edge TTS | ✔ | 文本转语音 | Edge 浏览器的免费 TTS |
|
||||||
|
| 阿里云百炼 TTS | ✔ | 文本转语音 | |
|
||||||
|
| Azure TTS | ✔ | 文本转语音 | Microsoft Azure TTS |
|
||||||
|
|
||||||
|
|
||||||
|
## ❤️ 贡献
|
||||||
|
|
||||||
|
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||||
|
|
||||||
|
### 如何贡献
|
||||||
|
|
||||||
|
你可以通过查看问题或帮助审核 PR(拉取请求)来贡献。任何问题或 PR 都欢迎参与,以促进社区贡献。当然,这些只是建议,你可以以任何方式进行贡献。对于新功能的添加,请先通过 Issue 讨论。
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
AstrBot 使用 `ruff` 进行代码格式化和检查。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Soulter/AstrBot
|
||||||
|
pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌟 支持
|
||||||
|
|
||||||
|
- Star 这个项目!
|
||||||
|
- 在[爱发电](https://afdian.com/a/soulter)支持我!
|
||||||
|
|
||||||
|
## ✨ Demo
|
||||||
|
|
||||||
|
<details><summary>👉 点击展开多张 Demo 截图 👈</summary>
|
||||||
|
|
||||||
|
<div align='center'>
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
|
||||||
|
|
||||||
|
_✨基于 Docker 的沙箱化代码执行器(Beta 测试)✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
|
||||||
|
|
||||||
|
_✨ 多模态、网页搜索、长文本转图片(可配置) ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
|
||||||
|
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
|
||||||
|
|
||||||
|
_✨ 插件系统——部分插件展示 ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/0cdbf564-2f59-4da5-b524-ce0e7ef3d978" width=600>
|
||||||
|
|
||||||
|
_✨ WebUI ✨_
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
- 超额时,程序自动切换openai的key,方便快捷
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
|
||||||
|
|
||||||
<summary>✅ 支持统计频道、消息数量等信息</summary>
|
## ❤️ Special Thanks
|
||||||
|
|
||||||
- 实现了简单的统计功能
|
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
|
||||||
|
|
||||||
</details>
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<details>
|
此外,本项目的诞生离不开以下开源项目:
|
||||||
<summary>✅ 多并发处理,回复速度快</summary>
|
|
||||||
|
|
||||||
- 使用了协程,理论最高可以支持每个子频道每秒回复5条信息
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
|
||||||
<summary>✅ 持久化转储历史记录,重启不丢失</summary>
|
- [wechatpy/wechatpy](https://github.com/wechatpy/wechatpy)
|
||||||
|
|
||||||
- 使用内置的sqlite数据库存储历史记录到本地
|
## ⭐ Star History
|
||||||
|
|
||||||
- 方式为定时转储,可在`config.yaml`下修改`dump_history_interval`来修改间隔时间,单位为分钟。
|
> [!TIP]
|
||||||
|
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star,这是我维护这个开源项目的动力 <3
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
<div align="center">
|
||||||
<summary>✅ 支持多种指令控制</summary>
|
|
||||||
|
[](https://star-history.com/#soulter/astrbot&Date)
|
||||||
- 详见下方`指令功能`
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
</div>
|
||||||
<summary>✅ 官方API,稳定</summary>
|
|
||||||
|
|
||||||
- 不使用ChatGPT逆向接口,而使用官方API接口,稳定方便。
|

|
||||||
|
|
||||||
- QQ频道机器人框架为QQ官方开源的框架,稳定。
|
|
||||||
|
|
||||||
</details> -->
|
|
||||||
|
|
||||||
<!-- > 关于token:token就相当于是AI中的单词数(但是不等于单词数),`text-davinci-003`模型中最大可以支持`4097`个token。在发送信息时,这个机器人会将用户的历史聊天记录打包发送给ChatGPT,因此,`token`也会相应的累加,为了保证聊天的上下文的逻辑性,就有了缓存token。 -->
|
|
||||||
|
|
||||||
### 🛠️ 插件支持
|
|
||||||
|
|
||||||
本项目支持接入插件。
|
|
||||||
|
|
||||||
> 使用`plugin i 插件GitHub链接`即可安装。
|
|
||||||
|
|
||||||
部分插件:
|
|
||||||
|
|
||||||
- `LLMS`: https://github.com/Soulter/llms | Claude, HuggingChat 大语言模型接入。
|
|
||||||
|
|
||||||
- `GoodPlugins`: https://github.com/Soulter/goodplugins | 随机动漫图片、搜番、喜报生成器等等
|
|
||||||
|
|
||||||
- `sysstat`: https://github.com/Soulter/sysstatqcbot | 查看系统状态
|
|
||||||
|
|
||||||
- `BiliMonitor`: https://github.com/Soulter/BiliMonitor | 订阅B站动态
|
|
||||||
|
|
||||||
- `liferestart`: https://github.com/Soulter/liferestart | 人生重开模拟器
|
|
||||||
|
|
||||||
|
|
||||||
<img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7">
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
### 指令
|
|
||||||
|
|
||||||
#### OpenAI官方API
|
|
||||||
在频道内需要先`@`机器人之后再输入指令;在QQ中暂时需要在消息前加上`ai `,不需要@
|
|
||||||
- `/reset`重置prompt
|
|
||||||
- `/his`查看历史记录(每个用户都有独立的会话)
|
|
||||||
- `/his [页码数]`查看不同页码的历史记录。例如`/his 2`查看第2页
|
|
||||||
- `/token`查看当前缓存的总token数
|
|
||||||
- `/count` 查看统计
|
|
||||||
- `/status` 查看chatGPT的配置
|
|
||||||
- `/help` 查看帮助
|
|
||||||
- `/key` 动态添加key
|
|
||||||
- `/set` 人格设置面板
|
|
||||||
- `/keyword nihao 你好` 设置关键词回复。nihao->你好
|
|
||||||
- `/画` 画画
|
|
||||||
|
|
||||||
#### 逆向ChatGPT库语言模型
|
|
||||||
- `/gpt` 切换为OpenAI官方API
|
|
||||||
|
|
||||||
* 切换模型指令支持临时回复。如`/a 你好`将会临时使用一次bing模型 -->
|
|
||||||
<!--
|
|
||||||
## 🙇感谢
|
|
||||||
|
|
||||||
本项目使用了一下项目:
|
|
||||||
|
|
||||||
[ChatGPT by acheong08](https://github.com/acheong08/ChatGPT)
|
|
||||||
|
|
||||||
[EdgeGPT by acheong08](https://github.com/acheong08/EdgeGPT)
|
|
||||||
|
|
||||||
[go-cqhttp by Mrs4s](https://github.com/Mrs4s/go-cqhttp)
|
|
||||||
|
|
||||||
[nakuru-project by Lxns-Network](https://github.com/Lxns-Network/nakuru-project) -->
|
|
||||||
|
|||||||
182
README_en.md
Normal file
182
README_en.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<p align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
_✨ Easy-to-use Multi-platform LLM Chatbot & Development Framework ✨_
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
|
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
||||||
|
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="Static Badge" src="https://img.shields.io/badge/QQ群-630166526-purple"></a>
|
||||||
|
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||||
|

|
||||||
|
[](https://codecov.io/gh/Soulter/AstrBot)
|
||||||
|
|
||||||
|
<a href="https://astrbot.app/">Documentation</a> |
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/issues">Issue Tracking</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AstrBot is a loosely coupled, asynchronous chatbot and development framework that supports multi-platform deployment, featuring an easy-to-use plugin system and comprehensive Large Language Model (LLM) integration capabilities.
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
1. **LLM Conversations** - Supports various LLMs including OpenAI API, Google Gemini, Llama, Deepseek, ChatGLM, etc. Enables local model deployment via Ollama/LLMTuner. Features multi-turn dialogues, personality contexts, multimodal capabilities (image understanding), and speech-to-text (Whisper).
|
||||||
|
2. **Multi-platform Integration** - Supports QQ (OneBot), QQ Channels, WeChat (Gewechat), Feishu, and Telegram. Planned support for DingTalk, Discord, WhatsApp, and Xiaomi Smart Speakers. Includes rate limiting, whitelisting, keyword filtering, and Baidu content moderation.
|
||||||
|
3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://dify.ai/) for easy access to Dify assistants/knowledge bases/workflows.
|
||||||
|
4. **Plugin System** - Optimized plugin mechanism with minimal development effort. Supports multiple installed plugins.
|
||||||
|
5. **Web Dashboard** - Visual configuration management, plugin controls, logging, and WebChat interface for direct LLM interaction.
|
||||||
|
6. **High Stability & Modularity** - Event bus and pipeline architecture ensures high modularization and loose coupling.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Dashboard Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
|
||||||
|
> Username: `astrbot`, Password: `astrbot` (LLM not configured for chat page)
|
||||||
|
|
||||||
|
## ✨ Deployment
|
||||||
|
|
||||||
|
#### Docker Deployment
|
||||||
|
|
||||||
|
See docs: [Deploy with Docker](https://astrbot.app/deploy/astrbot/docker.html#docker-deployment)
|
||||||
|
|
||||||
|
#### Windows Installer
|
||||||
|
|
||||||
|
Requires Python (>3.10). See docs: [Windows Installer Guide](https://astrbot.app/deploy/astrbot/windows.html)
|
||||||
|
|
||||||
|
#### Replit Deployment
|
||||||
|
|
||||||
|
[](https://repl.it/github/Soulter/AstrBot)
|
||||||
|
|
||||||
|
#### CasaOS Deployment
|
||||||
|
|
||||||
|
Community-contributed method.
|
||||||
|
See docs: [CasaOS Deployment](https://astrbot.app/deploy/astrbot/casaos.html)
|
||||||
|
|
||||||
|
#### Manual Deployment
|
||||||
|
|
||||||
|
See docs: [Source Code Deployment](https://astrbot.app/deploy/astrbot/cli.html)
|
||||||
|
|
||||||
|
## ⚡ Platform Support
|
||||||
|
|
||||||
|
| Platform | Status | Details | Message Types |
|
||||||
|
| -------------------------------------------------------------- | ------ | ------------------- | ------------------- |
|
||||||
|
| QQ (Official Bot) | ✔ | Private/Group chats | Text, Images |
|
||||||
|
| QQ (OneBot) | ✔ | Private/Group chats | Text, Images, Voice |
|
||||||
|
| WeChat (Personal) | ✔ | Private/Group chats | Text, Images, Voice |
|
||||||
|
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | Private/Group chats | Text, Images |
|
||||||
|
| [WeChat Work](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | Private chats | Text, Images, Voice |
|
||||||
|
| Feishu | ✔ | Group chats | Text, Images |
|
||||||
|
| WeChat Open Platform | 🚧 | Planned | - |
|
||||||
|
| Discord | 🚧 | Planned | - |
|
||||||
|
| WhatsApp | 🚧 | Planned | - |
|
||||||
|
| Xiaomi Speakers | 🚧 | Planned | - |
|
||||||
|
|
||||||
|
## Provider Support Status
|
||||||
|
|
||||||
|
| Name | Support | Type | Notes |
|
||||||
|
|---------------------------|---------|------------------------|-----------------------------------------------------------------------|
|
||||||
|
| OpenAI API | ✔ | Text Generation | Supports all OpenAI API-compatible services including DeepSeek, Google Gemini, GLM, Moonshot, Alibaba Cloud Bailian, Silicon Flow, xAI, etc. |
|
||||||
|
| Claude API | ✔ | Text Generation | |
|
||||||
|
| Google Gemini API | ✔ | Text Generation | |
|
||||||
|
| Dify | ✔ | LLMOps | |
|
||||||
|
| DashScope (Alibaba Cloud) | ✔ | LLMOps | |
|
||||||
|
| Ollama | ✔ | Model Loader | Local deployment for open-source LLMs (DeepSeek, Llama, etc.) |
|
||||||
|
| LM Studio | ✔ | Model Loader | Local deployment for open-source LLMs (DeepSeek, Llama, etc.) |
|
||||||
|
| LLMTuner | ✔ | Model Loader | Local loading of fine-tuned models (e.g. LoRA) |
|
||||||
|
| OneAPI | ✔ | LLM Distribution | |
|
||||||
|
| Whisper | ✔ | Speech-to-Text | Supports API and local deployment |
|
||||||
|
| SenseVoice | ✔ | Speech-to-Text | Local deployment |
|
||||||
|
| OpenAI TTS API | ✔ | Text-to-Speech | |
|
||||||
|
| Fishaudio | ✔ | Text-to-Speech | Project involving GPT-Sovits author |
|
||||||
|
|
||||||
|
# 🦌 Roadmap
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Suggestions welcome via Issues <3
|
||||||
|
|
||||||
|
- [ ] Ensure feature parity across all platform adapters
|
||||||
|
- [ ] Optimize plugin APIs
|
||||||
|
- [ ] Add default TTS services (e.g., GPT-Sovits)
|
||||||
|
- [ ] Enhance chat features with persistent memory
|
||||||
|
- [ ] i18n Planning
|
||||||
|
|
||||||
|
## ❤️ Contributions
|
||||||
|
|
||||||
|
All Issues/PRs welcome! Simply submit your changes to this project :)
|
||||||
|
|
||||||
|
For major features, please discuss via Issues first.
|
||||||
|
|
||||||
|
## 🌟 Support
|
||||||
|
|
||||||
|
- Star this project!
|
||||||
|
- Support via [Afdian](https://afdian.com/a/soulter)
|
||||||
|
- WeChat support: [QR Code](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)
|
||||||
|
|
||||||
|
## ✨ Demos
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Code executor file I/O currently tested with Napcat(QQ)/Lagrange(QQ)
|
||||||
|
|
||||||
|
<div align='center'>
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
|
||||||
|
|
||||||
|
_✨ Docker-based Sandboxed Code Executor (Beta) ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
|
||||||
|
|
||||||
|
_✨ Multimodal Input, Web Search, Text-to-Image ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/8ec12797-e70f-460a-959e-48eca39ca2bb" height=100>
|
||||||
|
|
||||||
|
_✨ Natural Language TODO Lists ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
|
||||||
|
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
|
||||||
|
|
||||||
|
_✨ Plugin System Showcase ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/592a8630-14c7-4e06-b496-9c0386e4f36c" width=600>
|
||||||
|
|
||||||
|
_✨ Web Dashboard ✨_
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_✨ Built-in Web Chat Interface ✨_
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ⭐ Star History
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> If this project helps you, please give it a star <3
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://star-history.com/#soulter/astrbot&Date)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
1. Licensed under `AGPL-v3`.
|
||||||
|
2. WeChat integration uses [Gewechat](https://github.com/Devo919/Gewechat). Use at your own risk with non-critical accounts.
|
||||||
|
3. Users must comply with local laws and regulations.
|
||||||
|
|
||||||
|
<!-- ## ✨ ATRI [Beta]
|
||||||
|
|
||||||
|
Available as plugin: [astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
|
||||||
|
|
||||||
|
1. Qwen1.5-7B-Chat Lora model fine-tuned with ATRI character data
|
||||||
|
2. Long-term memory
|
||||||
|
3. Meme understanding & responses
|
||||||
|
4. TTS integration
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
167
README_ja.md
Normal file
167
README_ja.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<p align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
_✨ 簡単に使えるマルチプラットフォーム LLM チャットボットおよび開発フレームワーク ✨_
|
||||||
|
|
||||||
|
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||||
|
|
||||||
|
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-630166526-purple">
|
||||||
|
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||||
|

|
||||||
|
[](https://codecov.io/gh/Soulter/AstrBot)
|
||||||
|
|
||||||
|
<a href="https://astrbot.app/">ドキュメントを見る</a> |
|
||||||
|
<a href="https://github.com/Soulter/AstrBot/issues">問題を報告する</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
AstrBot は、疎結合、非同期、複数のメッセージプラットフォームに対応したデプロイ、使いやすいプラグインシステム、および包括的な大規模言語モデル(LLM)接続機能を備えたチャットボットおよび開発フレームワークです。
|
||||||
|
|
||||||
|
## ✨ 主な機能
|
||||||
|
|
||||||
|
1. **大規模言語モデルの対話**。OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM など、さまざまな大規模言語モデルをサポートし、Ollama、LLMTuner を介してローカルにデプロイされた大規模モデルをサポートします。多輪対話、人格シナリオ、多モーダル機能を備え、画像理解、音声からテキストへの変換(Whisper)をサポートします。
|
||||||
|
2. **複数のメッセージプラットフォームの接続**。QQ(OneBot)、QQ チャンネル、Feishu、Telegram への接続をサポートします。今後、DingTalk、Discord、WhatsApp、Xiaoai 音響をサポートする予定です。レート制限、ホワイトリスト、キーワードフィルタリング、Baidu コンテンツ監査をサポートします。
|
||||||
|
3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://dify.ai/)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。
|
||||||
|
4. **プラグインの拡張**。深く最適化されたプラグインメカニズムを備え、[プラグインの開発](https://astrbot.app/dev/plugin.html)をサポートし、機能を拡張できます。複数のプラグインのインストールをサポートします。
|
||||||
|
5. **ビジュアル管理パネル**。設定の視覚的な変更、プラグイン管理、ログの表示などをサポートし、設定の難易度を低減します。WebChat を統合し、パネル上で大規模モデルと対話できます。
|
||||||
|
6. **高い安定性と高いモジュール性**。イベントバスとパイプラインに基づくアーキテクチャ設計により、高度にモジュール化され、低結合です。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 管理パネルのオンラインデモを体験する: [https://demo.astrbot.app/](https://demo.astrbot.app/)
|
||||||
|
>
|
||||||
|
> ユーザー名: `astrbot`, パスワード: `astrbot`。LLM が設定されていないため、チャットページで大規模モデルを使用することはできません。(デモのログインパスワードを変更しないでください 😭)
|
||||||
|
|
||||||
|
## ✨ 使用方法
|
||||||
|
|
||||||
|
#### Docker デプロイ
|
||||||
|
|
||||||
|
公式ドキュメント [Docker を使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) を参照してください。
|
||||||
|
|
||||||
|
#### Windows ワンクリックインストーラーのデプロイ
|
||||||
|
|
||||||
|
コンピュータに Python(>3.10)がインストールされている必要があります。公式ドキュメント [Windows ワンクリックインストーラーを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/windows.html) を参照してください。
|
||||||
|
|
||||||
|
#### Replit デプロイ
|
||||||
|
|
||||||
|
[](https://repl.it/github/Soulter/AstrBot)
|
||||||
|
|
||||||
|
#### CasaOS デプロイ
|
||||||
|
|
||||||
|
コミュニティが提供するデプロイ方法です。
|
||||||
|
|
||||||
|
公式ドキュメント [ソースコードを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/casaos.html) を参照してください。
|
||||||
|
|
||||||
|
#### 手動デプロイ
|
||||||
|
|
||||||
|
公式ドキュメント [ソースコードを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/cli.html) を参照してください。
|
||||||
|
|
||||||
|
## ⚡ メッセージプラットフォームのサポート状況
|
||||||
|
|
||||||
|
| プラットフォーム | サポート状況 | 詳細 | メッセージタイプ |
|
||||||
|
| -------- | ------- | ------- | ------ |
|
||||||
|
| QQ(公式ロボットインターフェース) | ✔ | プライベートチャット、グループチャット、QQ チャンネルプライベートチャット、グループチャット | テキスト、画像 |
|
||||||
|
| QQ(OneBot) | ✔ | プライベートチャット、グループチャット | テキスト、画像、音声 |
|
||||||
|
| WeChat(個人アカウント) | ✔ | WeChat 個人アカウントのプライベートチャット、グループチャット | テキスト、画像、音声 |
|
||||||
|
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | プライベートチャット、グループチャット | テキスト、画像 |
|
||||||
|
| [WeChat(企業 WeChat)](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | プライベートチャット | テキスト、画像、音声 |
|
||||||
|
| Feishu | ✔ | グループチャット | テキスト、画像 |
|
||||||
|
| WeChat 対話オープンプラットフォーム | 🚧 | 計画中 | - |
|
||||||
|
| Discord | 🚧 | 計画中 | - |
|
||||||
|
| WhatsApp | 🚧 | 計画中 | - |
|
||||||
|
| Xiaoai 音響 | 🚧 | 計画中 | - |
|
||||||
|
|
||||||
|
# 🦌 今後のロードマップ
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Issue でさらに多くの提案を歓迎します <3
|
||||||
|
|
||||||
|
- [ ] 現在のすべてのプラットフォームアダプターの機能の一貫性を確保し、改善する
|
||||||
|
- [ ] プラグインインターフェースの最適化
|
||||||
|
- [ ] GPT-Sovits などの TTS サービスをデフォルトでサポート
|
||||||
|
- [ ] "チャット強化" 部分を完成させ、永続的な記憶をサポート
|
||||||
|
- [ ] i18n の計画
|
||||||
|
|
||||||
|
## ❤️ 貢献
|
||||||
|
|
||||||
|
Issue や Pull Request を歓迎します!このプロジェクトに変更を加えるだけです :)
|
||||||
|
|
||||||
|
新機能の追加については、まず Issue で議論してください。
|
||||||
|
|
||||||
|
## 🌟 サポート
|
||||||
|
|
||||||
|
- このプロジェクトに Star を付けてください!
|
||||||
|
- [愛発電](https://afdian.com/a/soulter)で私をサポートしてください!
|
||||||
|
- [WeChat](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)で私をサポートしてください~
|
||||||
|
|
||||||
|
## ✨ デモ
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> コードエグゼキューターのファイル入力/出力は現在 Napcat(QQ)、Lagrange(QQ) でのみテストされています
|
||||||
|
|
||||||
|
<div align='center'>
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
|
||||||
|
|
||||||
|
_✨ Docker ベースのサンドボックス化されたコードエグゼキューター(ベータテスト中)✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
|
||||||
|
|
||||||
|
_✨ 多モーダル、ウェブ検索、長文の画像変換(設定可能)✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/8ec12797-e70f-460a-959e-48eca39ca2bb" height=100>
|
||||||
|
|
||||||
|
_✨ 自然言語タスク ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
|
||||||
|
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
|
||||||
|
|
||||||
|
_✨ プラグインシステム - 一部のプラグインの展示 ✨_
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/592a8630-14c7-4e06-b496-9c0386e4f36c" width="600">
|
||||||
|
|
||||||
|
_✨ 管理パネル ✨_
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_✨ 内蔵 Web Chat、オンラインでボットと対話 ✨_
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ⭐ Star History
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> このプロジェクトがあなたの生活や仕事に役立った場合、またはこのプロジェクトの将来の発展に関心がある場合は、プロジェクトに Star を付けてください。これはこのオープンソースプロジェクトを維持するためのモチベーションです <3
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://star-history.com/#soulter/astrbot&Date)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## スポンサー
|
||||||
|
|
||||||
|
[<img src="https://api.gitsponsors.com/api/badge/img?id=575865240" height="20">](https://api.gitsponsors.com/api/badge/link?p=XEpbdGxlitw/RbcwiTX93UMzNK/jgDYC8NiSzamIPMoKvG2lBFmyXhSS/b0hFoWlBBMX2L5X5CxTDsUdyvcIEHTOfnkXz47UNOZvMwyt5CzbYpq0SEzsSV1OJF1cCo90qC/ZyYKYOWedal3MhZ3ikw==)
|
||||||
|
|
||||||
|
## 免責事項
|
||||||
|
|
||||||
|
1. このプロジェクトは `AGPL-v3` オープンソースライセンスの下で保護されています。
|
||||||
|
2. このプロジェクトを使用する際は、現地の法律および規制を遵守してください。
|
||||||
|
|
||||||
|
<!-- ## ✨ ATRI [ベータテスト]
|
||||||
|
|
||||||
|
この機能はプラグインとしてロードされます。プラグインリポジトリのアドレス:[astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
|
||||||
|
|
||||||
|
1. 《ATRI ~ My Dear Moments》の主人公 ATRI のキャラクターセリフを微調整データセットとして使用した `Qwen1.5-7B-Chat Lora` 微調整モデル。
|
||||||
|
2. 長期記憶
|
||||||
|
3. ミームの理解と返信
|
||||||
|
4. TTS
|
||||||
|
-->
|
||||||
|
|
||||||
|
_私は、高性能ですから!_
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from aip import AipContentCensor
|
|
||||||
|
|
||||||
|
|
||||||
class BaiduJudge:
|
|
||||||
def __init__(self, baidu_configs) -> None:
|
|
||||||
if 'app_id' in baidu_configs and 'api_key' in baidu_configs and 'secret_key' in baidu_configs:
|
|
||||||
self.app_id = str(baidu_configs['app_id'])
|
|
||||||
self.api_key = baidu_configs['api_key']
|
|
||||||
self.secret_key = baidu_configs['secret_key']
|
|
||||||
self.client = AipContentCensor(
|
|
||||||
self.app_id, self.api_key, self.secret_key)
|
|
||||||
else:
|
|
||||||
raise ValueError("Baidu configs error! 请填写百度内容审核服务相关配置!")
|
|
||||||
|
|
||||||
def judge(self, text):
|
|
||||||
res = self.client.textCensorUserDefined(text)
|
|
||||||
if 'conclusionType' not in res:
|
|
||||||
return False, "百度审核服务未知错误"
|
|
||||||
if res['conclusionType'] == 1:
|
|
||||||
return True, "合规"
|
|
||||||
else:
|
|
||||||
if 'data' not in res:
|
|
||||||
return False, "百度审核服务未知错误"
|
|
||||||
count = len(res['data'])
|
|
||||||
info = f"百度审核服务发现 {count} 处违规:\n"
|
|
||||||
for i in res['data']:
|
|
||||||
info += f"{i['msg']};\n"
|
|
||||||
info += "\n判断结果:"+res['conclusion']
|
|
||||||
return False, info
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.page-breadcrumb .v-toolbar{background:transparent}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{x as i,o as l,c as _,w as s,a as e,f as a,J as m,V as c,b as t,t as u,ad as p,B as n,ae as o,j as f}from"./index-dc96e1be.js";const b={class:"text-h3"},h={class:"d-flex align-center"},g={class:"d-flex align-center"},V=i({__name:"BaseBreadcrumb",props:{title:String,breadcrumbs:Array,icon:String},setup(d){const r=d;return(x,B)=>(l(),_(c,{class:"page-breadcrumb mb-1 mt-1"},{default:s(()=>[e(a,{cols:"12",md:"12"},{default:s(()=>[e(m,{variant:"outlined",elevation:"0",class:"px-4 py-3 withbg"},{default:s(()=>[e(c,{"no-gutters":"",class:"align-center"},{default:s(()=>[e(a,{md:"5"},{default:s(()=>[t("h3",b,u(r.title),1)]),_:1}),e(a,{md:"7",sm:"12",cols:"12"},{default:s(()=>[e(p,{items:r.breadcrumbs,class:"text-h5 justify-md-end pa-1"},{divider:s(()=>[t("div",h,[e(n(o),{size:"17"})])]),prepend:s(()=>[e(f,{size:"small",icon:"mdi-home",class:"text-secondary mr-2"}),t("div",g,[e(n(o),{size:"17"})])]),_:1},8,["items"])]),_:1})]),_:1})]),_:1})]),_:1})]),_:1}))}});export{V as _};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{x as e,o as a,c as t,w as o,a as s,B as n,Z as r,W as c}from"./index-dc96e1be.js";const f=e({__name:"BlankLayout",setup(p){return(u,_)=>(a(),t(c,null,{default:o(()=>[s(n(r))]),_:1}))}});export{f as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as m}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as p,D as a,o as r,s,a as e,w as t,f as o,V as i,F as n,u as g,c as h,a0 as b,e as x,t as y}from"./index-dc96e1be.js";const P=p({__name:"ColorPage",setup(C){const c=a({title:"Colors Page"}),d=a([{title:"Utilities",disabled:!1,href:"#"},{title:"Colors",disabled:!0,href:"#"}]),u=a(["primary","lightprimary","secondary","lightsecondary","info","success","accent","warning","error","darkText","lightText","borderLight","inputBorder","containerBg"]);return(V,k)=>(r(),s(n,null,[e(m,{title:c.value.title,breadcrumbs:d.value},null,8,["title","breadcrumbs"]),e(i,null,{default:t(()=>[e(o,{cols:"12",md:"12"},{default:t(()=>[e(_,{title:"Color Palette"},{default:t(()=>[e(i,null,{default:t(()=>[(r(!0),s(n,null,g(u.value,(l,f)=>(r(),h(o,{md:"3",cols:"12",key:f},{default:t(()=>[e(b,{rounded:"md",class:"align-center justify-center d-flex",height:"100",width:"100%",color:l},{default:t(()=>[x("class: "+y(l),1)]),_:2},1032,["color"])]),_:2},1024))),128))]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{P as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{o as l,s as o,u as c,c as n,w as u,Q as g,b as s,R as k,F as t,ab as h,O as p,t as m,a as V,ac as f,i as C,q as x,k as v,A as U}from"./index-dc96e1be.js";import{_ as w}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";const S={__name:"ConfigDetailCard",props:{config:Array},setup(d){return(y,B)=>(l(!0),o(t,null,c(d.config,r=>(l(),n(w,{key:r.name,title:r.name,style:{"margin-bottom":"16px"}},{default:u(()=>[g(s("a",null,"No data",512),[[k,d.config.length===0]]),(l(!0),o(t,null,c(r.body,e=>(l(),o(t,null,[e.config_type==="item"?(l(),o(t,{key:0},[e.val_type==="bool"?(l(),n(h,{key:0,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,color:"primary",inset:""},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="str"?(l(),n(p,{key:1,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,style:{"margin-bottom":"8px"},variant:"outlined"},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="int"?(l(),n(p,{key:2,modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,label:e.name,hint:e.description,style:{"margin-bottom":"8px"},variant:"outlined"},null,8,["modelValue","onUpdate:modelValue","label","hint"])):e.val_type==="list"?(l(),o(t,{key:3},[s("span",null,m(e.name),1),V(f,{modelValue:e.value,"onUpdate:modelValue":a=>e.value=a,chips:"",clearable:"",label:"请添加",multiple:"","prepend-icon":"mdi-tag-multiple-outline"},{selection:u(({attrs:a,item:i,select:b,selected:_})=>[V(C,x(a,{"model-value":_,closable:"",onClick:b,"onClick:close":D=>y.remove(i)}),{default:u(()=>[s("strong",null,m(i),1)]),_:2},1040,["model-value","onClick","onClick:close"])]),_:2},1032,["modelValue","onUpdate:modelValue"])],64)):v("",!0)],64)):e.config_type==="divider"?(l(),n(U,{key:1,style:{"margin-top":"8px","margin-bottom":"8px"}})):v("",!0)],64))),256))]),_:2},1032,["title"]))),128))}};export{S as _};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as y}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as h,o,c as u,w as t,a,a8 as b,b as c,K as x,e as f,t as g,G as V,A as w,L as S,a9 as $,J as B,s as _,d as v,F as d,u as p,f as G,V as T,aa as j,T as l}from"./index-dc96e1be.js";import{_ as m}from"./ConfigDetailCard-8467c848.js";const D={class:"d-sm-flex align-center justify-space-between"},C=h({__name:"ConfigGroupCard",props:{title:String},setup(e){const s=e;return(i,n)=>(o(),u(B,{variant:"outlined",elevation:"0",class:"withbg",style:{width:"50%"}},{default:t(()=>[a(b,{style:{padding:"10px 20px"}},{default:t(()=>[c("div",D,[a(x,null,{default:t(()=>[f(g(s.title),1)]),_:1}),a(V)])]),_:1}),a(w),a(S,null,{default:t(()=>[$(i.$slots,"default")]),_:3})]),_:3}))}}),I={style:{display:"flex","flex-direction":"row","justify-content":"space-between","align-items":"center","margin-bottom":"12px"}},N={style:{display:"flex","flex-direction":"row"}},R={style:{"margin-right":"10px",color:"black"}},F={style:{color:"#222"}},k=h({__name:"ConfigGroupItem",props:{title:String,desc:String,btnRoute:String,namespace:String},setup(e){const s=e;return(i,n)=>(o(),_("div",I,[c("div",N,[c("h3",R,g(s.title),1),c("p",F,g(s.desc),1)]),a(v,{to:s.btnRoute,color:"primary",class:"ml-2",style:{"border-radius":"10px"}},{default:t(()=>[f("配置")]),_:1},8,["to"])]))}}),L={style:{display:"flex","flex-direction":"row",padding:"16px",gap:"16px",width:"100%"}},P={name:"ConfigPage",components:{UiParentCard:y,ConfigGroupCard:C,ConfigGroupItem:k,ConfigDetailCard:m},data(){return{config_data:[],config_base:[],save_message_snack:!1,save_message:"",save_message_success:"",config_outline:[],namespace:""}},mounted(){this.getConfig()},methods:{switchConfig(e){l.get("/api/configs?namespace="+e).then(s=>{this.namespace=e,this.config_data=s.data.data,console.log(this.config_data)}).catch(s=>{save_message=s,save_message_snack=!0,save_message_success="error"})},getConfig(){l.get("/api/config_outline").then(e=>{this.config_outline=e.data.data,console.log(this.config_outline)}).catch(e=>{save_message=e,save_message_snack=!0,save_message_success="error"}),l.get("/api/configs").then(e=>{this.config_base=e.data.data,console.log(this.config_data)}).catch(e=>{save_message=e,save_message_snack=!0,save_message_success="error"})},updateConfig(){l.post("/api/configs",{base_config:this.config_base,config:this.config_data,namespace:this.namespace}).then(e=>{e.data.status==="success"?(this.save_message=e.data.message,this.save_message_snack=!0,this.save_message_success="success"):(this.save_message=e.data.message,this.save_message_snack=!0,this.save_message_success="error")}).catch(e=>{this.save_message=e,this.save_message_snack=!0,this.save_message_success="error"})}}},J=Object.assign(P,{setup(e){return(s,i)=>(o(),_(d,null,[a(T,null,{default:t(()=>[c("div",L,[(o(!0),_(d,null,p(s.config_outline,n=>(o(),u(C,{key:n.name,title:n.name},{default:t(()=>[(o(!0),_(d,null,p(n.body,r=>(o(),u(k,{title:r.title,desc:r.desc,namespace:r.namespace,onClick:U=>s.switchConfig(r.namespace)},null,8,["title","desc","namespace","onClick"]))),256))]),_:2},1032,["title"]))),128))]),a(G,{cols:"12",md:"12"},{default:t(()=>[a(m,{config:s.config_data},null,8,["config"]),a(m,{config:s.config_base},null,8,["config"])]),_:1})]),_:1}),a(v,{icon:"mdi-content-save",size:"x-large",style:{position:"fixed",right:"52px",bottom:"52px"},color:"darkprimary",onClick:s.updateConfig},null,8,["onClick"]),a(j,{timeout:2e3,elevation:"24",color:s.save_message_success,modelValue:s.save_message_snack,"onUpdate:modelValue":i[0]||(i[0]=n=>s.save_message_snack=n)},{default:t(()=>[f(g(s.save_message),1)]),_:1},8,["color","modelValue"])],64))}});export{J as default};
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
||||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
||||||
* https://github.com/chjj/term.js
|
|
||||||
* @license MIT
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* Originally forked from (with the author's permission):
|
|
||||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
||||||
* http://bellard.org/jslinux/
|
|
||||||
* Copyright (c) 2011 Fabrice Bellard
|
|
||||||
* The original design remains. The terminal itself
|
|
||||||
* has been extended to include xterm CSI codes, among
|
|
||||||
* other features.
|
|
||||||
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
import{_ as t}from"./_plugin-vue_export-helper-c27b6911.js";import{o,c,w as s,V as i,a as r,b as e,d as l,e as a,f as d}from"./index-dc96e1be.js";const n="/assets/img-error-bg-ab6474a0.svg",_="/assets/img-error-blue-2675a7a9.svg",m="/assets/img-error-text-a6aebfa0.svg",g="/assets/img-error-purple-edee3fbc.svg";const p={},u={class:"text-center"},f=e("div",{class:"CardMediaWrapper"},[e("img",{src:n,alt:"grid",class:"w-100"}),e("img",{src:_,alt:"grid",class:"CardMediaParts"}),e("img",{src:m,alt:"build",class:"CardMediaBuild"}),e("img",{src:g,alt:"build",class:"CardMediaBuild"})],-1),h=e("h1",{class:"text-h1"},"Something is wrong",-1),v=e("p",null,[e("small",null,[a("The page you are looking was moved, removed, "),e("br"),a("renamed, or might never exist! ")])],-1);function x(b,V){return o(),c(i,{"no-gutters":"",class:"h-100vh"},{default:s(()=>[r(d,{class:"d-flex align-center justify-center"},{default:s(()=>[e("div",u,[f,h,v,r(l,{variant:"flat",color:"primary",class:"mt-4",to:"/","prepend-icon":"mdi-home"},{default:s(()=>[a(" Home")]),_:1})])]),_:1})]),_:1})}const C=t(p,[["render",x]]);export{C as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.CardMediaWrapper{max-width:720px;margin:0 auto;position:relative}.CardMediaBuild{position:absolute;top:0;left:0;width:100%;animation:5s bounce ease-in-out infinite}.CardMediaParts{position:absolute;top:0;left:0;width:100%;animation:10s blink ease-in-out infinite}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.custom-devider{border-color:#00000014!important}.googleBtn{border-color:#00000014;margin:30px 0 20px}.outlinedInput .v-field{border:1px solid rgba(0,0,0,.08);box-shadow:none}.orbtn{padding:2px 40px;border-color:#00000014;margin:20px 15px}.pwdInput{position:relative}.pwdInput .v-input__append{position:absolute;right:10px;top:50%;transform:translateY(-50%)}.loginForm .v-text-field .v-field--active input{font-weight:500}.loginBox{max-width:475px;margin:0 auto}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{av as _,x as d,D as n,o as c,s as m,a as f,w as p,Q as r,b as a,R as o,B as t,aw as h}from"./index-dc96e1be.js";const s={Sidebar_drawer:!0,Customizer_drawer:!1,mini_sidebar:!1,fontTheme:"Roboto",inputBg:!1},l=_({id:"customizer",state:()=>({Sidebar_drawer:s.Sidebar_drawer,Customizer_drawer:s.Customizer_drawer,mini_sidebar:s.mini_sidebar,fontTheme:"Poppins",inputBg:s.inputBg}),getters:{},actions:{SET_SIDEBAR_DRAWER(){this.Sidebar_drawer=!this.Sidebar_drawer},SET_MINI_SIDEBAR(e){this.mini_sidebar=e},SET_FONT(e){this.fontTheme=e}}}),u={class:"logo",style:{display:"flex","align-items":"center"}},b={style:{"font-size":"24px","font-weight":"1000"}},w={style:{"font-size":"20px","font-weight":"1000"}},S={style:{"font-size":"20px"}},z=d({__name:"LogoDark",setup(e){n("rgb(var(--v-theme-primary))"),n("rgb(var(--v-theme-secondary))");const i=l();return(g,B)=>(c(),m("div",u,[f(t(h),{to:"/",style:{"text-decoration":"none",color:"black"}},{default:p(()=>[r(a("span",b,"AstrBot 仪表盘",512),[[o,!t(i).mini_sidebar]]),r(a("span",w,"Astr",512),[[o,t(i).mini_sidebar]]),r(a("span",S,"Bot",512),[[o,t(i).mini_sidebar]])]),_:1})]))}});export{z as _,l as u};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as o}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as i}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as n,D as a,o as c,s as m,a as e,w as t,f as d,b as f,V as _,F as u}from"./index-dc96e1be.js";const p=["innerHTML"],v=n({__name:"MaterialIcons",setup(b){const s=a({title:"Material Icons"}),r=a('<iframe src="https://materialdesignicons.com/" frameborder="0" width="100%" height="1000"></iframe>'),l=a([{title:"Icons",disabled:!1,href:"#"},{title:"Material Icons",disabled:!0,href:"#"}]);return(h,M)=>(c(),m(u,null,[e(o,{title:s.value.title,breadcrumbs:l.value},null,8,["title","breadcrumbs"]),e(_,null,{default:t(()=>[e(d,{cols:"12",md:"12"},{default:t(()=>[e(i,{title:"Material Icons"},{default:t(()=>[f("div",{innerHTML:r.value},null,8,p)]),_:1})]),_:1})]),_:1})],64))}});export{v as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as B}from"./LogoDark.vue_vue_type_script_setup_true_lang-7df35c25.js";import{x as y,D as o,o as b,s as U,a as e,w as a,b as n,B as $,d as u,f as d,A as _,e as f,V as r,O as m,ap as A,au as E,F,c as T,N as q,J as V,L as P}from"./index-dc96e1be.js";const z="/assets/social-google-a359a253.svg",N=["src"],S=n("span",{class:"ml-2"},"Sign up with Google",-1),D=n("h5",{class:"text-h5 text-center my-4 mb-8"},"Sign up with Email address",-1),G={class:"d-sm-inline-flex align-center mt-2 mb-7 mb-sm-0 font-weight-bold"},L=n("a",{href:"#",class:"ml-1 text-lightText"},"Terms and Condition",-1),O={class:"mt-5 text-right"},j=y({__name:"AuthRegister",setup(w){const c=o(!1),i=o(!1),p=o(""),v=o(""),g=o(),h=o(""),x=o(""),k=o([s=>!!s||"Password is required",s=>s&&s.length<=10||"Password must be less than 10 characters"]),C=o([s=>!!s||"E-mail is required",s=>/.+@.+\..+/.test(s)||"E-mail must be valid"]);function R(){g.value.validate()}return(s,l)=>(b(),U(F,null,[e(u,{block:"",color:"primary",variant:"outlined",class:"text-lightText googleBtn"},{default:a(()=>[n("img",{src:$(z),alt:"google"},null,8,N),S]),_:1}),e(r,null,{default:a(()=>[e(d,{class:"d-flex align-center"},{default:a(()=>[e(_,{class:"custom-devider"}),e(u,{variant:"outlined",class:"orbtn",rounded:"md",size:"small"},{default:a(()=>[f("OR")]),_:1}),e(_,{class:"custom-devider"})]),_:1})]),_:1}),D,e(E,{ref_key:"Regform",ref:g,"lazy-validation":"",action:"/dashboards/analytical",class:"mt-7 loginForm"},{default:a(()=>[e(r,null,{default:a(()=>[e(d,{cols:"12",sm:"6"},{default:a(()=>[e(m,{modelValue:h.value,"onUpdate:modelValue":l[0]||(l[0]=t=>h.value=t),density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary",label:"Firstname"},null,8,["modelValue"])]),_:1}),e(d,{cols:"12",sm:"6"},{default:a(()=>[e(m,{modelValue:x.value,"onUpdate:modelValue":l[1]||(l[1]=t=>x.value=t),density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary",label:"Lastname"},null,8,["modelValue"])]),_:1})]),_:1}),e(m,{modelValue:v.value,"onUpdate:modelValue":l[2]||(l[2]=t=>v.value=t),rules:C.value,label:"Email Address / Username",class:"mt-4 mb-4",required:"",density:"comfortable","hide-details":"auto",variant:"outlined",color:"primary"},null,8,["modelValue","rules"]),e(m,{modelValue:p.value,"onUpdate:modelValue":l[3]||(l[3]=t=>p.value=t),rules:k.value,label:"Password",required:"",density:"comfortable",variant:"outlined",color:"primary","hide-details":"auto","append-icon":i.value?"mdi-eye":"mdi-eye-off",type:i.value?"text":"password","onClick:append":l[4]||(l[4]=t=>i.value=!i.value),class:"pwdInput"},null,8,["modelValue","rules","append-icon","type"]),n("div",G,[e(A,{modelValue:c.value,"onUpdate:modelValue":l[5]||(l[5]=t=>c.value=t),rules:[t=>!!t||"You must agree to continue!"],label:"Agree with?",required:"",color:"primary",class:"ms-n2","hide-details":""},null,8,["modelValue","rules"]),L]),e(u,{color:"secondary",block:"",class:"mt-2",variant:"flat",size:"large",onClick:l[6]||(l[6]=t=>R())},{default:a(()=>[f("Sign Up")]),_:1})]),_:1},512),n("div",O,[e(_),e(u,{variant:"plain",to:"/auth/login",class:"mt-2 text-capitalize mr-n2"},{default:a(()=>[f("Already have an account?")]),_:1})])],64))}});const I={class:"pa-7 pa-sm-12"},J=n("h2",{class:"text-secondary text-h2 mt-8"},"Sign up",-1),Y=n("h4",{class:"text-disabled text-h4 mt-3"},"Enter credentials to continue",-1),M=y({__name:"RegisterPage",setup(w){return(c,i)=>(b(),T(r,{class:"h-100vh","no-gutters":""},{default:a(()=>[e(d,{cols:"12",class:"d-flex align-center bg-lightprimary"},{default:a(()=>[e(q,null,{default:a(()=>[n("div",I,[e(r,{justify:"center"},{default:a(()=>[e(d,{cols:"12",lg:"10",xl:"6",md:"7"},{default:a(()=>[e(V,{elevation:"0",class:"loginBox"},{default:a(()=>[e(V,{variant:"outlined"},{default:a(()=>[e(P,{class:"pa-9"},{default:a(()=>[e(r,null,{default:a(()=>[e(d,{cols:"12",class:"text-center"},{default:a(()=>[e(B),J,Y]),_:1})]),_:1}),e(j)]),_:1})]),_:1})]),_:1})]),_:1})]),_:1})])]),_:1})]),_:1})]),_:1}))}});export{M as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
.custom-devider{border-color:#00000014!important}.googleBtn{border-color:#00000014;margin:30px 0 20px}.outlinedInput .v-field{border:1px solid rgba(0,0,0,.08);box-shadow:none}.orbtn{padding:2px 40px;border-color:#00000014;margin:20px 15px}.pwdInput{position:relative}.pwdInput .v-input__append{position:absolute;right:10px;top:50%;transform:translateY(-50%)}.loginBox{max-width:475px;margin:0 auto}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as c}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as f}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as m,D as s,o as l,s as r,a as e,w as a,f as i,V as o,F as d,u as _,J as p,X as b,b as h,t as g}from"./index-dc96e1be.js";const v=m({__name:"ShadowPage",setup(w){const n=s({title:"Shadow Page"}),u=s([{title:"Utilities",disabled:!1,href:"#"},{title:"Shadow",disabled:!0,href:"#"}]);return(V,x)=>(l(),r(d,null,[e(c,{title:n.value.title,breadcrumbs:u.value},null,8,["title","breadcrumbs"]),e(o,null,{default:a(()=>[e(i,{cols:"12",md:"12"},{default:a(()=>[e(f,{title:"Basic Shadow"},{default:a(()=>[e(o,{justify:"center"},{default:a(()=>[(l(),r(d,null,_(25,t=>e(i,{key:t,cols:"auto"},{default:a(()=>[e(p,{height:"100",width:"100",class:b(["mb-5",["d-flex justify-center align-center bg-primary",`elevation-${t}`]])},{default:a(()=>[h("div",null,g(t-1),1)]),_:2},1032,["class"])]),_:2},1024)),64))]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{v as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as o}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as n}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as c,D as a,o as i,s as m,a as e,w as t,f as d,b as f,V as _,F as u}from"./index-dc96e1be.js";const b=["innerHTML"],w=c({__name:"TablerIcons",setup(p){const s=a({title:"Tabler Icons"}),r=a('<iframe src="https://tablericons.com/" frameborder="0" width="100%" height="600"></iframe>'),l=a([{title:"Icons",disabled:!1,href:"#"},{title:"Tabler Icons",disabled:!0,href:"#"}]);return(h,T)=>(i(),m(u,null,[e(o,{title:s.value.title,breadcrumbs:l.value},null,8,["title","breadcrumbs"]),e(_,null,{default:t(()=>[e(d,{cols:"12",md:"12"},{default:t(()=>[e(n,{title:"Tabler Icons"},{default:t(()=>[f("div",{innerHTML:r.value},null,8,b)]),_:1})]),_:1})]),_:1})],64))}});export{w as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as m}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-e31f96f8.js";import{_ as v}from"./UiParentCard.vue_vue_type_script_setup_true_lang-f2b2db58.js";import{x as f,o as i,c as g,w as e,a,a8 as y,K as b,e as w,t as d,A as C,L as V,a9 as L,J as _,D as o,s as h,f as k,b as t,F as x,u as B,X as H,V as T}from"./index-dc96e1be.js";const s=f({__name:"UiChildCard",props:{title:String},setup(r){const l=r;return(n,c)=>(i(),g(_,{variant:"outlined"},{default:e(()=>[a(y,{class:"py-3"},{default:e(()=>[a(b,{class:"text-h5"},{default:e(()=>[w(d(l.title),1)]),_:1})]),_:1}),a(C),a(V,null,{default:e(()=>[L(n.$slots,"default")]),_:3})]),_:3}))}}),D={class:"d-flex flex-column gap-1"},S={class:"text-caption pa-2 bg-lightprimary"},z=t("div",{class:"text-grey"},"Class",-1),N={class:"font-weight-medium"},$=t("div",null,[t("p",{class:"text-left"},"Left aligned on all viewport sizes."),t("p",{class:"text-center"},"Center aligned on all viewport sizes."),t("p",{class:"text-right"},"Right aligned on all viewport sizes."),t("p",{class:"text-sm-left"},"Left aligned on viewports SM (small) or wider."),t("p",{class:"text-right text-md-left"},"Left aligned on viewports MD (medium) or wider."),t("p",{class:"text-right text-lg-left"},"Left aligned on viewports LG (large) or wider."),t("p",{class:"text-right text-xl-left"},"Left aligned on viewports XL (extra-large) or wider.")],-1),M=t("div",{class:"d-flex justify-space-between flex-row"},[t("a",{href:"#",class:"text-decoration-none"},"Non-underlined link"),t("div",{class:"text-decoration-line-through"},"Line-through text"),t("div",{class:"text-decoration-overline"},"Overline text"),t("div",{class:"text-decoration-underline"},"Underline text")],-1),O=t("div",null,[t("p",{class:"text-high-emphasis"},"High-emphasis has an opacity of 87% in light theme and 100% in dark."),t("p",{class:"text-medium-emphasis"},"Medium-emphasis text and hint text have opacities of 60% in light theme and 70% in dark."),t("p",{class:"text-disabled"},"Disabled text has an opacity of 38% in light theme and 50% in dark.")],-1),j=f({__name:"TypographyPage",setup(r){const l=o({title:"Typography Page"}),n=o([["Heading 1","text-h1"],["Heading 2","text-h2"],["Heading 3","text-h3"],["Heading 4","text-h4"],["Heading 5","text-h5"],["Heading 6","text-h6"],["Subtitle 1","text-subtitle-1"],["Subtitle 2","text-subtitle-2"],["Body 1","text-body-1"],["Body 2","text-body-2"],["Button","text-button"],["Caption","text-caption"],["Overline","text-overline"]]),c=o([{title:"Utilities",disabled:!1,href:"#"},{title:"Typography",disabled:!0,href:"#"}]);return(U,F)=>(i(),h(x,null,[a(m,{title:l.value.title,breadcrumbs:c.value},null,8,["title","breadcrumbs"]),a(T,null,{default:e(()=>[a(k,{cols:"12",md:"12"},{default:e(()=>[a(v,{title:"Basic Typography"},{default:e(()=>[a(s,{title:"Heading"},{default:e(()=>[t("div",D,[(i(!0),h(x,null,B(n.value,([p,u])=>(i(),g(_,{variant:"outlined",key:p,class:"my-4"},{default:e(()=>[t("div",{class:H([u,"pa-2"])},d(p),3),t("div",S,[z,t("div",N,d(u),1)])]),_:2},1024))),128))])]),_:1}),a(s,{title:"Text-alignment",class:"mt-8"},{default:e(()=>[$]),_:1}),a(s,{title:"Decoration",class:"mt-8"},{default:e(()=>[M]),_:1}),a(s,{title:"Opacity",class:"mt-8"},{default:e(()=>[O]),_:1})]),_:1})]),_:1})]),_:1})],64))}});export{j as default};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{x as n,o,c as i,w as e,a,a8 as d,b as c,K as u,e as p,t as _,a9 as s,A as f,L as V,J as m}from"./index-dc96e1be.js";const C={class:"d-sm-flex align-center justify-space-between"},h=n({__name:"UiParentCard",props:{title:String},setup(l){const r=l;return(t,x)=>(o(),i(m,{variant:"outlined",elevation:"0",class:"withbg"},{default:e(()=>[a(d,null,{default:e(()=>[c("div",C,[a(u,null,{default:e(()=>[p(_(r.title),1)]),_:1}),s(t.$slots,"action")])]),_:3}),a(f),a(V,null,{default:e(()=>[s(t.$slots,"default")]),_:3})]),_:3}))}});export{h as _};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<svg width="676" height="391" viewBox="0 0 676 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g opacity="0.09">
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 4.49127 197.53)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 342.315 387.578)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 28.0057 211.105)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 365.829 374.002)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 51.52 224.68)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 389.344 360.428)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 75.0345 238.255)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 412.858 346.852)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 98.5488 251.83)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 436.372 333.277)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 122.063 265.405)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 459.887 319.703)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 145.578 278.979)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 483.401 306.127)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 169.092 292.556)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 506.916 292.551)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 192.597 306.127)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 530.43 278.977)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 216.111 319.703)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 553.944 265.402)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 239.626 333.277)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 577.459 251.827)" stroke="black"/>
|
|
||||||
<path d="M263.231 346.905L601.064 151.871" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 600.973 238.252)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 286.654 360.428)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 624.487 224.677)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 310.169 374.002)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 648.002 211.102)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(0.866041 -0.499972 -0.866041 -0.499972 333.683 387.578)" stroke="black"/>
|
|
||||||
<line y1="-0.5" x2="390.089" y2="-0.5" transform="matrix(-0.866041 -0.499972 -0.866041 0.499972 671.516 197.527)" stroke="black"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<svg width="676" height="395" viewBox="0 0 676 395" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="26.998" height="26.8293" transform="matrix(0.866041 -0.499972 0.866041 0.499972 361.873 290.126)" fill="#E3F2FD"/>
|
|
||||||
<rect width="24.2748" height="24.1231" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364.249 291.115)" fill="#90CAF9"/>
|
|
||||||
<rect width="26.998" height="26.8293" transform="matrix(0.866041 -0.499972 0.866041 0.499972 291.67 86.4912)" fill="#E3F2FD"/>
|
|
||||||
<rect width="24.2748" height="24.1231" transform="matrix(0.866041 -0.499972 0.866041 0.499972 294.046 87.48)" fill="#90CAF9"/>
|
|
||||||
<g filter="url(#filter0_d)">
|
|
||||||
<path d="M370.694 211.828L365.394 208.768V215.835L365.404 215.829C365.459 216.281 365.785 216.724 366.383 217.069L417.03 246.308C418.347 247.068 420.481 247.068 421.798 246.308L468.671 219.248C469.374 218.842 469.702 218.301 469.654 217.77V210.861L464.282 213.962L418.024 187.257C416.708 186.497 414.573 186.497 413.257 187.257L370.694 211.828Z" fill="url(#paint0_linear)"/>
|
|
||||||
</g>
|
|
||||||
<rect width="59.6284" height="63.9858" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364 208.812)" fill="#90CAF9"/>
|
|
||||||
<rect width="59.6284" height="63.9858" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 364 208.812)" fill="url(#paint1_linear)"/>
|
|
||||||
<rect width="56.6816" height="60.8238" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 366.645 208.761)" fill="url(#paint2_linear)"/>
|
|
||||||
<path d="M421.238 206.161C421.238 206.434 421.62 206.655 422.092 206.655L432.159 206.656C435.164 206.656 437.6 208.063 437.601 209.798C437.602 211.533 435.166 212.939 432.162 212.938L422.09 212.937C421.62 212.937 421.24 213.157 421.24 213.428L421.241 215.814C421.241 216.087 421.624 216.308 422.096 216.308L432.689 216.309C438.917 216.31 443.967 213.395 443.965 209.799C443.964 206.202 438.914 203.286 432.684 203.286L422.086 203.284C421.617 203.284 421.236 203.504 421.237 203.775L421.238 206.161Z" fill="#1E88E5"/>
|
|
||||||
<path d="M413.422 213.43C413.422 213.157 413.039 212.936 412.567 212.936L402.896 212.935C399.891 212.935 397.455 211.528 397.454 209.793C397.453 208.059 399.889 206.652 402.894 206.653L412.57 206.654C413.039 206.654 413.419 206.435 413.419 206.164L413.418 203.777C413.418 203.504 413.035 203.283 412.563 203.283L402.366 203.282C396.138 203.281 391.089 206.197 391.09 209.793C391.091 213.389 396.141 216.305 402.371 216.306L412.573 216.307C413.042 216.307 413.423 216.088 413.423 215.817L413.422 213.43Z" fill="#1E88E5"/>
|
|
||||||
<path d="M407.999 198.145L411.211 201.235C411.266 201.288 411.332 201.336 411.405 201.379C411.813 201.614 412.461 201.669 412.979 201.49C413.59 201.278 413.787 200.821 413.421 200.469L410.209 197.379C409.843 197.027 409.051 196.913 408.441 197.124C407.831 197.335 407.633 197.793 407.999 198.145Z" fill="#1E88E5"/>
|
|
||||||
<path d="M416.235 200.853C416.235 201.058 416.38 201.244 416.613 201.379C416.846 201.513 417.168 201.597 417.524 201.597C418.236 201.596 418.813 201.263 418.813 200.852L418.812 197.021C418.811 196.61 418.234 196.277 417.522 196.277C416.811 196.278 416.234 196.611 416.234 197.022L416.235 200.853Z" fill="#1E88E5"/>
|
|
||||||
<path d="M421.627 200.47C421.317 200.769 421.412 201.143 421.82 201.379C421.893 201.421 421.977 201.459 422.069 201.491C422.68 201.703 423.472 201.588 423.838 201.236L427.047 198.147C427.413 197.794 427.215 197.337 426.605 197.126C425.994 196.915 425.203 197.029 424.836 197.381L421.627 200.47Z" fill="#1E88E5"/>
|
|
||||||
<path d="M427.056 221.447L423.844 218.357C423.478 218.005 422.686 217.891 422.076 218.102C421.466 218.314 421.268 218.771 421.634 219.123L424.846 222.213C424.901 222.266 424.967 222.314 425.04 222.357C425.448 222.592 426.097 222.647 426.614 222.468C427.225 222.257 427.423 221.799 427.056 221.447Z" fill="#1E88E5"/>
|
|
||||||
<path d="M418.82 218.739C418.82 218.328 418.243 217.995 417.531 217.995C416.819 217.995 416.242 218.329 416.242 218.74L416.243 222.57C416.244 222.776 416.388 222.962 416.621 223.096C416.854 223.231 417.177 223.314 417.533 223.314C418.245 223.314 418.822 222.981 418.821 222.57L418.82 218.739Z" fill="#1E88E5"/>
|
|
||||||
<path d="M413.428 219.122C413.794 218.77 413.596 218.312 412.986 218.101C412.375 217.89 411.584 218.004 411.217 218.356L408.008 221.445C407.698 221.744 407.793 222.118 408.201 222.354C408.274 222.396 408.358 222.434 408.45 222.466C409.061 222.678 409.853 222.563 410.219 222.211L413.428 219.122Z" fill="#1E88E5"/>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_d" x="301.394" y="186.687" width="232.264" height="208.191" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
|
||||||
<feOffset dy="84"/>
|
|
||||||
<feGaussianBlur stdDeviation="32"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0.129412 0 0 0 0 0.588235 0 0 0 0 0.952941 0 0 0 0.2 0"/>
|
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
|
||||||
</filter>
|
|
||||||
<linearGradient id="paint0_linear" x1="417.526" y1="205.789" x2="365.394" y2="216.782" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#2196F3"/>
|
|
||||||
<stop offset="1" stop-color="#B1DCFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear" x1="0.503035" y1="2.68177" x2="20.3032" y2="42.2842" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#FAFAFA" stop-opacity="0.74"/>
|
|
||||||
<stop offset="1" stop-color="#91CBFA"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear" x1="-18.5494" y1="-44.8799" x2="14.7845" y2="40.5766" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#FAFAFA" stop-opacity="0.74"/>
|
|
||||||
<stop offset="1" stop-color="#91CBFA"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,42 +0,0 @@
|
|||||||
<svg width="710" height="391" viewBox="0 0 710 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="26.9258" height="26.7576" transform="matrix(0.866041 -0.499972 0.866041 0.499972 161.088 154.333)" fill="#EDE7F6"/>
|
|
||||||
<rect width="24.9267" height="24.7709" transform="matrix(0.866041 -0.499972 0.866041 0.499972 162.809 155.327)" fill="#B39DDB"/>
|
|
||||||
<rect width="26.9258" height="26.7576" transform="matrix(0.866041 -0.499972 0.866041 0.499972 536.744 181.299)" fill="#EDE7F6"/>
|
|
||||||
<rect width="24.9267" height="24.7709" transform="matrix(0.866041 -0.499972 0.866041 0.499972 538.465 182.292)" fill="#B39DDB"/>
|
|
||||||
<g filter="url(#filter0_d)">
|
|
||||||
<path d="M67.7237 137.573V134.673H64.009V140.824L64.0177 140.829C64.0367 141.477 64.4743 142.121 65.3305 142.615L103.641 164.733C105.393 165.744 108.232 165.744 109.983 164.733L204.044 110.431C204.879 109.949 205.316 109.324 205.355 108.693L205.355 108.692V108.68C205.358 108.628 205.358 108.576 205.355 108.523L205.362 102.335L200.065 104.472L165.733 84.6523C163.982 83.6413 161.142 83.6413 159.391 84.6523L67.7237 137.573Z" fill="url(#paint0_linear)"/>
|
|
||||||
</g>
|
|
||||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
|
||||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="url(#paint1_linear)" fill-opacity="0.3"/>
|
|
||||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="64" y="78" width="141" height="81">
|
|
||||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0)">
|
|
||||||
</g>
|
|
||||||
<mask id="mask1" mask-type="alpha" maskUnits="userSpaceOnUse" x="64" y="78" width="141" height="81">
|
|
||||||
<rect width="115.933" height="51.5596" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 62.1588 134.683)" fill="#673AB7"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask1)">
|
|
||||||
<rect width="64.3732" height="64.3732" rx="5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 111.303 81.6006)" fill="#5E35B1"/>
|
|
||||||
<rect opacity="0.7" x="0.866041" width="63.3732" height="63.3732" rx="4.5" transform="matrix(0.866041 -0.499972 0.866041 0.499972 79.1848 87.8305)" stroke="#5E35B1"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_d" x="0.0090332" y="83.894" width="269.353" height="229.597" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
|
||||||
<feOffset dy="84"/>
|
|
||||||
<feGaussianBlur stdDeviation="32"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0.403922 0 0 0 0 0.227451 0 0 0 0 0.717647 0 0 0 0.2 0"/>
|
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
|
||||||
</filter>
|
|
||||||
<linearGradient id="paint0_linear" x1="200.346" y1="102.359" x2="71.0293" y2="158.071" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#A491C8"/>
|
|
||||||
<stop offset="1" stop-color="#D7C5F8"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear" x1="8.1531" y1="-0.145767" x2="57.1962" y2="72.3003" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="white"/>
|
|
||||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,27 +0,0 @@
|
|||||||
<svg width="676" height="391" viewBox="0 0 676 391" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M267.744 237.142L279.699 230.24L300.636 242.329L288.682 249.231L313.566 263.598L286.344 279.314L261.46 264.947L215.984 291.203L197.779 282.558L169.334 211.758L169.092 211.618L196.313 195.902L267.744 237.142ZM219.359 265.077L240.523 252.859L204.445 232.029L205.487 234.589L219.359 265.077Z" fill="#FFAB91"/>
|
|
||||||
<path d="M469.959 120.206L481.913 113.304L502.851 125.392L490.897 132.294L515.78 146.661L488.559 162.377L463.675 148.011L418.199 174.266L399.994 165.621L371.548 94.8211L371.307 94.6816L398.528 78.9654L469.959 120.206ZM421.574 148.141L442.737 135.922L406.66 115.093L407.701 117.653L421.574 148.141Z" fill="#FFAB91"/>
|
|
||||||
<path d="M204.523 235.027V232.237L219.401 265.014L240.555 252.926V255.018L218.936 267.339L204.523 235.027Z" fill="#D84315"/>
|
|
||||||
<path d="M406.738 118.09V115.301L421.616 148.078L442.77 135.99V138.082L421.151 150.402L406.738 118.09Z" fill="#D84315"/>
|
|
||||||
<rect width="109.114" height="136.405" transform="matrix(0.866025 -0.5 0.866025 0.5 220.507 181.925)" fill="url(#paint0_linear)"/>
|
|
||||||
<rect width="40.2357" height="70.0545" transform="matrix(0.866025 -0.5 0.866025 0.5 280.437 201.886)" fill="url(#paint1_linear)"/>
|
|
||||||
<rect x="25.1147" width="80.1144" height="107.405" transform="matrix(0.866025 -0.5 0.866025 0.5 223.872 194.482)" stroke="#1565C0" stroke-width="29"/>
|
|
||||||
<rect x="25.1147" width="80.1144" height="107.405" transform="matrix(0.866025 -0.5 0.866025 0.5 223.872 194.482)" stroke="url(#paint2_linear)" stroke-width="29"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M279.517 230.177L267.662 237.15L196.064 195.772L168.866 211.58L169.331 212.097L170.096 214.002L196.436 198.795L267.866 240.035L279.821 233.133L298.211 243.751L300.787 242.265L279.517 230.177ZM291.278 250.695L288.804 252.124L311.1 264.996L313.805 263.418L291.278 250.695Z" fill="#D84315"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M481.732 113.24L469.877 120.214L398.279 78.8359L371.081 94.6433L371.546 95.1603L372.311 97.0652L398.651 81.8581L470.081 123.099L482.036 116.196L500.426 126.814L503.002 125.328L481.732 113.24ZM493.493 133.759L491.019 135.187L513.315 148.06L516.02 146.482L493.493 133.759Z" fill="#D84315"/>
|
|
||||||
<path d="M288.674 252.229V249.207L291.929 251.067L288.674 252.229Z" fill="#D84315"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear" x1="77.7511" y1="139.902" x2="-10.8629" y2="8.75671" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#3076C8"/>
|
|
||||||
<stop offset="0.992076" stop-color="#91CBFA"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear" x1="25.8162" y1="51.0447" x2="68.7073" y2="-5.41524" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#2E75C7"/>
|
|
||||||
<stop offset="1" stop-color="#4283CC"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear" x1="-16.1224" y1="-47.972" x2="123.494" y2="290.853" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="white"/>
|
|
||||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB |
File diff suppressed because one or more lines are too long
720
addons/dashboard/dist/assets/index-dc96e1be.js
vendored
720
addons/dashboard/dist/assets/index-dc96e1be.js
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9
addons/dashboard/dist/assets/md5-45627dcb.js
vendored
9
addons/dashboard/dist/assets/md5-45627dcb.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M5.06129 13.2253L4.31871 15.9975L1.60458 16.0549C0.793457 14.5504 0.333374 12.8292 0.333374 11C0.333374 9.23119 0.763541 7.56319 1.52604 6.09448H1.52662L3.94296 6.53748L5.00146 8.93932C4.77992 9.58519 4.65917 10.2785 4.65917 11C4.65925 11.783 4.80108 12.5332 5.06129 13.2253Z" fill="#FBBB00"/>
|
|
||||||
<path d="M21.4804 9.00732C21.6029 9.65257 21.6668 10.3189 21.6668 11C21.6668 11.7637 21.5865 12.5086 21.4335 13.2271C20.9143 15.6722 19.5575 17.8073 17.678 19.3182L17.6774 19.3177L14.6339 19.1624L14.2031 16.4734C15.4503 15.742 16.425 14.5974 16.9384 13.2271H11.2346V9.00732H17.0216H21.4804Z" fill="#518EF8"/>
|
|
||||||
<path d="M17.6772 19.3176L17.6777 19.3182C15.8498 20.7875 13.5277 21.6666 11 21.6666C6.93783 21.6666 3.40612 19.3962 1.60449 16.0549L5.0612 13.2253C5.96199 15.6294 8.28112 17.3408 11 17.3408C12.1686 17.3408 13.2634 17.0249 14.2029 16.4734L17.6772 19.3176Z" fill="#28B446"/>
|
|
||||||
<path d="M17.8085 2.78892L14.353 5.61792C13.3807 5.01017 12.2313 4.65908 11 4.65908C8.21963 4.65908 5.85713 6.44896 5.00146 8.93925L1.52658 6.09442H1.526C3.30125 2.67171 6.8775 0.333252 11 0.333252C13.5881 0.333252 15.9612 1.25517 17.8085 2.78892Z" fill="#F14336"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,533 +0,0 @@
|
|||||||
from addons.dashboard.server import AstrBotDashBoard, DashBoardData
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import Union, Optional
|
|
||||||
import uuid
|
|
||||||
from util import general_utils as gu
|
|
||||||
from util.cmd_config import CmdConfig
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import asyncio
|
|
||||||
from util.plugin_dev.api.v1.config import update_config
|
|
||||||
from SparkleLogging.utils.core import LogManager
|
|
||||||
from logging import Logger
|
|
||||||
|
|
||||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DashBoardConfig():
|
|
||||||
config_type: str
|
|
||||||
name: Optional[str] = None
|
|
||||||
description: Optional[str] = None
|
|
||||||
path: Optional[str] = None # 仅 item 才需要
|
|
||||||
body: Optional[list['DashBoardConfig']] = None # 仅 group 才需要
|
|
||||||
value: Optional[Union[list, dict, str, int, bool]] = None # 仅 item 才需要
|
|
||||||
val_type: Optional[str] = None # 仅 item 才需要
|
|
||||||
|
|
||||||
|
|
||||||
class DashBoardHelper():
|
|
||||||
def __init__(self, global_object, config: dict):
|
|
||||||
self.loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(self.loop)
|
|
||||||
dashboard_data = global_object.dashboard_data
|
|
||||||
dashboard_data.configs = {
|
|
||||||
"data": []
|
|
||||||
}
|
|
||||||
self.parse_default_config(dashboard_data, config)
|
|
||||||
self.dashboard_data: DashBoardData = dashboard_data
|
|
||||||
self.dashboard = AstrBotDashBoard(global_object)
|
|
||||||
self.key_map = {} # key: uuid, value: config key name
|
|
||||||
self.cc = CmdConfig()
|
|
||||||
|
|
||||||
@self.dashboard.register("post_configs")
|
|
||||||
def on_post_configs(post_configs: dict):
|
|
||||||
try:
|
|
||||||
if 'base_config' in post_configs:
|
|
||||||
self.save_config(
|
|
||||||
post_configs['base_config'], namespace='') # 基础配置
|
|
||||||
self.save_config(
|
|
||||||
post_configs['config'], namespace=post_configs['namespace']) # 选定配置
|
|
||||||
self.parse_default_config(
|
|
||||||
self.dashboard_data, self.cc.get_all())
|
|
||||||
# 重启
|
|
||||||
threading.Thread(target=self.dashboard.shutdown_bot,
|
|
||||||
args=(2,), daemon=True).start()
|
|
||||||
except Exception as e:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# 将 config.yaml、 中的配置解析到 dashboard_data.configs 中
|
|
||||||
def parse_default_config(self, dashboard_data: DashBoardData, config: dict):
|
|
||||||
|
|
||||||
try:
|
|
||||||
qq_official_platform_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="QQ_OFFICIAL 平台配置",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启用 QQ_OFFICIAL 平台",
|
|
||||||
description="官方的接口,仅支持 QQ 频道。详见 q.qq.com",
|
|
||||||
value=config['qqbot']['enable'],
|
|
||||||
path="qqbot.enable",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="QQ机器人APPID",
|
|
||||||
description="详见 q.qq.com",
|
|
||||||
value=config['qqbot']['appid'],
|
|
||||||
path="qqbot.appid",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="QQ机器人令牌",
|
|
||||||
description="详见 q.qq.com",
|
|
||||||
value=config['qqbot']['token'],
|
|
||||||
path="qqbot.token",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="QQ机器人 Secret",
|
|
||||||
description="详见 q.qq.com",
|
|
||||||
value=config['qqbot_secret'],
|
|
||||||
path="qqbot_secret",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否允许 QQ 频道私聊",
|
|
||||||
description="如果启用,机器人会响应私聊消息。",
|
|
||||||
value=config['direct_message_mode'],
|
|
||||||
path="direct_message_mode",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否接收QQ群消息",
|
|
||||||
description="需要机器人有相应的群消息接收权限。在 q.qq.com 上查看。",
|
|
||||||
value=config['qqofficial_enable_group_message'],
|
|
||||||
path="qqofficial_enable_group_message",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
qq_gocq_platform_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="OneBot协议平台配置",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启用",
|
|
||||||
description="支持cq-http、shamrock等(目前仅支持QQ平台)",
|
|
||||||
value=config['gocqbot']['enable'],
|
|
||||||
path="gocqbot.enable",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="HTTP 服务器地址",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_host'],
|
|
||||||
path="gocq_host",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="HTTP 服务器端口",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_http_port'],
|
|
||||||
path="gocq_http_port",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="WebSocket 服务器端口",
|
|
||||||
description="目前仅支持正向 WebSocket",
|
|
||||||
value=config['gocq_websocket_port'],
|
|
||||||
path="gocq_websocket_port",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否响应群消息",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_react_group'],
|
|
||||||
path="gocq_react_group",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否响应私聊消息",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_react_friend'],
|
|
||||||
path="gocq_react_friend",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否响应群成员增加消息",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_react_group_increase'],
|
|
||||||
path="gocq_react_group_increase",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="是否响应频道消息",
|
|
||||||
description="",
|
|
||||||
value=config['gocq_react_guild'],
|
|
||||||
path="gocq_react_guild",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="转发阈值(字符数)",
|
|
||||||
description="机器人回复的消息长度超出这个值后,会被折叠成转发卡片发出以减少刷屏。",
|
|
||||||
value=config['qq_forward_threshold'],
|
|
||||||
path="qq_forward_threshold",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
general_platform_detail_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="通用平台配置",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启动消息文字转图片",
|
|
||||||
description="启动后,机器人会将消息转换为图片发送,以降低风控风险。",
|
|
||||||
value=config['qq_pic_mode'],
|
|
||||||
path="qq_pic_mode",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="消息限制时间",
|
|
||||||
description="在此时间内,机器人不会回复同一个用户的消息。单位:秒",
|
|
||||||
value=config['limit']['time'],
|
|
||||||
path="limit.time",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="消息限制次数",
|
|
||||||
description="在上面的时间内,如果用户发送消息超过此次数,则机器人不会回复。单位:次",
|
|
||||||
value=config['limit']['count'],
|
|
||||||
path="limit.count",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="回复前缀",
|
|
||||||
description="[xxxx] 你好! 其中xxxx是你可以填写的前缀。如果为空则不显示。",
|
|
||||||
value=config['reply_prefix'],
|
|
||||||
path="reply_prefix",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="list",
|
|
||||||
name="通用管理员用户 ID(支持多个管理员)。通过 !myid 指令获取。",
|
|
||||||
description="",
|
|
||||||
value=config['other_admins'],
|
|
||||||
path="other_admins",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="独立会话",
|
|
||||||
description="是否启用独立会话模式,即 1 个用户自然账号 1 个会话。",
|
|
||||||
value=config['uniqueSessionMode'],
|
|
||||||
path="uniqueSessionMode",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="LLM 唤醒词",
|
|
||||||
description="如果不为空, 那么只有当消息以此词开头时,才会调用大语言模型进行回复。如设置为 /chat,那么只有当消息以 /chat 开头时,才会调用大语言模型进行回复。",
|
|
||||||
value=config['llm_wake_prefix'],
|
|
||||||
path="llm_wake_prefix",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
openai_official_llm_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="OpenAI 官方接口类设置",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="list",
|
|
||||||
name="OpenAI API Key",
|
|
||||||
description="OpenAI API 的 Key。支持使用非官方但兼容的 API(第三方中转key)。",
|
|
||||||
value=config['openai']['key'],
|
|
||||||
path="openai.key",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI API 节点地址(api base)",
|
|
||||||
description="OpenAI API 的节点地址,配合非官方 API 使用。如果不想填写,那么请填写 none",
|
|
||||||
value=config['openai']['api_base'],
|
|
||||||
path="openai.api_base",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI model",
|
|
||||||
description="OpenAI LLM 模型。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['model'],
|
|
||||||
path="openai.chatGPTConfigs.model",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="OpenAI max_tokens",
|
|
||||||
description="OpenAI 最大生成长度。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['max_tokens'],
|
|
||||||
path="openai.chatGPTConfigs.max_tokens",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="float",
|
|
||||||
name="OpenAI temperature",
|
|
||||||
description="OpenAI 温度。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['temperature'],
|
|
||||||
path="openai.chatGPTConfigs.temperature",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="float",
|
|
||||||
name="OpenAI top_p",
|
|
||||||
description="OpenAI top_p。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['top_p'],
|
|
||||||
path="openai.chatGPTConfigs.top_p",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="float",
|
|
||||||
name="OpenAI frequency_penalty",
|
|
||||||
description="OpenAI frequency_penalty。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['frequency_penalty'],
|
|
||||||
path="openai.chatGPTConfigs.frequency_penalty",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="float",
|
|
||||||
name="OpenAI presence_penalty",
|
|
||||||
description="OpenAI presence_penalty。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['chatGPTConfigs']['presence_penalty'],
|
|
||||||
path="openai.chatGPTConfigs.presence_penalty",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="int",
|
|
||||||
name="OpenAI 总生成长度限制",
|
|
||||||
description="OpenAI 总生成长度限制。详见 https://platform.openai.com/docs/api-reference/chat",
|
|
||||||
value=config['openai']['total_tokens_limit'],
|
|
||||||
path="openai.total_tokens_limit",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI 图像生成模型",
|
|
||||||
description="OpenAI 图像生成模型。",
|
|
||||||
value=config['openai_image_generate']['model'],
|
|
||||||
path="openai_image_generate.model",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI 图像生成大小",
|
|
||||||
description="OpenAI 图像生成大小。",
|
|
||||||
value=config['openai_image_generate']['size'],
|
|
||||||
path="openai_image_generate.size",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI 图像生成风格",
|
|
||||||
description="OpenAI 图像生成风格。修改前请参考 OpenAI 官方文档",
|
|
||||||
value=config['openai_image_generate']['style'],
|
|
||||||
path="openai_image_generate.style",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="OpenAI 图像生成质量",
|
|
||||||
description="OpenAI 图像生成质量。修改前请参考 OpenAI 官方文档",
|
|
||||||
value=config['openai_image_generate']['quality'],
|
|
||||||
path="openai_image_generate.quality",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="问题题首提示词",
|
|
||||||
description="如果填写了此项,在每个对大语言模型的请求中,都会在问题前加上此提示词。",
|
|
||||||
value=config['llm_env_prompt'],
|
|
||||||
path="llm_env_prompt",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="默认人格文本",
|
|
||||||
description="默认人格文本",
|
|
||||||
value=config['default_personality_str'],
|
|
||||||
path="default_personality_str",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
baidu_aip_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="百度内容审核",
|
|
||||||
description="需要去申请",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启动百度内容审核服务",
|
|
||||||
description="",
|
|
||||||
value=config['baidu_aip']['enable'],
|
|
||||||
path="baidu_aip.enable"
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="APP ID",
|
|
||||||
description="",
|
|
||||||
value=config['baidu_aip']['app_id'],
|
|
||||||
path="baidu_aip.app_id"
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="API KEY",
|
|
||||||
description="",
|
|
||||||
value=config['baidu_aip']['api_key'],
|
|
||||||
path="baidu_aip.api_key"
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="SECRET KEY",
|
|
||||||
description="",
|
|
||||||
value=config['baidu_aip']['secret_key'],
|
|
||||||
path="baidu_aip.secret_key"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
other_group = DashBoardConfig(
|
|
||||||
config_type="group",
|
|
||||||
name="其他配置",
|
|
||||||
description="其他配置描述",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="HTTP 代理地址",
|
|
||||||
description="建议上下一致",
|
|
||||||
value=config['http_proxy'],
|
|
||||||
path="http_proxy",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="HTTPS 代理地址",
|
|
||||||
description="建议上下一致",
|
|
||||||
value=config['https_proxy'],
|
|
||||||
path="https_proxy",
|
|
||||||
),
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="str",
|
|
||||||
name="面板用户名",
|
|
||||||
description="是的,就是你理解的这个面板的用户名",
|
|
||||||
value=config['dashboard_username'],
|
|
||||||
path="dashboard_username",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
dashboard_data.configs['data'] = [
|
|
||||||
qq_official_platform_group,
|
|
||||||
qq_gocq_platform_group,
|
|
||||||
general_platform_detail_group,
|
|
||||||
openai_official_llm_group,
|
|
||||||
other_group,
|
|
||||||
baidu_aip_group
|
|
||||||
]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"配置文件解析错误:{e}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def save_config(self, post_config: list, namespace: str):
|
|
||||||
'''
|
|
||||||
根据 path 解析并保存配置
|
|
||||||
'''
|
|
||||||
|
|
||||||
queue = post_config
|
|
||||||
while len(queue) > 0:
|
|
||||||
config = queue.pop(0)
|
|
||||||
if config['config_type'] == "group":
|
|
||||||
for item in config['body']:
|
|
||||||
queue.append(item)
|
|
||||||
elif config['config_type'] == "item":
|
|
||||||
if config['path'] is None or config['path'] == "":
|
|
||||||
continue
|
|
||||||
|
|
||||||
path = config['path'].split('.')
|
|
||||||
if len(path) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if config['val_type'] == "bool":
|
|
||||||
self._write_config(
|
|
||||||
namespace, config['path'], config['value'])
|
|
||||||
elif config['val_type'] == "str":
|
|
||||||
self._write_config(
|
|
||||||
namespace, config['path'], config['value'])
|
|
||||||
elif config['val_type'] == "int":
|
|
||||||
try:
|
|
||||||
self._write_config(
|
|
||||||
namespace, config['path'], int(config['value']))
|
|
||||||
except:
|
|
||||||
raise ValueError(f"配置项 {config['name']} 的值必须是整数")
|
|
||||||
elif config['val_type'] == "float":
|
|
||||||
try:
|
|
||||||
self._write_config(
|
|
||||||
namespace, config['path'], float(config['value']))
|
|
||||||
except:
|
|
||||||
raise ValueError(f"配置项 {config['name']} 的值必须是浮点数")
|
|
||||||
elif config['val_type'] == "list":
|
|
||||||
if config['value'] is None:
|
|
||||||
self._write_config(namespace, config['path'], [])
|
|
||||||
elif not isinstance(config['value'], list):
|
|
||||||
raise ValueError(f"配置项 {config['name']} 的值必须是列表")
|
|
||||||
self._write_config(
|
|
||||||
namespace, config['path'], config['value'])
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
f"未知或者未实现的配置项类型:{config['val_type']}")
|
|
||||||
|
|
||||||
def _write_config(self, namespace: str, key: str, value):
|
|
||||||
if namespace == "" or namespace.startswith("internal_"):
|
|
||||||
# 机器人自带配置,存到 config.yaml
|
|
||||||
self.cc.put_by_dot_str(key, value)
|
|
||||||
else:
|
|
||||||
update_config(namespace, key, value)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.dashboard.run()
|
|
||||||
@@ -1,476 +0,0 @@
|
|||||||
import util.plugin_util as putil
|
|
||||||
import websockets
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from flask import Flask, request
|
|
||||||
from flask.logging import default_handler
|
|
||||||
from werkzeug.serving import make_server
|
|
||||||
from util import general_utils as gu
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from persist.session import dbConn
|
|
||||||
from type.register import RegisteredPlugin
|
|
||||||
from typing import List
|
|
||||||
from util.cmd_config import CmdConfig
|
|
||||||
from util.updator import check_update, update_project, request_release_info
|
|
||||||
from SparkleLogging.utils.core import LogManager
|
|
||||||
from logging import Logger
|
|
||||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DashBoardData():
|
|
||||||
stats: dict
|
|
||||||
configs: dict
|
|
||||||
logs: dict
|
|
||||||
plugins: List[RegisteredPlugin]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Response():
|
|
||||||
status: str
|
|
||||||
message: str
|
|
||||||
data: dict
|
|
||||||
|
|
||||||
|
|
||||||
class AstrBotDashBoard():
|
|
||||||
def __init__(self, global_object: 'gu.GlobalObject'):
|
|
||||||
self.global_object = global_object
|
|
||||||
self.loop = asyncio.get_event_loop()
|
|
||||||
asyncio.set_event_loop(self.loop)
|
|
||||||
self.dashboard_data: DashBoardData = global_object.dashboard_data
|
|
||||||
self.dashboard_be = Flask(
|
|
||||||
__name__, static_folder="dist", static_url_path="/")
|
|
||||||
self.funcs = {}
|
|
||||||
self.cc = CmdConfig()
|
|
||||||
self.ws_clients = {} # remote_ip: ws
|
|
||||||
# 启动 websocket 服务器
|
|
||||||
self.ws_server = websockets.serve(self.__handle_msg, "0.0.0.0", 6186)
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/")
|
|
||||||
def index():
|
|
||||||
# 返回页面
|
|
||||||
return self.dashboard_be.send_static_file("index.html")
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/config")
|
|
||||||
def rt_config():
|
|
||||||
return self.dashboard_be.send_static_file("index.html")
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/logs")
|
|
||||||
def rt_logs():
|
|
||||||
return self.dashboard_be.send_static_file("index.html")
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/extension")
|
|
||||||
def rt_extension():
|
|
||||||
return self.dashboard_be.send_static_file("index.html")
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/dashboard/default")
|
|
||||||
def rt_dashboard():
|
|
||||||
return self.dashboard_be.send_static_file("index.html")
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/authenticate")
|
|
||||||
def authenticate():
|
|
||||||
username = self.cc.get("dashboard_username", "")
|
|
||||||
password = self.cc.get("dashboard_password", "")
|
|
||||||
# 获得请求体
|
|
||||||
post_data = request.json
|
|
||||||
if post_data["username"] == username and post_data["password"] == password:
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="登录成功。",
|
|
||||||
data={
|
|
||||||
"token": "astrbot-test-token",
|
|
||||||
"username": username
|
|
||||||
}
|
|
||||||
).__dict__
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message="用户名或密码错误。",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/change_password")
|
|
||||||
def change_password():
|
|
||||||
password = self.cc.get("dashboard_password", "")
|
|
||||||
# 获得请求体
|
|
||||||
post_data = request.json
|
|
||||||
if post_data["password"] == password:
|
|
||||||
self.cc.put("dashboard_password", post_data["new_password"])
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="修改成功。",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message="原密码错误。",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/stats")
|
|
||||||
def get_stats():
|
|
||||||
db_inst = dbConn()
|
|
||||||
all_session = db_inst.get_all_stat_session()
|
|
||||||
last_24_message = db_inst.get_last_24h_stat_message()
|
|
||||||
# last_24_platform = db_inst.get_last_24h_stat_platform()
|
|
||||||
platforms = db_inst.get_platform_cnt_total()
|
|
||||||
self.dashboard_data.stats["session"] = []
|
|
||||||
self.dashboard_data.stats["session_total"] = db_inst.get_session_cnt_total(
|
|
||||||
)
|
|
||||||
self.dashboard_data.stats["message"] = last_24_message
|
|
||||||
self.dashboard_data.stats["message_total"] = db_inst.get_message_cnt_total(
|
|
||||||
)
|
|
||||||
self.dashboard_data.stats["platform"] = platforms
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=self.dashboard_data.stats
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/configs")
|
|
||||||
def get_configs():
|
|
||||||
# 如果params中有namespace,则返回该namespace下的配置
|
|
||||||
# 否则返回所有配置
|
|
||||||
namespace = "" if "namespace" not in request.args else request.args["namespace"]
|
|
||||||
conf = self._get_configs(namespace)
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=conf
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/config_outline")
|
|
||||||
def get_config_outline():
|
|
||||||
outline = self._generate_outline()
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=outline
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/configs")
|
|
||||||
def post_configs():
|
|
||||||
post_configs = request.json
|
|
||||||
try:
|
|
||||||
self.funcs["post_configs"](post_configs)
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="保存成功~ 机器人将在 2 秒内重启以应用新的配置。",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=self.dashboard_data.configs
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/extensions")
|
|
||||||
def get_plugins():
|
|
||||||
_plugin_resp = []
|
|
||||||
for plugin in self.dashboard_data.plugins:
|
|
||||||
_p = plugin.metadata
|
|
||||||
_t = {
|
|
||||||
"name": _p.plugin_name,
|
|
||||||
"repo": '' if _p.repo is None else _p.repo,
|
|
||||||
"author": _p.author,
|
|
||||||
"desc": _p.desc,
|
|
||||||
"version": _p.version
|
|
||||||
}
|
|
||||||
_plugin_resp.append(_t)
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=_plugin_resp
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/extensions/install")
|
|
||||||
def install_plugin():
|
|
||||||
post_data = request.json
|
|
||||||
repo_url = post_data["url"]
|
|
||||||
try:
|
|
||||||
logger.info(f"正在安装插件 {repo_url}")
|
|
||||||
putil.install_plugin(repo_url, self.dashboard_data.plugins)
|
|
||||||
logger.info(f"安装插件 {repo_url} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="安装成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/extensions/uninstall")
|
|
||||||
def uninstall_plugin():
|
|
||||||
post_data = request.json
|
|
||||||
plugin_name = post_data["name"]
|
|
||||||
try:
|
|
||||||
logger.info(f"正在卸载插件 {plugin_name}")
|
|
||||||
putil.uninstall_plugin(
|
|
||||||
plugin_name, self.dashboard_data.plugins)
|
|
||||||
logger.info(f"卸载插件 {plugin_name} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="卸载成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/extensions/update")
|
|
||||||
def update_plugin():
|
|
||||||
post_data = request.json
|
|
||||||
plugin_name = post_data["name"]
|
|
||||||
try:
|
|
||||||
logger.info(f"正在更新插件 {plugin_name}")
|
|
||||||
putil.update_plugin(plugin_name, self.dashboard_data.plugins)
|
|
||||||
logger.info(f"更新插件 {plugin_name} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="更新成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/log")
|
|
||||||
def log():
|
|
||||||
for item in self.ws_clients:
|
|
||||||
try:
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
self.ws_clients[item].send(request.data.decode()), self.loop)
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
return 'ok'
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/check_update")
|
|
||||||
def get_update_info():
|
|
||||||
try:
|
|
||||||
ret = check_update()
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message=ret,
|
|
||||||
data={
|
|
||||||
"has_new_version": ret != "当前已经是最新版本。" # 先这样吧,累了=.=
|
|
||||||
}
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/update_project")
|
|
||||||
def update_project_api():
|
|
||||||
version = request.json['version']
|
|
||||||
if version == "" or version == "latest":
|
|
||||||
latest = True
|
|
||||||
version = ''
|
|
||||||
else:
|
|
||||||
latest = False
|
|
||||||
try:
|
|
||||||
update_project(request_release_info(latest),
|
|
||||||
latest=latest, version=version)
|
|
||||||
threading.Thread(target=self.shutdown_bot, args=(3,)).start()
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="更新成功,机器人将在 3 秒内重启。",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/llm/list")
|
|
||||||
def llm_list():
|
|
||||||
ret = []
|
|
||||||
for llm in self.global_object.llms:
|
|
||||||
ret.append(llm.llm_name)
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=ret
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.get("/api/llm")
|
|
||||||
def llm():
|
|
||||||
text = request.args["text"]
|
|
||||||
llm = request.args["llm"]
|
|
||||||
for llm_ in self.global_object.llms:
|
|
||||||
if llm_.llm_name == llm:
|
|
||||||
try:
|
|
||||||
# ret = await llm_.llm_instance.text_chat(text)
|
|
||||||
ret = asyncio.run_coroutine_threadsafe(
|
|
||||||
llm_.llm_instance.text_chat(text), self.loop).result()
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="",
|
|
||||||
data=ret
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message="LLM not found.",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
def shutdown_bot(self, delay_s: int):
|
|
||||||
time.sleep(delay_s)
|
|
||||||
py = sys.executable
|
|
||||||
os.execl(py, py, *sys.argv)
|
|
||||||
|
|
||||||
def _get_configs(self, namespace: str):
|
|
||||||
if namespace == "":
|
|
||||||
ret = [self.dashboard_data.configs['data'][4],
|
|
||||||
self.dashboard_data.configs['data'][5],]
|
|
||||||
elif namespace == "internal_platform_qq_official":
|
|
||||||
ret = [self.dashboard_data.configs['data'][0],]
|
|
||||||
elif namespace == "internal_platform_qq_gocq":
|
|
||||||
ret = [self.dashboard_data.configs['data'][1],]
|
|
||||||
elif namespace == "internal_platform_general": # 全局平台配置
|
|
||||||
ret = [self.dashboard_data.configs['data'][2],]
|
|
||||||
elif namespace == "internal_llm_openai_official":
|
|
||||||
ret = [self.dashboard_data.configs['data'][3],]
|
|
||||||
else:
|
|
||||||
path = f"data/config/{namespace}.json"
|
|
||||||
if not os.path.exists(path):
|
|
||||||
return []
|
|
||||||
with open(path, "r", encoding="utf-8-sig") as f:
|
|
||||||
ret = [{
|
|
||||||
"config_type": "group",
|
|
||||||
"name": namespace + " 插件配置",
|
|
||||||
"description": "",
|
|
||||||
"body": list(json.load(f).values())
|
|
||||||
},]
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _generate_outline(self):
|
|
||||||
'''
|
|
||||||
生成配置大纲。目前分为 platform(消息平台配置) 和 llm(语言模型配置) 两大类。
|
|
||||||
插件的info函数中如果带了plugin_type字段,则会被归类到对应的大纲中。目前仅支持 platform 和 llm 两种类型。
|
|
||||||
'''
|
|
||||||
outline = [
|
|
||||||
{
|
|
||||||
"type": "platform",
|
|
||||||
"name": "配置通用消息平台",
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"title": "通用",
|
|
||||||
"desc": "通用平台配置",
|
|
||||||
"namespace": "internal_platform_general",
|
|
||||||
"tag": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "QQ_OFFICIAL",
|
|
||||||
"desc": "QQ官方API,仅支持频道",
|
|
||||||
"namespace": "internal_platform_qq_official",
|
|
||||||
"tag": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "OneBot协议",
|
|
||||||
"desc": "支持cq-http、shamrock等(目前仅支持QQ平台)",
|
|
||||||
"namespace": "internal_platform_qq_gocq",
|
|
||||||
"tag": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "llm",
|
|
||||||
"name": "配置 LLM",
|
|
||||||
"body": [
|
|
||||||
{
|
|
||||||
"title": "OpenAI Official",
|
|
||||||
"desc": "也支持使用官方接口的中转服务",
|
|
||||||
"namespace": "internal_llm_openai_official",
|
|
||||||
"tag": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
for plugin in self.global_object.cached_plugins:
|
|
||||||
for item in outline:
|
|
||||||
if item['type'] == plugin.metadata.plugin_type:
|
|
||||||
item['body'].append({
|
|
||||||
"title": plugin.metadata.plugin_name,
|
|
||||||
"desc": plugin.metadata.desc,
|
|
||||||
"namespace": plugin.metadata.plugin_name,
|
|
||||||
"tag": plugin.metadata.plugin_name
|
|
||||||
})
|
|
||||||
return outline
|
|
||||||
|
|
||||||
def register(self, name: str):
|
|
||||||
def decorator(func):
|
|
||||||
self.funcs[name] = func
|
|
||||||
return func
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
async def get_log_history(self):
|
|
||||||
try:
|
|
||||||
with open("logs/astrbot-core/astrbot-core.log", "r", encoding="utf-8") as f:
|
|
||||||
return f.readlines()[-100:]
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"读取日志历史失败: {e.__str__()}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def __handle_msg(self, websocket, path):
|
|
||||||
address = websocket.remote_address
|
|
||||||
self.ws_clients[address] = websocket
|
|
||||||
data = await self.get_log_history()
|
|
||||||
data = ''.join(data).replace('\n', '\r\n')
|
|
||||||
await websocket.send(data)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
msg = await websocket.recv()
|
|
||||||
except websockets.exceptions.ConnectionClosedError:
|
|
||||||
# logger.info(f"和 {address} 的 websocket 连接已断开")
|
|
||||||
del self.ws_clients[address]
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
# logger.info(f"和 {path} 的 websocket 连接发生了错误: {e.__str__()}")
|
|
||||||
del self.ws_clients[address]
|
|
||||||
break
|
|
||||||
|
|
||||||
def run_ws_server(self, loop):
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_until_complete(self.ws_server)
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
threading.Thread(target=self.run_ws_server, args=(self.loop,)).start()
|
|
||||||
logger.info("已启动 websocket 服务器")
|
|
||||||
ip_address = gu.get_local_ip_addresses()
|
|
||||||
ip_str = f"http://{ip_address}:6185\n\thttp://localhost:6185"
|
|
||||||
logger.info(
|
|
||||||
f"\n==================\n您可访问:\n\n\t{ip_str}\n\n来登录可视化面板,默认账号密码为空。\n注意: 所有配置项现已全量迁移至 cmd_config.json 文件下,可登录可视化面板在线修改配置。\n==================\n")
|
|
||||||
|
|
||||||
http_server = make_server(
|
|
||||||
'0.0.0.0', 6185, self.dashboard_be, threaded=True)
|
|
||||||
http_server.serve_forever()
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
from nakuru.entities.components import *
|
|
||||||
flag_not_support = False
|
|
||||||
try:
|
|
||||||
from util.plugin_dev.api.v1.config import *
|
|
||||||
from util.plugin_dev.api.v1.bot import (
|
|
||||||
AstrMessageEvent,
|
|
||||||
CommandResult,
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
flag_not_support = True
|
|
||||||
print("导入接口失败。请升级到 AstrBot 最新版本。")
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
注意改插件名噢!格式:XXXPlugin 或 Main
|
|
||||||
小提示:把此模板仓库 fork 之后 clone 到机器人文件夹下的 addons/plugins/ 目录下,然后用 Pycharm/VSC 等工具打开可获更棒的编程体验(自动补全等)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
class HelloWorldPlugin:
|
|
||||||
"""
|
|
||||||
初始化函数, 可以选择直接pass
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
# 复制旧配置文件到 data 目录下。
|
|
||||||
if os.path.exists("keyword.json"):
|
|
||||||
shutil.move("keyword.json", "data/keyword.json")
|
|
||||||
self.keywords = {}
|
|
||||||
if os.path.exists("data/keyword.json"):
|
|
||||||
self.keywords = json.load(open("data/keyword.json", "r"))
|
|
||||||
else:
|
|
||||||
self.save_keyword()
|
|
||||||
|
|
||||||
"""
|
|
||||||
机器人程序会调用此函数。
|
|
||||||
返回规范: bool: 插件是否响应该消息 (所有的消息均会调用每一个载入的插件, 如果不响应, 则应返回 False)
|
|
||||||
Tuple: Non e或者长度为 3 的元组。如果不响应, 返回 None; 如果响应, 第 1 个参数为指令是否调用成功, 第 2 个参数为返回的消息链列表, 第 3 个参数为指令名称
|
|
||||||
例子:一个名为"yuanshen"的插件;当接收到消息为“原神 可莉”, 如果不想要处理此消息,则返回False, None;如果想要处理,但是执行失败了,返回True, tuple([False, "请求失败。", "yuanshen"]) ;执行成功了,返回True, tuple([True, "结果文本", "yuanshen"])
|
|
||||||
"""
|
|
||||||
|
|
||||||
def run(self, ame: AstrMessageEvent):
|
|
||||||
if ame.message_str == "helloworld":
|
|
||||||
return CommandResult(
|
|
||||||
hit=True,
|
|
||||||
success=True,
|
|
||||||
message_chain=[Plain("Hello World!!")],
|
|
||||||
command_name="helloworld"
|
|
||||||
)
|
|
||||||
if ame.message_str.startswith("/keyword") or ame.message_str.startswith("keyword"):
|
|
||||||
return self.handle_keyword_command(ame)
|
|
||||||
|
|
||||||
ret = self.check_keyword(ame.message_str)
|
|
||||||
if ret:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
return CommandResult(
|
|
||||||
hit=False,
|
|
||||||
success=False,
|
|
||||||
message_chain=None,
|
|
||||||
command_name=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_keyword_command(self, ame: AstrMessageEvent):
|
|
||||||
l = ame.message_str.split(" ")
|
|
||||||
|
|
||||||
# 获取图片
|
|
||||||
image_url = ""
|
|
||||||
for comp in ame.message_obj.message:
|
|
||||||
if isinstance(comp, Image) and image_url == "":
|
|
||||||
if comp.url is None:
|
|
||||||
image_url = comp.file
|
|
||||||
else:
|
|
||||||
image_url = comp.url
|
|
||||||
|
|
||||||
command_result = CommandResult(
|
|
||||||
hit=True,
|
|
||||||
success=False,
|
|
||||||
message_chain=None,
|
|
||||||
command_name="keyword"
|
|
||||||
)
|
|
||||||
if len(l) == 1 or (len(l) == 2 and image_url == ""):
|
|
||||||
ret = """【设置关键词回复】
|
|
||||||
示例:
|
|
||||||
1. keyword <触发词> <回复词>
|
|
||||||
keyword hi 你好
|
|
||||||
发送 hi 回复你好
|
|
||||||
* 回复词支持图片
|
|
||||||
|
|
||||||
2. keyword d <触发词>
|
|
||||||
keyword d hi
|
|
||||||
删除 hi 触发词产生的回复"""
|
|
||||||
command_result.success = True
|
|
||||||
command_result.message_chain = [Plain(ret)]
|
|
||||||
return command_result
|
|
||||||
elif len(l) == 3 and l[1] == "d":
|
|
||||||
if l[2] not in self.keywords:
|
|
||||||
command_result.message_chain = [Plain(f"关键词 {l[2]} 不存在")]
|
|
||||||
return command_result
|
|
||||||
self.keywords.pop(l[2])
|
|
||||||
self.save_keyword()
|
|
||||||
command_result.success = True
|
|
||||||
command_result.message_chain = [Plain("删除成功")]
|
|
||||||
return command_result
|
|
||||||
else:
|
|
||||||
self.keywords[l[1]] = {
|
|
||||||
"plain_text": " ".join(l[2:]),
|
|
||||||
"image_url": image_url
|
|
||||||
}
|
|
||||||
self.save_keyword()
|
|
||||||
command_result.success = True
|
|
||||||
command_result.message_chain = [Plain("设置成功")]
|
|
||||||
return command_result
|
|
||||||
|
|
||||||
def save_keyword(self):
|
|
||||||
json.dump(self.keywords, open(
|
|
||||||
"data/keyword.json", "w"), ensure_ascii=False)
|
|
||||||
|
|
||||||
def check_keyword(self, message_str: str):
|
|
||||||
for k in self.keywords:
|
|
||||||
if message_str == k:
|
|
||||||
plain_text = ""
|
|
||||||
if 'plain_text' in self.keywords[k]:
|
|
||||||
plain_text = self.keywords[k]['plain_text']
|
|
||||||
else:
|
|
||||||
plain_text = self.keywords[k]
|
|
||||||
image_url = ""
|
|
||||||
if 'image_url' in self.keywords[k]:
|
|
||||||
image_url = self.keywords[k]['image_url']
|
|
||||||
if image_url != "":
|
|
||||||
res = [Plain(plain_text), Image.fromURL(image_url)]
|
|
||||||
return CommandResult(
|
|
||||||
hit=True,
|
|
||||||
success=True,
|
|
||||||
message_chain=res,
|
|
||||||
command_name="keyword"
|
|
||||||
)
|
|
||||||
return CommandResult(
|
|
||||||
hit=True,
|
|
||||||
success=True,
|
|
||||||
message_chain=[Plain(plain_text)],
|
|
||||||
command_name="keyword"
|
|
||||||
)
|
|
||||||
|
|
||||||
"""
|
|
||||||
插件元信息。
|
|
||||||
当用户输入 plugin v 插件名称 时,会调用此函数,返回帮助信息。
|
|
||||||
返回参数要求(必填):dict{
|
|
||||||
"name": str, # 插件名称
|
|
||||||
"desc": str, # 插件简短描述
|
|
||||||
"help": str, # 插件帮助信息
|
|
||||||
"version": str, # 插件版本
|
|
||||||
"author": str, # 插件作者
|
|
||||||
"repo": str, # 插件仓库地址 [ 可选 ]
|
|
||||||
"homepage": str, # 插件主页 [ 可选 ]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def info(self):
|
|
||||||
return {
|
|
||||||
"name": "helloworld",
|
|
||||||
"desc": "这是 AstrBot 的默认插件,支持关键词回复。",
|
|
||||||
"help": "输入 /keyword 查看关键词回复帮助。",
|
|
||||||
"version": "v1.3",
|
|
||||||
"author": "Soulter"
|
|
||||||
}
|
|
||||||
3
astrbot/__init__.py
Normal file
3
astrbot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .core.log import LogManager
|
||||||
|
|
||||||
|
logger = LogManager.GetLogger(log_name="astrbot")
|
||||||
20
astrbot/api/__init__.py
Normal file
20
astrbot/api/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core import html_renderer
|
||||||
|
from astrbot.core import sp
|
||||||
|
from astrbot.core.star.register import register_llm_tool as llm_tool
|
||||||
|
from astrbot.core.star.register import register_agent as agent
|
||||||
|
from astrbot.core.agent.tool import ToolSet, FunctionTool
|
||||||
|
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AstrBotConfig",
|
||||||
|
"logger",
|
||||||
|
"html_renderer",
|
||||||
|
"llm_tool",
|
||||||
|
"agent",
|
||||||
|
"sp",
|
||||||
|
"ToolSet",
|
||||||
|
"FunctionTool",
|
||||||
|
"BaseFunctionToolExecutor",
|
||||||
|
]
|
||||||
53
astrbot/api/all.py
Normal file
53
astrbot/api/all.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core import html_renderer
|
||||||
|
from astrbot.core.star.register import register_llm_tool as llm_tool
|
||||||
|
|
||||||
|
# event
|
||||||
|
from astrbot.core.message.message_event_result import (
|
||||||
|
MessageEventResult,
|
||||||
|
MessageChain,
|
||||||
|
CommandResult,
|
||||||
|
EventResultType,
|
||||||
|
)
|
||||||
|
from astrbot.core.platform import AstrMessageEvent
|
||||||
|
|
||||||
|
# star register
|
||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_command as command,
|
||||||
|
register_command_group as command_group,
|
||||||
|
register_event_message_type as event_message_type,
|
||||||
|
register_regex as regex,
|
||||||
|
register_platform_adapter_type as platform_adapter_type,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.filter.event_message_type import (
|
||||||
|
EventMessageTypeFilter,
|
||||||
|
EventMessageType,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.filter.platform_adapter_type import (
|
||||||
|
PlatformAdapterTypeFilter,
|
||||||
|
PlatformAdapterType,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_star as register, # 注册插件(Star)
|
||||||
|
)
|
||||||
|
from astrbot.core.star import Context, Star
|
||||||
|
from astrbot.core.star.config import *
|
||||||
|
|
||||||
|
|
||||||
|
# provider
|
||||||
|
from astrbot.core.provider import Provider, Personality, ProviderMetaData
|
||||||
|
|
||||||
|
# platform
|
||||||
|
from astrbot.core.platform import (
|
||||||
|
AstrMessageEvent,
|
||||||
|
Platform,
|
||||||
|
AstrBotMessage,
|
||||||
|
MessageMember,
|
||||||
|
MessageType,
|
||||||
|
PlatformMetadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform.register import register_platform_adapter
|
||||||
|
|
||||||
|
from .message_components import *
|
||||||
18
astrbot/api/event/__init__.py
Normal file
18
astrbot/api/event/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from astrbot.core.message.message_event_result import (
|
||||||
|
MessageEventResult,
|
||||||
|
MessageChain,
|
||||||
|
CommandResult,
|
||||||
|
EventResultType,
|
||||||
|
ResultContentType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform import AstrMessageEvent
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"MessageEventResult",
|
||||||
|
"MessageChain",
|
||||||
|
"CommandResult",
|
||||||
|
"EventResultType",
|
||||||
|
"AstrMessageEvent",
|
||||||
|
"ResultContentType",
|
||||||
|
]
|
||||||
49
astrbot/api/event/filter/__init__.py
Normal file
49
astrbot/api/event/filter/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_command as command,
|
||||||
|
register_command_group as command_group,
|
||||||
|
register_event_message_type as event_message_type,
|
||||||
|
register_regex as regex,
|
||||||
|
register_platform_adapter_type as platform_adapter_type,
|
||||||
|
register_permission_type as permission_type,
|
||||||
|
register_custom_filter as custom_filter,
|
||||||
|
register_on_astrbot_loaded as on_astrbot_loaded,
|
||||||
|
register_on_llm_request as on_llm_request,
|
||||||
|
register_on_llm_response as on_llm_response,
|
||||||
|
register_llm_tool as llm_tool,
|
||||||
|
register_on_decorating_result as on_decorating_result,
|
||||||
|
register_after_message_sent as after_message_sent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.star.filter.event_message_type import (
|
||||||
|
EventMessageTypeFilter,
|
||||||
|
EventMessageType,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.filter.platform_adapter_type import (
|
||||||
|
PlatformAdapterTypeFilter,
|
||||||
|
PlatformAdapterType,
|
||||||
|
)
|
||||||
|
from astrbot.core.star.filter.permission import PermissionTypeFilter, PermissionType
|
||||||
|
from astrbot.core.star.filter.custom_filter import CustomFilter
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"command",
|
||||||
|
"command_group",
|
||||||
|
"event_message_type",
|
||||||
|
"regex",
|
||||||
|
"platform_adapter_type",
|
||||||
|
"permission_type",
|
||||||
|
"EventMessageTypeFilter",
|
||||||
|
"EventMessageType",
|
||||||
|
"PlatformAdapterTypeFilter",
|
||||||
|
"PlatformAdapterType",
|
||||||
|
"PermissionTypeFilter",
|
||||||
|
"CustomFilter",
|
||||||
|
"custom_filter",
|
||||||
|
"PermissionType",
|
||||||
|
"on_astrbot_loaded",
|
||||||
|
"on_llm_request",
|
||||||
|
"llm_tool",
|
||||||
|
"on_decorating_result",
|
||||||
|
"after_message_sent",
|
||||||
|
"on_llm_response",
|
||||||
|
]
|
||||||
1
astrbot/api/message_components.py
Normal file
1
astrbot/api/message_components.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from astrbot.core.message.components import *
|
||||||
23
astrbot/api/platform/__init__.py
Normal file
23
astrbot/api/platform/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from astrbot.core.platform import (
|
||||||
|
AstrMessageEvent,
|
||||||
|
Platform,
|
||||||
|
AstrBotMessage,
|
||||||
|
MessageMember,
|
||||||
|
MessageType,
|
||||||
|
PlatformMetadata,
|
||||||
|
Group,
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.platform.register import register_platform_adapter
|
||||||
|
from astrbot.core.message.components import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AstrMessageEvent",
|
||||||
|
"Platform",
|
||||||
|
"AstrBotMessage",
|
||||||
|
"MessageMember",
|
||||||
|
"MessageType",
|
||||||
|
"PlatformMetadata",
|
||||||
|
"register_platform_adapter",
|
||||||
|
"Group",
|
||||||
|
]
|
||||||
17
astrbot/api/provider/__init__.py
Normal file
17
astrbot/api/provider/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from astrbot.core.provider import Provider, STTProvider, Personality
|
||||||
|
from astrbot.core.provider.entities import (
|
||||||
|
ProviderRequest,
|
||||||
|
ProviderType,
|
||||||
|
ProviderMetaData,
|
||||||
|
LLMResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Provider",
|
||||||
|
"STTProvider",
|
||||||
|
"Personality",
|
||||||
|
"ProviderRequest",
|
||||||
|
"ProviderType",
|
||||||
|
"ProviderMetaData",
|
||||||
|
"LLMResponse",
|
||||||
|
]
|
||||||
8
astrbot/api/star/__init__.py
Normal file
8
astrbot/api/star/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from astrbot.core.star.register import (
|
||||||
|
register_star as register, # 注册插件(Star)
|
||||||
|
)
|
||||||
|
|
||||||
|
from astrbot.core.star import Context, Star, StarTools
|
||||||
|
from astrbot.core.star.config import *
|
||||||
|
|
||||||
|
__all__ = ["register", "Context", "Star", "StarTools"]
|
||||||
7
astrbot/api/util/__init__.py
Normal file
7
astrbot/api/util/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from astrbot.core.utils.session_waiter import (
|
||||||
|
SessionWaiter,
|
||||||
|
SessionController,
|
||||||
|
session_waiter,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["SessionWaiter", "SessionController", "session_waiter"]
|
||||||
1
astrbot/cli/__init__.py
Normal file
1
astrbot/cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = "3.5.23"
|
||||||
59
astrbot/cli/__main__.py
Normal file
59
astrbot/cli/__main__.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
AstrBot CLI入口
|
||||||
|
"""
|
||||||
|
|
||||||
|
import click
|
||||||
|
import sys
|
||||||
|
from . import __version__
|
||||||
|
from .commands import init, run, plug, conf
|
||||||
|
|
||||||
|
logo_tmpl = r"""
|
||||||
|
___ _______.___________..______ .______ ______ .___________.
|
||||||
|
/ \ / | || _ \ | _ \ / __ \ | |
|
||||||
|
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
|
||||||
|
/ /_\ \ \ \ | | | / | _ < | | | | | |
|
||||||
|
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
|
||||||
|
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(__version__, prog_name="AstrBot")
|
||||||
|
def cli() -> None:
|
||||||
|
"""The AstrBot CLI"""
|
||||||
|
click.echo(logo_tmpl)
|
||||||
|
click.echo("Welcome to AstrBot CLI!")
|
||||||
|
click.echo(f"AstrBot CLI version: {__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("command_name", required=False, type=str)
|
||||||
|
def help(command_name: str | None) -> None:
|
||||||
|
"""显示命令的帮助信息
|
||||||
|
|
||||||
|
如果提供了 COMMAND_NAME,则显示该命令的详细帮助信息。
|
||||||
|
否则,显示通用帮助信息。
|
||||||
|
"""
|
||||||
|
ctx = click.get_current_context()
|
||||||
|
if command_name:
|
||||||
|
# 查找指定命令
|
||||||
|
command = cli.get_command(ctx, command_name)
|
||||||
|
if command:
|
||||||
|
# 显示特定命令的帮助信息
|
||||||
|
click.echo(command.get_help(ctx))
|
||||||
|
else:
|
||||||
|
click.echo(f"Unknown command: {command_name}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# 显示通用帮助信息
|
||||||
|
click.echo(cli.get_help(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
cli.add_command(init)
|
||||||
|
cli.add_command(run)
|
||||||
|
cli.add_command(help)
|
||||||
|
cli.add_command(plug)
|
||||||
|
cli.add_command(conf)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli()
|
||||||
6
astrbot/cli/commands/__init__.py
Normal file
6
astrbot/cli/commands/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .cmd_init import init
|
||||||
|
from .cmd_run import run
|
||||||
|
from .cmd_plug import plug
|
||||||
|
from .cmd_conf import conf
|
||||||
|
|
||||||
|
__all__ = ["init", "run", "plug", "conf"]
|
||||||
206
astrbot/cli/commands/cmd_conf.py
Normal file
206
astrbot/cli/commands/cmd_conf.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import json
|
||||||
|
import click
|
||||||
|
import hashlib
|
||||||
|
import zoneinfo
|
||||||
|
from typing import Any, Callable
|
||||||
|
from ..utils import get_astrbot_root, check_astrbot_root
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_log_level(value: str) -> str:
|
||||||
|
"""验证日志级别"""
|
||||||
|
value = value.upper()
|
||||||
|
if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||||
|
raise click.ClickException(
|
||||||
|
"日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dashboard_port(value: str) -> int:
|
||||||
|
"""验证 Dashboard 端口"""
|
||||||
|
try:
|
||||||
|
port = int(value)
|
||||||
|
if port < 1 or port > 65535:
|
||||||
|
raise click.ClickException("端口必须在 1-65535 范围内")
|
||||||
|
return port
|
||||||
|
except ValueError:
|
||||||
|
raise click.ClickException("端口必须是数字")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dashboard_username(value: str) -> str:
|
||||||
|
"""验证 Dashboard 用户名"""
|
||||||
|
if not value:
|
||||||
|
raise click.ClickException("用户名不能为空")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dashboard_password(value: str) -> str:
|
||||||
|
"""验证 Dashboard 密码"""
|
||||||
|
if not value:
|
||||||
|
raise click.ClickException("密码不能为空")
|
||||||
|
return hashlib.md5(value.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_timezone(value: str) -> str:
|
||||||
|
"""验证时区"""
|
||||||
|
try:
|
||||||
|
zoneinfo.ZoneInfo(value)
|
||||||
|
except Exception:
|
||||||
|
raise click.ClickException(f"无效的时区: {value},请使用有效的IANA时区名称")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_callback_api_base(value: str) -> str:
|
||||||
|
"""验证回调接口基址"""
|
||||||
|
if not value.startswith("http://") and not value.startswith("https://"):
|
||||||
|
raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# 可通过CLI设置的配置项,配置键到验证器函数的映射
|
||||||
|
CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
|
||||||
|
"timezone": _validate_timezone,
|
||||||
|
"log_level": _validate_log_level,
|
||||||
|
"dashboard.port": _validate_dashboard_port,
|
||||||
|
"dashboard.username": _validate_dashboard_username,
|
||||||
|
"dashboard.password": _validate_dashboard_password,
|
||||||
|
"callback_api_base": _validate_callback_api_base,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config() -> dict[str, Any]:
|
||||||
|
"""加载或初始化配置文件"""
|
||||||
|
root = get_astrbot_root()
|
||||||
|
if not check_astrbot_root(root):
|
||||||
|
raise click.ClickException(
|
||||||
|
f"{root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
|
||||||
|
)
|
||||||
|
|
||||||
|
config_path = root / "data" / "cmd_config.json"
|
||||||
|
if not config_path.exists():
|
||||||
|
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||||
|
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(config_path.read_text(encoding="utf-8-sig"))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise click.ClickException(f"配置文件解析失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _save_config(config: dict[str, Any]) -> None:
|
||||||
|
"""保存配置文件"""
|
||||||
|
config_path = get_astrbot_root() / "data" / "cmd_config.json"
|
||||||
|
|
||||||
|
config_path.write_text(
|
||||||
|
json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
|
||||||
|
"""设置嵌套字典中的值"""
|
||||||
|
parts = path.split(".")
|
||||||
|
for part in parts[:-1]:
|
||||||
|
if part not in obj:
|
||||||
|
obj[part] = {}
|
||||||
|
elif not isinstance(obj[part], dict):
|
||||||
|
raise click.ClickException(
|
||||||
|
f"配置路径冲突: {'.'.join(parts[: parts.index(part) + 1])} 不是字典"
|
||||||
|
)
|
||||||
|
obj = obj[part]
|
||||||
|
obj[parts[-1]] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
|
||||||
|
"""获取嵌套字典中的值"""
|
||||||
|
parts = path.split(".")
|
||||||
|
for part in parts:
|
||||||
|
obj = obj[part]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(name="conf")
|
||||||
|
def conf():
|
||||||
|
"""配置管理命令
|
||||||
|
|
||||||
|
支持的配置项:
|
||||||
|
|
||||||
|
- timezone: 时区设置 (例如: Asia/Shanghai)
|
||||||
|
|
||||||
|
- log_level: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
|
||||||
|
|
||||||
|
- dashboard.port: Dashboard 端口
|
||||||
|
|
||||||
|
- dashboard.username: Dashboard 用户名
|
||||||
|
|
||||||
|
- dashboard.password: Dashboard 密码
|
||||||
|
|
||||||
|
- callback_api_base: 回调接口基址
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@conf.command(name="set")
|
||||||
|
@click.argument("key")
|
||||||
|
@click.argument("value")
|
||||||
|
def set_config(key: str, value: str):
|
||||||
|
"""设置配置项的值"""
|
||||||
|
if key not in CONFIG_VALIDATORS.keys():
|
||||||
|
raise click.ClickException(f"不支持的配置项: {key}")
|
||||||
|
|
||||||
|
config = _load_config()
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_value = _get_nested_item(config, key)
|
||||||
|
validated_value = CONFIG_VALIDATORS[key](value)
|
||||||
|
_set_nested_item(config, key, validated_value)
|
||||||
|
_save_config(config)
|
||||||
|
|
||||||
|
click.echo(f"配置已更新: {key}")
|
||||||
|
if key == "dashboard.password":
|
||||||
|
click.echo(" 原值: ********")
|
||||||
|
click.echo(" 新值: ********")
|
||||||
|
else:
|
||||||
|
click.echo(f" 原值: {old_value}")
|
||||||
|
click.echo(f" 新值: {validated_value}")
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
raise click.ClickException(f"未知的配置项: {key}")
|
||||||
|
except Exception as e:
|
||||||
|
raise click.UsageError(f"设置配置失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@conf.command(name="get")
|
||||||
|
@click.argument("key", required=False)
|
||||||
|
def get_config(key: str = None):
|
||||||
|
"""获取配置项的值,不提供key则显示所有可配置项"""
|
||||||
|
config = _load_config()
|
||||||
|
|
||||||
|
if key:
|
||||||
|
if key not in CONFIG_VALIDATORS.keys():
|
||||||
|
raise click.ClickException(f"不支持的配置项: {key}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = _get_nested_item(config, key)
|
||||||
|
if key == "dashboard.password":
|
||||||
|
value = "********"
|
||||||
|
click.echo(f"{key}: {value}")
|
||||||
|
except KeyError:
|
||||||
|
raise click.ClickException(f"未知的配置项: {key}")
|
||||||
|
except Exception as e:
|
||||||
|
raise click.UsageError(f"获取配置失败: {str(e)}")
|
||||||
|
else:
|
||||||
|
click.echo("当前配置:")
|
||||||
|
for key in CONFIG_VALIDATORS.keys():
|
||||||
|
try:
|
||||||
|
value = (
|
||||||
|
"********"
|
||||||
|
if key == "dashboard.password"
|
||||||
|
else _get_nested_item(config, key)
|
||||||
|
)
|
||||||
|
click.echo(f" {key}: {value}")
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
55
astrbot/cli/commands/cmd_init.py
Normal file
55
astrbot/cli/commands/cmd_init.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import click
|
||||||
|
from filelock import FileLock, Timeout
|
||||||
|
|
||||||
|
from ..utils import check_dashboard, get_astrbot_root
|
||||||
|
|
||||||
|
|
||||||
|
async def initialize_astrbot(astrbot_root) -> None:
|
||||||
|
"""执行 AstrBot 初始化逻辑"""
|
||||||
|
dot_astrbot = astrbot_root / ".astrbot"
|
||||||
|
|
||||||
|
if not dot_astrbot.exists():
|
||||||
|
click.echo(f"Current Directory: {astrbot_root}")
|
||||||
|
click.echo(
|
||||||
|
"如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。"
|
||||||
|
)
|
||||||
|
if click.confirm(
|
||||||
|
f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}",
|
||||||
|
default=True,
|
||||||
|
abort=True,
|
||||||
|
):
|
||||||
|
dot_astrbot.touch()
|
||||||
|
click.echo(f"Created {dot_astrbot}")
|
||||||
|
|
||||||
|
paths = {
|
||||||
|
"data": astrbot_root / "data",
|
||||||
|
"config": astrbot_root / "data" / "config",
|
||||||
|
"plugins": astrbot_root / "data" / "plugins",
|
||||||
|
"temp": astrbot_root / "data" / "temp",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, path in paths.items():
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")
|
||||||
|
|
||||||
|
await check_dashboard(astrbot_root / "data")
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def init() -> None:
|
||||||
|
"""初始化 AstrBot"""
|
||||||
|
click.echo("Initializing AstrBot...")
|
||||||
|
astrbot_root = get_astrbot_root()
|
||||||
|
lock_file = astrbot_root / "astrbot.lock"
|
||||||
|
lock = FileLock(lock_file, timeout=5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock.acquire():
|
||||||
|
asyncio.run(initialize_astrbot(astrbot_root))
|
||||||
|
except Timeout:
|
||||||
|
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise click.ClickException(f"初始化失败: {e!s}")
|
||||||
247
astrbot/cli/commands/cmd_plug.py
Normal file
247
astrbot/cli/commands/cmd_plug.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
get_git_repo,
|
||||||
|
build_plug_list,
|
||||||
|
manage_plugin,
|
||||||
|
PluginStatus,
|
||||||
|
check_astrbot_root,
|
||||||
|
get_astrbot_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def plug():
|
||||||
|
"""插件管理"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_data_path() -> Path:
|
||||||
|
base = get_astrbot_root()
|
||||||
|
if not check_astrbot_root(base):
|
||||||
|
raise click.ClickException(
|
||||||
|
f"{base}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
|
||||||
|
)
|
||||||
|
return (base / "data").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def display_plugins(plugins, title=None, color=None):
|
||||||
|
if title:
|
||||||
|
click.echo(click.style(title, fg=color, bold=True))
|
||||||
|
|
||||||
|
click.echo(f"{'名称':<20} {'版本':<10} {'状态':<10} {'作者':<15} {'描述':<30}")
|
||||||
|
click.echo("-" * 85)
|
||||||
|
|
||||||
|
for p in plugins:
|
||||||
|
desc = p["desc"][:30] + ("..." if len(p["desc"]) > 30 else "")
|
||||||
|
click.echo(
|
||||||
|
f"{p['name']:<20} {p['version']:<10} {p['status']:<10} "
|
||||||
|
f"{p['author']:<15} {desc:<30}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.argument("name")
|
||||||
|
def new(name: str):
|
||||||
|
"""创建新插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plug_path = base_path / "plugins" / name
|
||||||
|
|
||||||
|
if plug_path.exists():
|
||||||
|
raise click.ClickException(f"插件 {name} 已存在")
|
||||||
|
|
||||||
|
author = click.prompt("请输入插件作者", type=str)
|
||||||
|
desc = click.prompt("请输入插件描述", type=str)
|
||||||
|
version = click.prompt("请输入插件版本", type=str)
|
||||||
|
if not re.match(r"^\d+\.\d+(\.\d+)?$", version.lower().lstrip("v")):
|
||||||
|
raise click.ClickException("版本号必须为 x.y 或 x.y.z 格式")
|
||||||
|
repo = click.prompt("请输入插件仓库:", type=str)
|
||||||
|
if not repo.startswith("http"):
|
||||||
|
raise click.ClickException("仓库地址必须以 http 开头")
|
||||||
|
|
||||||
|
click.echo("下载插件模板...")
|
||||||
|
get_git_repo(
|
||||||
|
"https://github.com/Soulter/helloworld",
|
||||||
|
plug_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo("重写插件信息...")
|
||||||
|
# 重写 metadata.yaml
|
||||||
|
with open(plug_path / "metadata.yaml", "w", encoding="utf-8") as f:
|
||||||
|
f.write(
|
||||||
|
f"name: {name}\n"
|
||||||
|
f"desc: {desc}\n"
|
||||||
|
f"version: {version}\n"
|
||||||
|
f"author: {author}\n"
|
||||||
|
f"repo: {repo}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 重写 README.md
|
||||||
|
with open(plug_path / "README.md", "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"# {name}\n\n{desc}\n\n# 支持\n\n[帮助文档](https://astrbot.app)\n")
|
||||||
|
|
||||||
|
# 重写 main.py
|
||||||
|
with open(plug_path / "main.py", "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
new_content = content.replace(
|
||||||
|
'@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0")',
|
||||||
|
f'@register("{name}", "{author}", "{desc}", "{version}")',
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(plug_path / "main.py", "w", encoding="utf-8") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
click.echo(f"插件 {name} 创建成功")
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
|
||||||
|
def list(all: bool):
|
||||||
|
"""列出插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
|
||||||
|
# 未发布的插件
|
||||||
|
not_published_plugins = [
|
||||||
|
p for p in plugins if p["status"] == PluginStatus.NOT_PUBLISHED
|
||||||
|
]
|
||||||
|
if not_published_plugins:
|
||||||
|
display_plugins(not_published_plugins, "未发布的插件", "red")
|
||||||
|
|
||||||
|
# 需要更新的插件
|
||||||
|
need_update_plugins = [
|
||||||
|
p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
|
||||||
|
]
|
||||||
|
if need_update_plugins:
|
||||||
|
display_plugins(need_update_plugins, "需要更新的插件", "yellow")
|
||||||
|
|
||||||
|
# 已安装的插件
|
||||||
|
installed_plugins = [p for p in plugins if p["status"] == PluginStatus.INSTALLED]
|
||||||
|
if installed_plugins:
|
||||||
|
display_plugins(installed_plugins, "已安装的插件", "green")
|
||||||
|
|
||||||
|
# 未安装的插件
|
||||||
|
not_installed_plugins = [
|
||||||
|
p for p in plugins if p["status"] == PluginStatus.NOT_INSTALLED
|
||||||
|
]
|
||||||
|
if not_installed_plugins and all:
|
||||||
|
display_plugins(not_installed_plugins, "未安装的插件", "blue")
|
||||||
|
|
||||||
|
if (
|
||||||
|
not any([not_published_plugins, need_update_plugins, installed_plugins])
|
||||||
|
and not all
|
||||||
|
):
|
||||||
|
click.echo("未安装任何插件")
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.argument("name")
|
||||||
|
@click.option("--proxy", help="代理服务器地址")
|
||||||
|
def install(name: str, proxy: str | None):
|
||||||
|
"""安装插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plug_path = base_path / "plugins"
|
||||||
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
|
||||||
|
plugin = next(
|
||||||
|
(
|
||||||
|
p
|
||||||
|
for p in plugins
|
||||||
|
if p["name"] == name and p["status"] == PluginStatus.NOT_INSTALLED
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not plugin:
|
||||||
|
raise click.ClickException(f"未找到可安装的插件 {name},可能是不存在或已安装")
|
||||||
|
|
||||||
|
manage_plugin(plugin, plug_path, is_update=False, proxy=proxy)
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.argument("name")
|
||||||
|
def remove(name: str):
|
||||||
|
"""卸载插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
plugin = next((p for p in plugins if p["name"] == name), None)
|
||||||
|
|
||||||
|
if not plugin or not plugin.get("local_path"):
|
||||||
|
raise click.ClickException(f"插件 {name} 不存在或未安装")
|
||||||
|
|
||||||
|
plugin_path = plugin["local_path"]
|
||||||
|
|
||||||
|
click.confirm(f"确定要卸载插件 {name} 吗?", default=False, abort=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(plugin_path)
|
||||||
|
click.echo(f"插件 {name} 已卸载")
|
||||||
|
except Exception as e:
|
||||||
|
raise click.ClickException(f"卸载插件 {name} 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.argument("name", required=False)
|
||||||
|
@click.option("--proxy", help="Github代理地址")
|
||||||
|
def update(name: str, proxy: str | None):
|
||||||
|
"""更新插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plug_path = base_path / "plugins"
|
||||||
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
|
||||||
|
if name:
|
||||||
|
plugin = next(
|
||||||
|
(
|
||||||
|
p
|
||||||
|
for p in plugins
|
||||||
|
if p["name"] == name and p["status"] == PluginStatus.NEED_UPDATE
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not plugin:
|
||||||
|
raise click.ClickException(f"插件 {name} 不需要更新或无法更新")
|
||||||
|
|
||||||
|
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
|
||||||
|
else:
|
||||||
|
need_update_plugins = [
|
||||||
|
p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
|
||||||
|
]
|
||||||
|
|
||||||
|
if not need_update_plugins:
|
||||||
|
click.echo("没有需要更新的插件")
|
||||||
|
return
|
||||||
|
|
||||||
|
click.echo(f"发现 {len(need_update_plugins)} 个插件需要更新")
|
||||||
|
for plugin in need_update_plugins:
|
||||||
|
plugin_name = plugin["name"]
|
||||||
|
click.echo(f"正在更新插件 {plugin_name}...")
|
||||||
|
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
|
||||||
|
|
||||||
|
|
||||||
|
@plug.command()
|
||||||
|
@click.argument("query")
|
||||||
|
def search(query: str):
|
||||||
|
"""搜索插件"""
|
||||||
|
base_path = _get_data_path()
|
||||||
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
|
||||||
|
matched_plugins = [
|
||||||
|
p
|
||||||
|
for p in plugins
|
||||||
|
if query.lower() in p["name"].lower()
|
||||||
|
or query.lower() in p["desc"].lower()
|
||||||
|
or query.lower() in p["author"].lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
if not matched_plugins:
|
||||||
|
click.echo(f"未找到匹配 '{query}' 的插件")
|
||||||
|
return
|
||||||
|
|
||||||
|
display_plugins(matched_plugins, f"搜索结果: '{query}'", "cyan")
|
||||||
63
astrbot/cli/commands/cmd_run.py
Normal file
63
astrbot/cli/commands/cmd_run.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from filelock import FileLock, Timeout
|
||||||
|
|
||||||
|
from ..utils import check_dashboard, check_astrbot_root, get_astrbot_root
|
||||||
|
|
||||||
|
|
||||||
|
async def run_astrbot(astrbot_root: Path):
|
||||||
|
"""运行 AstrBot"""
|
||||||
|
from astrbot.core import logger, LogManager, LogBroker, db_helper
|
||||||
|
from astrbot.core.initial_loader import InitialLoader
|
||||||
|
|
||||||
|
await check_dashboard(astrbot_root / "data")
|
||||||
|
|
||||||
|
log_broker = LogBroker()
|
||||||
|
LogManager.set_queue_handler(logger, log_broker)
|
||||||
|
db = db_helper
|
||||||
|
|
||||||
|
core_lifecycle = InitialLoader(db, log_broker)
|
||||||
|
|
||||||
|
await core_lifecycle.start()
|
||||||
|
|
||||||
|
|
||||||
|
@click.option("--reload", "-r", is_flag=True, help="插件自动重载")
|
||||||
|
@click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str)
|
||||||
|
@click.command()
|
||||||
|
def run(reload: bool, port: str) -> None:
|
||||||
|
"""运行 AstrBot"""
|
||||||
|
try:
|
||||||
|
os.environ["ASTRBOT_CLI"] = "1"
|
||||||
|
astrbot_root = get_astrbot_root()
|
||||||
|
|
||||||
|
if not check_astrbot_root(astrbot_root):
|
||||||
|
raise click.ClickException(
|
||||||
|
f"{astrbot_root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
|
||||||
|
)
|
||||||
|
|
||||||
|
os.environ["ASTRBOT_ROOT"] = str(astrbot_root)
|
||||||
|
sys.path.insert(0, str(astrbot_root))
|
||||||
|
|
||||||
|
if port:
|
||||||
|
os.environ["DASHBOARD_PORT"] = port
|
||||||
|
|
||||||
|
if reload:
|
||||||
|
click.echo("启用插件自动重载")
|
||||||
|
os.environ["ASTRBOT_RELOAD"] = "1"
|
||||||
|
|
||||||
|
lock_file = astrbot_root / "astrbot.lock"
|
||||||
|
lock = FileLock(lock_file, timeout=5)
|
||||||
|
with lock.acquire():
|
||||||
|
asyncio.run(run_astrbot(astrbot_root))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
click.echo("AstrBot 已关闭...")
|
||||||
|
except Timeout:
|
||||||
|
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
|
||||||
|
except Exception as e:
|
||||||
|
raise click.ClickException(f"运行时出现错误: {e}\n{traceback.format_exc()}")
|
||||||
18
astrbot/cli/utils/__init__.py
Normal file
18
astrbot/cli/utils/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from .basic import (
|
||||||
|
get_astrbot_root,
|
||||||
|
check_astrbot_root,
|
||||||
|
check_dashboard,
|
||||||
|
)
|
||||||
|
from .plugin import get_git_repo, manage_plugin, build_plug_list, PluginStatus
|
||||||
|
from .version_comparator import VersionComparator
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_astrbot_root",
|
||||||
|
"check_astrbot_root",
|
||||||
|
"check_dashboard",
|
||||||
|
"get_git_repo",
|
||||||
|
"manage_plugin",
|
||||||
|
"build_plug_list",
|
||||||
|
"VersionComparator",
|
||||||
|
"PluginStatus",
|
||||||
|
]
|
||||||
67
astrbot/cli/utils/basic.py
Normal file
67
astrbot/cli/utils/basic.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
def check_astrbot_root(path: str | Path) -> bool:
|
||||||
|
"""检查路径是否为 AstrBot 根目录"""
|
||||||
|
if not isinstance(path, Path):
|
||||||
|
path = Path(path)
|
||||||
|
if not path.exists() or not path.is_dir():
|
||||||
|
return False
|
||||||
|
if not (path / ".astrbot").exists():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_astrbot_root() -> Path:
|
||||||
|
"""获取Astrbot根目录路径"""
|
||||||
|
return Path.cwd()
|
||||||
|
|
||||||
|
|
||||||
|
async def check_dashboard(astrbot_root: Path) -> None:
|
||||||
|
"""检查是否安装了dashboard"""
|
||||||
|
from astrbot.core.utils.io import get_dashboard_version, download_dashboard
|
||||||
|
from astrbot.core.config.default import VERSION
|
||||||
|
from .version_comparator import VersionComparator
|
||||||
|
|
||||||
|
try:
|
||||||
|
dashboard_version = await get_dashboard_version()
|
||||||
|
match dashboard_version:
|
||||||
|
case None:
|
||||||
|
click.echo("未安装管理面板")
|
||||||
|
if click.confirm(
|
||||||
|
"是否安装管理面板?",
|
||||||
|
default=True,
|
||||||
|
abort=True,
|
||||||
|
):
|
||||||
|
click.echo("正在安装管理面板...")
|
||||||
|
await download_dashboard(
|
||||||
|
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
||||||
|
)
|
||||||
|
click.echo("管理面板安装完成")
|
||||||
|
|
||||||
|
case str():
|
||||||
|
if VersionComparator.compare_version(VERSION, dashboard_version) <= 0:
|
||||||
|
click.echo("管理面板已是最新版本")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
version = dashboard_version.split("v")[1]
|
||||||
|
click.echo(f"管理面板版本: {version}")
|
||||||
|
await download_dashboard(
|
||||||
|
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"下载管理面板失败: {e}")
|
||||||
|
return
|
||||||
|
except FileNotFoundError:
|
||||||
|
click.echo("初始化管理面板目录...")
|
||||||
|
try:
|
||||||
|
await download_dashboard(
|
||||||
|
path=str(astrbot_root / "dashboard.zip"), extract_path=str(astrbot_root)
|
||||||
|
)
|
||||||
|
click.echo("管理面板初始化完成")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"下载管理面板失败: {e}")
|
||||||
|
return
|
||||||
233
astrbot/cli/utils/plugin.py
Normal file
233
astrbot/cli/utils/plugin.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import yaml
|
||||||
|
from enum import Enum
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
import click
|
||||||
|
from .version_comparator import VersionComparator
|
||||||
|
|
||||||
|
|
||||||
|
class PluginStatus(str, Enum):
|
||||||
|
INSTALLED = "已安装"
|
||||||
|
NEED_UPDATE = "需更新"
|
||||||
|
NOT_INSTALLED = "未安装"
|
||||||
|
NOT_PUBLISHED = "未发布"
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_repo(url: str, target_path: Path, proxy: str | None = None):
|
||||||
|
"""从 Git 仓库下载代码并解压到指定路径"""
|
||||||
|
temp_dir = Path(tempfile.mkdtemp())
|
||||||
|
try:
|
||||||
|
# 解析仓库信息
|
||||||
|
repo_namespace = url.split("/")[-2:]
|
||||||
|
author = repo_namespace[0]
|
||||||
|
repo = repo_namespace[1]
|
||||||
|
|
||||||
|
# 尝试获取最新的 release
|
||||||
|
release_url = f"https://api.github.com/repos/{author}/{repo}/releases"
|
||||||
|
try:
|
||||||
|
with httpx.Client(
|
||||||
|
proxy=proxy if proxy else None, follow_redirects=True
|
||||||
|
) as client:
|
||||||
|
resp = client.get(release_url)
|
||||||
|
resp.raise_for_status()
|
||||||
|
releases = resp.json()
|
||||||
|
|
||||||
|
if releases:
|
||||||
|
# 使用最新的 release
|
||||||
|
download_url = releases[0]["zipball_url"]
|
||||||
|
else:
|
||||||
|
# 没有 release,使用默认分支
|
||||||
|
click.echo(f"正在从默认分支下载 {author}/{repo}")
|
||||||
|
download_url = f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip"
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"获取 release 信息失败: {e},将直接使用提供的 URL")
|
||||||
|
download_url = url
|
||||||
|
|
||||||
|
# 应用代理
|
||||||
|
if proxy:
|
||||||
|
download_url = f"{proxy}/{download_url}"
|
||||||
|
|
||||||
|
# 下载并解压
|
||||||
|
with httpx.Client(
|
||||||
|
proxy=proxy if proxy else None, follow_redirects=True
|
||||||
|
) as client:
|
||||||
|
resp = client.get(download_url)
|
||||||
|
if (
|
||||||
|
resp.status_code == 404
|
||||||
|
and "archive/refs/heads/master.zip" in download_url
|
||||||
|
):
|
||||||
|
alt_url = download_url.replace("master.zip", "main.zip")
|
||||||
|
click.echo("master 分支不存在,尝试下载 main 分支")
|
||||||
|
resp = client.get(alt_url)
|
||||||
|
resp.raise_for_status()
|
||||||
|
else:
|
||||||
|
resp.raise_for_status()
|
||||||
|
zip_content = BytesIO(resp.content)
|
||||||
|
with ZipFile(zip_content) as z:
|
||||||
|
z.extractall(temp_dir)
|
||||||
|
namelist = z.namelist()
|
||||||
|
root_dir = Path(namelist[0]).parts[0] if namelist else ""
|
||||||
|
if target_path.exists():
|
||||||
|
shutil.rmtree(target_path)
|
||||||
|
shutil.move(temp_dir / root_dir, target_path)
|
||||||
|
finally:
|
||||||
|
if temp_dir.exists():
|
||||||
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml_metadata(plugin_dir: Path) -> dict:
|
||||||
|
"""从 metadata.yaml 文件加载插件元数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_dir: 插件目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含元数据的字典,如果读取失败则返回空字典
|
||||||
|
"""
|
||||||
|
yaml_path = plugin_dir / "metadata.yaml"
|
||||||
|
if yaml_path.exists():
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"读取 {yaml_path} 失败: {e}", err=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def build_plug_list(plugins_dir: Path) -> list:
|
||||||
|
"""构建插件列表,包含本地和在线插件信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugins_dir (Path): 插件目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 包含插件信息的字典列表
|
||||||
|
"""
|
||||||
|
# 获取本地插件信息
|
||||||
|
result = []
|
||||||
|
if plugins_dir.exists():
|
||||||
|
for plugin_name in [d.name for d in plugins_dir.glob("*") if d.is_dir()]:
|
||||||
|
plugin_dir = plugins_dir / plugin_name
|
||||||
|
|
||||||
|
# 从 metadata.yaml 加载元数据
|
||||||
|
metadata = load_yaml_metadata(plugin_dir)
|
||||||
|
|
||||||
|
if "desc" not in metadata and "description" in metadata:
|
||||||
|
metadata["desc"] = metadata["description"]
|
||||||
|
|
||||||
|
# 如果成功加载元数据,添加到结果列表
|
||||||
|
if metadata and all(
|
||||||
|
k in metadata for k in ["name", "desc", "version", "author", "repo"]
|
||||||
|
):
|
||||||
|
result.append({
|
||||||
|
"name": str(metadata.get("name", "")),
|
||||||
|
"desc": str(metadata.get("desc", "")),
|
||||||
|
"version": str(metadata.get("version", "")),
|
||||||
|
"author": str(metadata.get("author", "")),
|
||||||
|
"repo": str(metadata.get("repo", "")),
|
||||||
|
"status": PluginStatus.INSTALLED,
|
||||||
|
"local_path": str(plugin_dir),
|
||||||
|
})
|
||||||
|
|
||||||
|
# 获取在线插件列表
|
||||||
|
online_plugins = []
|
||||||
|
try:
|
||||||
|
with httpx.Client() as client:
|
||||||
|
resp = client.get("https://api.soulter.top/astrbot/plugins")
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
for plugin_id, plugin_info in data.items():
|
||||||
|
online_plugins.append({
|
||||||
|
"name": str(plugin_id),
|
||||||
|
"desc": str(plugin_info.get("desc", "")),
|
||||||
|
"version": str(plugin_info.get("version", "")),
|
||||||
|
"author": str(plugin_info.get("author", "")),
|
||||||
|
"repo": str(plugin_info.get("repo", "")),
|
||||||
|
"status": PluginStatus.NOT_INSTALLED,
|
||||||
|
"local_path": None,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"获取在线插件列表失败: {e}", err=True)
|
||||||
|
|
||||||
|
# 与在线插件比对,更新状态
|
||||||
|
online_plugin_names = {plugin["name"] for plugin in online_plugins}
|
||||||
|
for local_plugin in result:
|
||||||
|
if local_plugin["name"] in online_plugin_names:
|
||||||
|
# 查找对应的在线插件
|
||||||
|
online_plugin = next(
|
||||||
|
p for p in online_plugins if p["name"] == local_plugin["name"]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
VersionComparator.compare_version(
|
||||||
|
local_plugin["version"], online_plugin["version"]
|
||||||
|
)
|
||||||
|
< 0
|
||||||
|
):
|
||||||
|
local_plugin["status"] = PluginStatus.NEED_UPDATE
|
||||||
|
else:
|
||||||
|
# 本地插件未在线上发布
|
||||||
|
local_plugin["status"] = PluginStatus.NOT_PUBLISHED
|
||||||
|
|
||||||
|
# 添加未安装的在线插件
|
||||||
|
for online_plugin in online_plugins:
|
||||||
|
if not any(plugin["name"] == online_plugin["name"] for plugin in result):
|
||||||
|
result.append(online_plugin)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def manage_plugin(
|
||||||
|
plugin: dict, plugins_dir: Path, is_update: bool = False, proxy: str | None = None
|
||||||
|
) -> None:
|
||||||
|
"""安装或更新插件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin (dict): 插件信息字典
|
||||||
|
plugins_dir (Path): 插件目录
|
||||||
|
is_update (bool, optional): 是否为更新操作. 默认为 False
|
||||||
|
proxy (str, optional): 代理服务器地址
|
||||||
|
"""
|
||||||
|
plugin_name = plugin["name"]
|
||||||
|
repo_url = plugin["repo"]
|
||||||
|
|
||||||
|
# 如果是更新且有本地路径,直接使用本地路径
|
||||||
|
if is_update and plugin.get("local_path"):
|
||||||
|
target_path = Path(plugin["local_path"])
|
||||||
|
else:
|
||||||
|
target_path = plugins_dir / plugin_name
|
||||||
|
|
||||||
|
backup_path = Path(f"{target_path}_backup") if is_update else None
|
||||||
|
|
||||||
|
# 检查插件是否存在
|
||||||
|
if is_update and not target_path.exists():
|
||||||
|
raise click.ClickException(f"插件 {plugin_name} 未安装,无法更新")
|
||||||
|
|
||||||
|
# 备份现有插件
|
||||||
|
if is_update and backup_path.exists():
|
||||||
|
shutil.rmtree(backup_path)
|
||||||
|
if is_update:
|
||||||
|
shutil.copytree(target_path, backup_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
click.echo(
|
||||||
|
f"正在从 {repo_url} {'更新' if is_update else '下载'}插件 {plugin_name}..."
|
||||||
|
)
|
||||||
|
get_git_repo(repo_url, target_path, proxy)
|
||||||
|
|
||||||
|
# 更新成功,删除备份
|
||||||
|
if is_update and backup_path.exists():
|
||||||
|
shutil.rmtree(backup_path)
|
||||||
|
click.echo(f"插件 {plugin_name} {'更新' if is_update else '安装'}成功")
|
||||||
|
except Exception as e:
|
||||||
|
if target_path.exists():
|
||||||
|
shutil.rmtree(target_path, ignore_errors=True)
|
||||||
|
if is_update and backup_path.exists():
|
||||||
|
shutil.move(backup_path, target_path)
|
||||||
|
raise click.ClickException(
|
||||||
|
f"{'更新' if is_update else '安装'}插件 {plugin_name} 时出错: {e}"
|
||||||
|
)
|
||||||
92
astrbot/cli/utils/version_comparator.py
Normal file
92
astrbot/cli/utils/version_comparator.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
拷贝自 astrbot.core.utils.version_comparator
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class VersionComparator:
|
||||||
|
@staticmethod
|
||||||
|
def compare_version(v1: str, v2: str) -> int:
|
||||||
|
"""根据 Semver 语义版本规范来比较版本号的大小。支持不仅局限于 3 个数字的版本号,并处理预发布标签。
|
||||||
|
|
||||||
|
参考: https://semver.org/lang/zh-CN/
|
||||||
|
|
||||||
|
返回 1 表示 v1 > v2,返回 -1 表示 v1 < v2,返回 0 表示 v1 = v2。
|
||||||
|
"""
|
||||||
|
v1 = v1.lower().replace("v", "")
|
||||||
|
v2 = v2.lower().replace("v", "")
|
||||||
|
|
||||||
|
def split_version(version):
|
||||||
|
match = re.match(
|
||||||
|
r"^([0-9]+(?:\.[0-9]+)*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(.+))?$",
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
return [], None
|
||||||
|
major_minor_patch = match.group(1).split(".")
|
||||||
|
prerelease = match.group(2)
|
||||||
|
# buildmetadata = match.group(3) # 构建元数据在比较时忽略
|
||||||
|
parts = [int(x) for x in major_minor_patch]
|
||||||
|
prerelease = VersionComparator._split_prerelease(prerelease)
|
||||||
|
return parts, prerelease
|
||||||
|
|
||||||
|
v1_parts, v1_prerelease = split_version(v1)
|
||||||
|
v2_parts, v2_prerelease = split_version(v2)
|
||||||
|
|
||||||
|
# 比较数字部分
|
||||||
|
length = max(len(v1_parts), len(v2_parts))
|
||||||
|
v1_parts.extend([0] * (length - len(v1_parts)))
|
||||||
|
v2_parts.extend([0] * (length - len(v2_parts)))
|
||||||
|
|
||||||
|
for i in range(length):
|
||||||
|
if v1_parts[i] > v2_parts[i]:
|
||||||
|
return 1
|
||||||
|
elif v1_parts[i] < v2_parts[i]:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# 比较预发布标签
|
||||||
|
if v1_prerelease is None and v2_prerelease is not None:
|
||||||
|
return 1 # 没有预发布标签的版本高于有预发布标签的版本
|
||||||
|
elif v1_prerelease is not None and v2_prerelease is None:
|
||||||
|
return -1 # 有预发布标签的版本低于没有预发布标签的版本
|
||||||
|
elif v1_prerelease is not None and v2_prerelease is not None:
|
||||||
|
len_pre = max(len(v1_prerelease), len(v2_prerelease))
|
||||||
|
for i in range(len_pre):
|
||||||
|
p1 = v1_prerelease[i] if i < len(v1_prerelease) else None
|
||||||
|
p2 = v2_prerelease[i] if i < len(v2_prerelease) else None
|
||||||
|
|
||||||
|
if p1 is None and p2 is not None:
|
||||||
|
return -1
|
||||||
|
elif p1 is not None and p2 is None:
|
||||||
|
return 1
|
||||||
|
elif isinstance(p1, int) and isinstance(p2, str):
|
||||||
|
return -1
|
||||||
|
elif isinstance(p1, str) and isinstance(p2, int):
|
||||||
|
return 1
|
||||||
|
elif isinstance(p1, int) and isinstance(p2, int):
|
||||||
|
if p1 > p2:
|
||||||
|
return 1
|
||||||
|
elif p1 < p2:
|
||||||
|
return -1
|
||||||
|
elif isinstance(p1, str) and isinstance(p2, str):
|
||||||
|
if p1 > p2:
|
||||||
|
return 1
|
||||||
|
elif p1 < p2:
|
||||||
|
return -1
|
||||||
|
return 0 # 预发布标签完全相同
|
||||||
|
|
||||||
|
return 0 # 数字部分和预发布标签都相同
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _split_prerelease(prerelease):
|
||||||
|
if not prerelease:
|
||||||
|
return None
|
||||||
|
parts = prerelease.split(".")
|
||||||
|
result = []
|
||||||
|
for part in parts:
|
||||||
|
if part.isdigit():
|
||||||
|
result.append(int(part))
|
||||||
|
else:
|
||||||
|
result.append(part)
|
||||||
|
return result
|
||||||
491
astrbot/core.py
491
astrbot/core.py
@@ -1,491 +0,0 @@
|
|||||||
import re
|
|
||||||
import threading
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
import util.unfit_words as uw
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import util.agent.web_searcher as web_searcher
|
|
||||||
import util.plugin_util as putil
|
|
||||||
|
|
||||||
from nakuru.entities.components import Plain, At, Image
|
|
||||||
|
|
||||||
from addons.baidu_aip_judge import BaiduJudge
|
|
||||||
from model.provider.provider import Provider
|
|
||||||
from model.command.command import Command
|
|
||||||
from util import general_utils as gu
|
|
||||||
from util.general_utils import upload, run_monitor
|
|
||||||
from util.cmd_config import CmdConfig as cc
|
|
||||||
from util.cmd_config import init_astrbot_config_items
|
|
||||||
from type.types import GlobalObject
|
|
||||||
from type.register import *
|
|
||||||
from type.message import AstrBotMessage
|
|
||||||
from addons.dashboard.helper import DashBoardHelper
|
|
||||||
from addons.dashboard.server import DashBoardData
|
|
||||||
from persist.session import dbConn
|
|
||||||
from model.platform._message_result import MessageResult
|
|
||||||
from SparkleLogging.utils.core import LogManager
|
|
||||||
from logging import Logger
|
|
||||||
|
|
||||||
logger: Logger = LogManager.GetLogger(log_name='astrbot-core')
|
|
||||||
|
|
||||||
# 用户发言频率
|
|
||||||
user_frequency = {}
|
|
||||||
# 时间默认值
|
|
||||||
frequency_time = 60
|
|
||||||
# 计数默认值
|
|
||||||
frequency_count = 10
|
|
||||||
|
|
||||||
# 版本
|
|
||||||
version = '3.1.13'
|
|
||||||
|
|
||||||
# 语言模型
|
|
||||||
OPENAI_OFFICIAL = 'openai_official'
|
|
||||||
NONE_LLM = 'none_llm'
|
|
||||||
chosen_provider = None
|
|
||||||
# 语言模型对象
|
|
||||||
llm_instance: dict[str, Provider] = {}
|
|
||||||
llm_command_instance: dict[str, Command] = {}
|
|
||||||
llm_wake_prefix = ""
|
|
||||||
|
|
||||||
# 百度内容审核实例
|
|
||||||
baidu_judge = None
|
|
||||||
|
|
||||||
# CLI
|
|
||||||
PLATFORM_CLI = 'cli'
|
|
||||||
|
|
||||||
init_astrbot_config_items()
|
|
||||||
|
|
||||||
# 全局对象
|
|
||||||
_global_object: GlobalObject = None
|
|
||||||
|
|
||||||
# 语言模型选择
|
|
||||||
|
|
||||||
|
|
||||||
def privider_chooser(cfg):
|
|
||||||
l = []
|
|
||||||
if 'openai' in cfg and len(cfg['openai']['key']) > 0 and cfg['openai']['key'][0] is not None:
|
|
||||||
l.append('openai_official')
|
|
||||||
return l
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
初始化机器人
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
|
||||||
global llm_instance, llm_command_instance
|
|
||||||
global baidu_judge, chosen_provider
|
|
||||||
global frequency_count, frequency_time
|
|
||||||
global _global_object
|
|
||||||
|
|
||||||
# 迁移旧配置
|
|
||||||
gu.try_migrate_config()
|
|
||||||
# 使用新配置
|
|
||||||
cfg = cc.get_all()
|
|
||||||
|
|
||||||
_event_loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(_event_loop)
|
|
||||||
|
|
||||||
# 初始化 global_object
|
|
||||||
_global_object = GlobalObject()
|
|
||||||
_global_object.version = version
|
|
||||||
_global_object.base_config = cfg
|
|
||||||
logger.info("AstrBot v"+version)
|
|
||||||
|
|
||||||
if 'reply_prefix' in cfg:
|
|
||||||
# 适配旧版配置
|
|
||||||
if isinstance(cfg['reply_prefix'], dict):
|
|
||||||
_global_object.reply_prefix = ""
|
|
||||||
cfg['reply_prefix'] = ""
|
|
||||||
cc.put("reply_prefix", "")
|
|
||||||
else:
|
|
||||||
_global_object.reply_prefix = cfg['reply_prefix']
|
|
||||||
|
|
||||||
default_personality_str = cc.get("default_personality_str", "")
|
|
||||||
if default_personality_str == "":
|
|
||||||
_global_object.default_personality = None
|
|
||||||
else:
|
|
||||||
_global_object.default_personality = {
|
|
||||||
"name": "default",
|
|
||||||
"prompt": default_personality_str,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 语言模型提供商
|
|
||||||
logger.info("正在载入语言模型...")
|
|
||||||
prov = privider_chooser(cfg)
|
|
||||||
if OPENAI_OFFICIAL in prov:
|
|
||||||
logger.info("初始化:OpenAI官方")
|
|
||||||
if cfg['openai']['key'] is not None and cfg['openai']['key'] != [None]:
|
|
||||||
from model.provider.openai_official import ProviderOpenAIOfficial
|
|
||||||
from model.command.openai_official import CommandOpenAIOfficial
|
|
||||||
llm_instance[OPENAI_OFFICIAL] = ProviderOpenAIOfficial(
|
|
||||||
cfg['openai'])
|
|
||||||
llm_command_instance[OPENAI_OFFICIAL] = CommandOpenAIOfficial(
|
|
||||||
llm_instance[OPENAI_OFFICIAL], _global_object)
|
|
||||||
_global_object.llms.append(RegisteredLLM(
|
|
||||||
llm_name=OPENAI_OFFICIAL, llm_instance=llm_instance[OPENAI_OFFICIAL], origin="internal"))
|
|
||||||
chosen_provider = OPENAI_OFFICIAL
|
|
||||||
|
|
||||||
instance = llm_instance[OPENAI_OFFICIAL]
|
|
||||||
assert isinstance(instance, ProviderOpenAIOfficial)
|
|
||||||
instance.DEFAULT_PERSONALITY = _global_object.default_personality
|
|
||||||
instance.curr_personality = instance.DEFAULT_PERSONALITY
|
|
||||||
|
|
||||||
# 检查provider设置偏好
|
|
||||||
p = cc.get("chosen_provider", None)
|
|
||||||
if p is not None and p in llm_instance:
|
|
||||||
chosen_provider = p
|
|
||||||
|
|
||||||
# 百度内容审核
|
|
||||||
if 'baidu_aip' in cfg and 'enable' in cfg['baidu_aip'] and cfg['baidu_aip']['enable']:
|
|
||||||
try:
|
|
||||||
baidu_judge = BaiduJudge(cfg['baidu_aip'])
|
|
||||||
logger.info("百度内容审核初始化成功")
|
|
||||||
except BaseException as e:
|
|
||||||
logger.info("百度内容审核初始化失败")
|
|
||||||
|
|
||||||
threading.Thread(target=upload, args=(
|
|
||||||
_global_object, ), daemon=True).start()
|
|
||||||
|
|
||||||
# 得到发言频率配置
|
|
||||||
if 'limit' in cfg:
|
|
||||||
if 'count' in cfg['limit']:
|
|
||||||
frequency_count = cfg['limit']['count']
|
|
||||||
if 'time' in cfg['limit']:
|
|
||||||
frequency_time = cfg['limit']['time']
|
|
||||||
|
|
||||||
try:
|
|
||||||
if 'uniqueSessionMode' in cfg and cfg['uniqueSessionMode']:
|
|
||||||
_global_object.unique_session = True
|
|
||||||
else:
|
|
||||||
_global_object.unique_session = False
|
|
||||||
except BaseException as e:
|
|
||||||
logger.info("独立会话配置错误: "+str(e))
|
|
||||||
|
|
||||||
nick_qq = cc.get("nick_qq", None)
|
|
||||||
if nick_qq == None:
|
|
||||||
nick_qq = ("ai", "!", "!")
|
|
||||||
if isinstance(nick_qq, str):
|
|
||||||
nick_qq = (nick_qq,)
|
|
||||||
if isinstance(nick_qq, list):
|
|
||||||
nick_qq = tuple(nick_qq)
|
|
||||||
_global_object.nick = nick_qq
|
|
||||||
|
|
||||||
# 语言模型唤醒词
|
|
||||||
global llm_wake_prefix
|
|
||||||
llm_wake_prefix = cc.get("llm_wake_prefix", "")
|
|
||||||
|
|
||||||
logger.info("正在载入插件...")
|
|
||||||
# 加载插件
|
|
||||||
_command = Command(None, _global_object)
|
|
||||||
ok, err = putil.plugin_reload(_global_object.cached_plugins)
|
|
||||||
if ok:
|
|
||||||
logger.info(
|
|
||||||
f"成功载入 {len(_global_object.cached_plugins)} 个插件")
|
|
||||||
else:
|
|
||||||
logger.info(err)
|
|
||||||
|
|
||||||
if chosen_provider is None:
|
|
||||||
llm_command_instance[NONE_LLM] = _command
|
|
||||||
chosen_provider = NONE_LLM
|
|
||||||
|
|
||||||
logger.info("正在载入机器人消息平台")
|
|
||||||
# logger.info("提示:需要添加管理员 ID 才能使用 update/plugin 等指令),可在可视化面板添加。(如已添加可忽略)")
|
|
||||||
platform_str = ""
|
|
||||||
# GOCQ
|
|
||||||
if 'gocqbot' in cfg and cfg['gocqbot']['enable']:
|
|
||||||
logger.info("启用 QQ_GOCQ 机器人消息平台")
|
|
||||||
threading.Thread(target=run_gocq_bot, args=(
|
|
||||||
cfg, _global_object), daemon=True).start()
|
|
||||||
platform_str += "QQ_GOCQ,"
|
|
||||||
|
|
||||||
# QQ频道
|
|
||||||
if 'qqbot' in cfg and cfg['qqbot']['enable'] and cfg['qqbot']['appid'] != None:
|
|
||||||
logger.info("启用 QQ_OFFICIAL 机器人消息平台")
|
|
||||||
threading.Thread(target=run_qqchan_bot, args=(
|
|
||||||
cfg, _global_object), daemon=True).start()
|
|
||||||
platform_str += "QQ_OFFICIAL,"
|
|
||||||
|
|
||||||
# 初始化dashboard
|
|
||||||
_global_object.dashboard_data = DashBoardData(
|
|
||||||
stats={},
|
|
||||||
configs={},
|
|
||||||
logs={},
|
|
||||||
plugins=_global_object.cached_plugins,
|
|
||||||
)
|
|
||||||
dashboard_helper = DashBoardHelper(_global_object, config=cc.get_all())
|
|
||||||
dashboard_thread = threading.Thread(
|
|
||||||
target=dashboard_helper.run, daemon=True)
|
|
||||||
dashboard_thread.start()
|
|
||||||
|
|
||||||
# 运行 monitor
|
|
||||||
threading.Thread(target=run_monitor, args=(
|
|
||||||
_global_object,), daemon=True).start()
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"如果有任何问题, 请在 https://github.com/Soulter/AstrBot 上提交 issue 或加群 322154837。")
|
|
||||||
logger.info("请给 https://github.com/Soulter/AstrBot 点个 star。")
|
|
||||||
if platform_str == '':
|
|
||||||
platform_str = "(未启动任何平台,请前往面板添加)"
|
|
||||||
logger.info(f"🎉 项目启动完成")
|
|
||||||
|
|
||||||
dashboard_thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
运行 QQ_OFFICIAL 机器人
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def run_qqchan_bot(cfg: dict, global_object: GlobalObject):
|
|
||||||
try:
|
|
||||||
from model.platform.qq_official import QQOfficial
|
|
||||||
qqchannel_bot = QQOfficial(
|
|
||||||
cfg=cfg, message_handler=oper_msg, global_object=global_object)
|
|
||||||
global_object.platforms.append(RegisteredPlatform(
|
|
||||||
platform_name="qqchan", platform_instance=qqchannel_bot, origin="internal"))
|
|
||||||
qqchannel_bot.run()
|
|
||||||
except BaseException as e:
|
|
||||||
logger.error("启动 QQ 频道机器人时出现错误, 原因如下: " + str(e))
|
|
||||||
logger.error(r"如果您是初次启动,请前往可视化面板填写配置。详情请看:https://astrbot.soulter.top/center/。")
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
运行 QQ_GOCQ 机器人
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def run_gocq_bot(cfg: dict, _global_object: GlobalObject):
|
|
||||||
from model.platform.qq_gocq import QQGOCQ
|
|
||||||
|
|
||||||
noticed = False
|
|
||||||
host = cc.get("gocq_host", "127.0.0.1")
|
|
||||||
port = cc.get("gocq_websocket_port", 6700)
|
|
||||||
http_port = cc.get("gocq_http_port", 5700)
|
|
||||||
logger.info(
|
|
||||||
f"正在检查连接...host: {host}, ws port: {port}, http port: {http_port}")
|
|
||||||
while True:
|
|
||||||
if not gu.port_checker(port=port, host=host) or not gu.port_checker(port=http_port, host=host):
|
|
||||||
if not noticed:
|
|
||||||
noticed = True
|
|
||||||
logger.warning(
|
|
||||||
f"连接到{host}:{port}(或{http_port})失败。程序会每隔 5s 自动重试。")
|
|
||||||
time.sleep(5)
|
|
||||||
else:
|
|
||||||
logger.info("已连接到 gocq。")
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
qq_gocq = QQGOCQ(cfg=cfg, message_handler=oper_msg,
|
|
||||||
global_object=_global_object)
|
|
||||||
_global_object.platforms.append(RegisteredPlatform(
|
|
||||||
platform_name="gocq", platform_instance=qq_gocq, origin="internal"))
|
|
||||||
qq_gocq.run()
|
|
||||||
except BaseException as e:
|
|
||||||
input("启动QQ机器人出现错误"+str(e))
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
检查发言频率
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def check_frequency(id) -> bool:
|
|
||||||
ts = int(time.time())
|
|
||||||
if id in user_frequency:
|
|
||||||
if ts-user_frequency[id]['time'] > frequency_time:
|
|
||||||
user_frequency[id]['time'] = ts
|
|
||||||
user_frequency[id]['count'] = 1
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
if user_frequency[id]['count'] >= frequency_count:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
user_frequency[id]['count'] += 1
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
t = {'time': ts, 'count': 1}
|
|
||||||
user_frequency[id] = t
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def record_message(platform: str, session_id: str):
|
|
||||||
# TODO: 这里会非常吃资源。然而 sqlite3 不支持多线程,所以暂时这样写。
|
|
||||||
curr_ts = int(time.time())
|
|
||||||
db_inst = dbConn()
|
|
||||||
db_inst.increment_stat_session(platform, session_id, 1)
|
|
||||||
db_inst.increment_stat_message(curr_ts, 1)
|
|
||||||
db_inst.increment_stat_platform(curr_ts, platform, 1)
|
|
||||||
_global_object.cnt_total += 1
|
|
||||||
|
|
||||||
|
|
||||||
async def oper_msg(message: AstrBotMessage,
|
|
||||||
session_id: str,
|
|
||||||
role: str = 'member',
|
|
||||||
platform: str = None,
|
|
||||||
) -> MessageResult:
|
|
||||||
"""
|
|
||||||
处理消息。
|
|
||||||
message: 消息对象
|
|
||||||
session_id: 该消息源的唯一识别号
|
|
||||||
role: member | admin
|
|
||||||
platform: str 所注册的平台的名称。如果没有注册,将抛出一个异常。
|
|
||||||
"""
|
|
||||||
global chosen_provider, _global_object
|
|
||||||
message_str = ''
|
|
||||||
session_id = session_id
|
|
||||||
role = role
|
|
||||||
hit = False # 是否命中指令
|
|
||||||
command_result = () # 调用指令返回的结果
|
|
||||||
|
|
||||||
# 获取平台实例
|
|
||||||
reg_platform: RegisteredPlatform = None
|
|
||||||
for p in _global_object.platforms:
|
|
||||||
if p.platform_name == platform:
|
|
||||||
reg_platform = p
|
|
||||||
break
|
|
||||||
if not reg_platform:
|
|
||||||
raise Exception(f"未找到平台 {platform} 的实例。")
|
|
||||||
|
|
||||||
# 统计数据,如频道消息量
|
|
||||||
await record_message(platform, session_id)
|
|
||||||
|
|
||||||
for i in message.message:
|
|
||||||
if isinstance(i, Plain):
|
|
||||||
message_str += i.text.strip()
|
|
||||||
if message_str == "":
|
|
||||||
return MessageResult("Hi~")
|
|
||||||
|
|
||||||
# 检查发言频率
|
|
||||||
if not check_frequency(message.sender.user_id):
|
|
||||||
return MessageResult(f'你的发言超过频率限制(╯▔皿▔)╯。\n管理员设置{frequency_time}秒内只能提问{frequency_count}次。')
|
|
||||||
|
|
||||||
# 检查是否是更换语言模型的请求
|
|
||||||
temp_switch = ""
|
|
||||||
if message_str.startswith('/gpt'):
|
|
||||||
target = chosen_provider
|
|
||||||
if message_str.startswith('/gpt'):
|
|
||||||
target = OPENAI_OFFICIAL
|
|
||||||
l = message_str.split(' ')
|
|
||||||
if len(l) > 1 and l[1] != "":
|
|
||||||
# 临时对话模式,先记录下之前的语言模型,回答完毕后再切回
|
|
||||||
temp_switch = chosen_provider
|
|
||||||
chosen_provider = target
|
|
||||||
message_str = l[1]
|
|
||||||
else:
|
|
||||||
chosen_provider = target
|
|
||||||
cc.put("chosen_provider", chosen_provider)
|
|
||||||
return MessageResult(f"已切换至【{chosen_provider}】")
|
|
||||||
|
|
||||||
llm_result_str = ""
|
|
||||||
|
|
||||||
# check commands and plugins
|
|
||||||
message_str_no_wake_prefix = message_str
|
|
||||||
for wake_prefix in _global_object.nick: # nick: tuple
|
|
||||||
if message_str.startswith(wake_prefix):
|
|
||||||
message_str_no_wake_prefix = message_str.removeprefix(wake_prefix)
|
|
||||||
break
|
|
||||||
hit, command_result = await llm_command_instance[chosen_provider].check_command(
|
|
||||||
message_str_no_wake_prefix,
|
|
||||||
session_id,
|
|
||||||
role,
|
|
||||||
reg_platform,
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 没触发指令
|
|
||||||
if not hit:
|
|
||||||
# 关键词拦截
|
|
||||||
for i in uw.unfit_words_q:
|
|
||||||
matches = re.match(i, message_str.strip(), re.I | re.M)
|
|
||||||
if matches:
|
|
||||||
return MessageResult(f"你的提问得到的回复未通过【默认关键词拦截】服务, 不予回复。")
|
|
||||||
if baidu_judge != None:
|
|
||||||
check, msg = await asyncio.to_thread(baidu_judge.judge, message_str)
|
|
||||||
if not check:
|
|
||||||
return MessageResult(f"你的提问得到的回复未通过【百度AI内容审核】服务, 不予回复。\n\n{msg}")
|
|
||||||
if chosen_provider == NONE_LLM:
|
|
||||||
logger.info("一条消息由于 Bot 未启动任何语言模型并且未触发指令而将被忽略。")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if llm_wake_prefix != "" and not message_str.startswith(llm_wake_prefix):
|
|
||||||
return
|
|
||||||
# check image url
|
|
||||||
image_url = None
|
|
||||||
for comp in message.message:
|
|
||||||
if isinstance(comp, Image):
|
|
||||||
if comp.url is None:
|
|
||||||
image_url = comp.file
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
image_url = comp.url
|
|
||||||
break
|
|
||||||
# web search keyword
|
|
||||||
web_sch_flag = False
|
|
||||||
if message_str.startswith("ws ") and message_str != "ws ":
|
|
||||||
message_str = message_str[3:]
|
|
||||||
web_sch_flag = True
|
|
||||||
else:
|
|
||||||
message_str += " " + cc.get("llm_env_prompt", "")
|
|
||||||
if chosen_provider == OPENAI_OFFICIAL:
|
|
||||||
if _global_object.web_search or web_sch_flag:
|
|
||||||
official_fc = chosen_provider == OPENAI_OFFICIAL
|
|
||||||
llm_result_str = await web_searcher.web_search(message_str, llm_instance[chosen_provider], session_id, official_fc)
|
|
||||||
else:
|
|
||||||
llm_result_str = await llm_instance[chosen_provider].text_chat(message_str, session_id, image_url)
|
|
||||||
|
|
||||||
llm_result_str = _global_object.reply_prefix + llm_result_str
|
|
||||||
except BaseException as e:
|
|
||||||
logger.error(f"调用异常:{traceback.format_exc()}")
|
|
||||||
return MessageResult(f"调用异常。详细原因:{str(e)}")
|
|
||||||
|
|
||||||
# 切换回原来的语言模型
|
|
||||||
if temp_switch != "":
|
|
||||||
chosen_provider = temp_switch
|
|
||||||
|
|
||||||
if hit:
|
|
||||||
# 有指令或者插件触发
|
|
||||||
# command_result 是一个元组:(指令调用是否成功, 指令返回的文本结果, 指令类型)
|
|
||||||
if command_result == None:
|
|
||||||
return
|
|
||||||
command = command_result[2]
|
|
||||||
|
|
||||||
if command == "update latest r":
|
|
||||||
def update_restart():
|
|
||||||
py = sys.executable
|
|
||||||
os.execl(py, py, *sys.argv)
|
|
||||||
return MessageResult(command_result[1] + "\n\n即将自动重启。", callback=update_restart)
|
|
||||||
|
|
||||||
if not command_result[0]:
|
|
||||||
return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
|
|
||||||
|
|
||||||
# 画图指令
|
|
||||||
if command == 'draw':
|
|
||||||
# 保存到本地
|
|
||||||
path = await gu.download_image_by_url(command_result[1])
|
|
||||||
return MessageResult([Image.fromFileSystem(path)])
|
|
||||||
# 其他指令
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return MessageResult(command_result[1])
|
|
||||||
except BaseException as e:
|
|
||||||
return MessageResult(f"回复消息出错: {str(e)}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 敏感过滤
|
|
||||||
# 过滤不合适的词
|
|
||||||
for i in uw.unfit_words:
|
|
||||||
llm_result_str = re.sub(i, "***", llm_result_str)
|
|
||||||
# 百度内容审核服务二次审核
|
|
||||||
if baidu_judge != None:
|
|
||||||
check, msg = await asyncio.to_thread(baidu_judge.judge, llm_result_str)
|
|
||||||
if not check:
|
|
||||||
return MessageResult(f"你的提问得到的回复【百度内容审核】未通过,不予回复。\n\n{msg}")
|
|
||||||
# 发送信息
|
|
||||||
try:
|
|
||||||
return MessageResult(llm_result_str)
|
|
||||||
except BaseException as e:
|
|
||||||
logger.info("回复消息错误: \n"+str(e))
|
|
||||||
29
astrbot/core/__init__.py
Normal file
29
astrbot/core/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
from .log import LogManager, LogBroker # noqa
|
||||||
|
from astrbot.core.utils.t2i.renderer import HtmlRenderer
|
||||||
|
from astrbot.core.utils.shared_preferences import SharedPreferences
|
||||||
|
from astrbot.core.utils.pip_installer import PipInstaller
|
||||||
|
from astrbot.core.db.sqlite import SQLiteDatabase
|
||||||
|
from astrbot.core.config.default import DB_PATH
|
||||||
|
from astrbot.core.config import AstrBotConfig
|
||||||
|
from astrbot.core.file_token_service import FileTokenService
|
||||||
|
from .utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
|
# 初始化数据存储文件夹
|
||||||
|
os.makedirs(get_astrbot_data_path(), exist_ok=True)
|
||||||
|
|
||||||
|
DEMO_MODE = os.getenv("DEMO_MODE", False)
|
||||||
|
|
||||||
|
astrbot_config = AstrBotConfig()
|
||||||
|
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
|
||||||
|
html_renderer = HtmlRenderer(t2i_base_url)
|
||||||
|
logger = LogManager.GetLogger(log_name="astrbot")
|
||||||
|
db_helper = SQLiteDatabase(DB_PATH)
|
||||||
|
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中
|
||||||
|
sp = SharedPreferences(db_helper=db_helper)
|
||||||
|
# 文件令牌服务
|
||||||
|
file_token_service = FileTokenService()
|
||||||
|
pip_installer = PipInstaller(
|
||||||
|
astrbot_config.get("pip_install_arg", ""),
|
||||||
|
astrbot_config.get("pypi_index_url", None),
|
||||||
|
)
|
||||||
13
astrbot/core/agent/agent.py
Normal file
13
astrbot/core/agent/agent.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from .tool import FunctionTool
|
||||||
|
from typing import Generic
|
||||||
|
from .run_context import TContext
|
||||||
|
from .hooks import BaseAgentRunHooks
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Agent(Generic[TContext]):
|
||||||
|
name: str
|
||||||
|
instructions: str | None = None
|
||||||
|
tools: list[str, FunctionTool] | None = None
|
||||||
|
run_hooks: BaseAgentRunHooks[TContext] | None = None
|
||||||
34
astrbot/core/agent/handoff.py
Normal file
34
astrbot/core/agent/handoff.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import Generic
|
||||||
|
from .tool import FunctionTool
|
||||||
|
from .agent import Agent
|
||||||
|
from .run_context import TContext
|
||||||
|
|
||||||
|
|
||||||
|
class HandoffTool(FunctionTool, Generic[TContext]):
|
||||||
|
"""Handoff tool for delegating tasks to another agent."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, agent: Agent[TContext], parameters: dict | None = None, **kwargs
|
||||||
|
):
|
||||||
|
self.agent = agent
|
||||||
|
super().__init__(
|
||||||
|
name=f"transfer_to_{agent.name}",
|
||||||
|
parameters=parameters or self.default_parameters(),
|
||||||
|
description=agent.instructions or self.default_description(agent.name),
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def default_parameters(self) -> dict:
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"input": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The input to be handed off to another agent. This should be a clear and concise request or task.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def default_description(self, agent_name: str | None) -> str:
|
||||||
|
agent_name = agent_name or "another"
|
||||||
|
return f"Delegate tasks to {self.name} agent to handle the request."
|
||||||
27
astrbot/core/agent/hooks.py
Normal file
27
astrbot/core/agent/hooks.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import mcp
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from .run_context import ContextWrapper, TContext
|
||||||
|
from typing import Generic
|
||||||
|
from astrbot.core.provider.entities import LLMResponse
|
||||||
|
from astrbot.core.agent.tool import FunctionTool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseAgentRunHooks(Generic[TContext]):
|
||||||
|
async def on_agent_begin(self, run_context: ContextWrapper[TContext]): ...
|
||||||
|
async def on_tool_start(
|
||||||
|
self,
|
||||||
|
run_context: ContextWrapper[TContext],
|
||||||
|
tool: FunctionTool,
|
||||||
|
tool_args: dict | None,
|
||||||
|
): ...
|
||||||
|
async def on_tool_end(
|
||||||
|
self,
|
||||||
|
run_context: ContextWrapper[TContext],
|
||||||
|
tool: FunctionTool,
|
||||||
|
tool_args: dict | None,
|
||||||
|
tool_result: mcp.types.CallToolResult | None,
|
||||||
|
): ...
|
||||||
|
async def on_agent_done(
|
||||||
|
self, run_context: ContextWrapper[TContext], llm_response: LLMResponse
|
||||||
|
): ...
|
||||||
208
astrbot/core/agent/mcp_client.py
Normal file
208
astrbot/core/agent/mcp_client.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from contextlib import AsyncExitStack
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core.utils.log_pipe import LogPipe
|
||||||
|
|
||||||
|
try:
|
||||||
|
import mcp
|
||||||
|
from mcp.client.sse import sse_client
|
||||||
|
except (ModuleNotFoundError, ImportError):
|
||||||
|
logger.warning("警告: 缺少依赖库 'mcp',将无法使用 MCP 服务。")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
except (ModuleNotFoundError, ImportError):
|
||||||
|
logger.warning(
|
||||||
|
"警告: 缺少依赖库 'mcp' 或者 mcp 库版本过低,无法使用 Streamable HTTP 连接方式。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_config(config: dict) -> dict:
|
||||||
|
"""准备配置,处理嵌套格式"""
|
||||||
|
if "mcpServers" in config and config["mcpServers"]:
|
||||||
|
first_key = next(iter(config["mcpServers"]))
|
||||||
|
config = config["mcpServers"][first_key]
|
||||||
|
config.pop("active", None)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
||||||
|
"""快速测试 MCP 服务器可达性"""
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
cfg = _prepare_config(config.copy())
|
||||||
|
|
||||||
|
url = cfg["url"]
|
||||||
|
headers = cfg.get("headers", {})
|
||||||
|
timeout = cfg.get("timeout", 10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
if cfg.get("transport") == "streamable_http":
|
||||||
|
test_payload = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "initialize",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {"name": "test-client", "version": "1.2.3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
**headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json, text/event-stream",
|
||||||
|
},
|
||||||
|
json=test_payload,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
return False, f"HTTP {response.status}: {response.reason}"
|
||||||
|
else:
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
**headers,
|
||||||
|
"Accept": "application/json, text/event-stream",
|
||||||
|
},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
return False, f"HTTP {response.status}: {response.reason}"
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return False, f"连接超时: {timeout}秒"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"{e!s}"
|
||||||
|
|
||||||
|
|
||||||
|
class MCPClient:
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize session and client objects
|
||||||
|
self.session: Optional[mcp.ClientSession] = None
|
||||||
|
self.exit_stack = AsyncExitStack()
|
||||||
|
|
||||||
|
self.name = None
|
||||||
|
self.active: bool = True
|
||||||
|
self.tools: list[mcp.Tool] = []
|
||||||
|
self.server_errlogs: list[str] = []
|
||||||
|
self.running_event = asyncio.Event()
|
||||||
|
|
||||||
|
async def connect_to_server(self, mcp_server_config: dict, name: str):
|
||||||
|
"""连接到 MCP 服务器
|
||||||
|
|
||||||
|
如果 `url` 参数存在:
|
||||||
|
1. 当 transport 指定为 `streamable_http` 时,使用 Streamable HTTP 连接方式。
|
||||||
|
1. 当 transport 指定为 `sse` 时,使用 SSE 连接方式。
|
||||||
|
2. 如果没有指定,默认使用 SSE 的方式连接到 MCP 服务。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mcp_server_config (dict): Configuration for the MCP server. See https://modelcontextprotocol.io/quickstart/server
|
||||||
|
"""
|
||||||
|
cfg = _prepare_config(mcp_server_config.copy())
|
||||||
|
|
||||||
|
def logging_callback(msg: str):
|
||||||
|
# 处理 MCP 服务的错误日志
|
||||||
|
print(f"MCP Server {name} Error: {msg}")
|
||||||
|
self.server_errlogs.append(msg)
|
||||||
|
|
||||||
|
if "url" in cfg:
|
||||||
|
success, error_msg = await _quick_test_mcp_connection(cfg)
|
||||||
|
if not success:
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
if cfg.get("transport") != "streamable_http":
|
||||||
|
# SSE transport method
|
||||||
|
self._streams_context = sse_client(
|
||||||
|
url=cfg["url"],
|
||||||
|
headers=cfg.get("headers", {}),
|
||||||
|
timeout=cfg.get("timeout", 5),
|
||||||
|
sse_read_timeout=cfg.get("sse_read_timeout", 60 * 5),
|
||||||
|
)
|
||||||
|
streams = await self.exit_stack.enter_async_context(
|
||||||
|
self._streams_context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new client session
|
||||||
|
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
|
||||||
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
|
mcp.ClientSession(
|
||||||
|
*streams,
|
||||||
|
read_timeout_seconds=read_timeout,
|
||||||
|
logging_callback=logging_callback, # type: ignore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
timeout = timedelta(seconds=cfg.get("timeout", 30))
|
||||||
|
sse_read_timeout = timedelta(
|
||||||
|
seconds=cfg.get("sse_read_timeout", 60 * 5)
|
||||||
|
)
|
||||||
|
self._streams_context = streamablehttp_client(
|
||||||
|
url=cfg["url"],
|
||||||
|
headers=cfg.get("headers", {}),
|
||||||
|
timeout=timeout,
|
||||||
|
sse_read_timeout=sse_read_timeout,
|
||||||
|
terminate_on_close=cfg.get("terminate_on_close", True),
|
||||||
|
)
|
||||||
|
read_s, write_s, _ = await self.exit_stack.enter_async_context(
|
||||||
|
self._streams_context
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new client session
|
||||||
|
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 20))
|
||||||
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
|
mcp.ClientSession(
|
||||||
|
read_stream=read_s,
|
||||||
|
write_stream=write_s,
|
||||||
|
read_timeout_seconds=read_timeout,
|
||||||
|
logging_callback=logging_callback, # type: ignore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
server_params = mcp.StdioServerParameters(
|
||||||
|
**cfg,
|
||||||
|
)
|
||||||
|
|
||||||
|
def callback(msg: str):
|
||||||
|
# 处理 MCP 服务的错误日志
|
||||||
|
self.server_errlogs.append(msg)
|
||||||
|
|
||||||
|
stdio_transport = await self.exit_stack.enter_async_context(
|
||||||
|
mcp.stdio_client(
|
||||||
|
server_params,
|
||||||
|
errlog=LogPipe(
|
||||||
|
level=logging.ERROR,
|
||||||
|
logger=logger,
|
||||||
|
identifier=f"MCPServer-{name}",
|
||||||
|
callback=callback,
|
||||||
|
), # type: ignore
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new client session
|
||||||
|
self.session = await self.exit_stack.enter_async_context(
|
||||||
|
mcp.ClientSession(*stdio_transport)
|
||||||
|
)
|
||||||
|
await self.session.initialize()
|
||||||
|
|
||||||
|
async def list_tools_and_save(self) -> mcp.ListToolsResult:
|
||||||
|
"""List all tools from the server and save them to self.tools"""
|
||||||
|
response = await self.session.list_tools()
|
||||||
|
self.tools = response.tools
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
"""Clean up resources"""
|
||||||
|
await self.exit_stack.aclose()
|
||||||
|
self.running_event.set() # Set the running event to indicate cleanup is done
|
||||||
12
astrbot/core/agent/response.py
Normal file
12
astrbot/core/agent/response.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
import typing as T
|
||||||
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
|
|
||||||
|
class AgentResponseData(T.TypedDict):
|
||||||
|
chain: MessageChain
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentResponse:
|
||||||
|
type: str
|
||||||
|
data: AgentResponseData
|
||||||
17
astrbot/core/agent/run_context.py
Normal file
17
astrbot/core/agent/run_context.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Generic
|
||||||
|
from typing_extensions import TypeVar
|
||||||
|
|
||||||
|
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||||
|
|
||||||
|
TContext = TypeVar("TContext", default=Any)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContextWrapper(Generic[TContext]):
|
||||||
|
"""A context for running an agent, which can be used to pass additional data or state."""
|
||||||
|
|
||||||
|
context: TContext
|
||||||
|
event: AstrMessageEvent
|
||||||
|
|
||||||
|
NoContext = ContextWrapper[None]
|
||||||
3
astrbot/core/agent/runners/__init__.py
Normal file
3
astrbot/core/agent/runners/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .base import BaseAgentRunner
|
||||||
|
|
||||||
|
__all__ = ["BaseAgentRunner"]
|
||||||
58
astrbot/core/agent/runners/base.py
Normal file
58
astrbot/core/agent/runners/base.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import abc
|
||||||
|
import typing as T
|
||||||
|
from enum import Enum, auto
|
||||||
|
from ..run_context import ContextWrapper, TContext
|
||||||
|
from ..response import AgentResponse
|
||||||
|
from ..hooks import BaseAgentRunHooks
|
||||||
|
from ..tool_executor import BaseFunctionToolExecutor
|
||||||
|
from astrbot.core.provider import Provider
|
||||||
|
from astrbot.core.provider.entities import LLMResponse
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState(Enum):
|
||||||
|
"""Defines the state of the agent."""
|
||||||
|
|
||||||
|
IDLE = auto() # Initial state
|
||||||
|
RUNNING = auto() # Currently processing
|
||||||
|
DONE = auto() # Completed
|
||||||
|
ERROR = auto() # Error state
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgentRunner(T.Generic[TContext]):
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def reset(
|
||||||
|
self,
|
||||||
|
provider: Provider,
|
||||||
|
run_context: ContextWrapper[TContext],
|
||||||
|
tool_executor: BaseFunctionToolExecutor[TContext],
|
||||||
|
agent_hooks: BaseAgentRunHooks[TContext],
|
||||||
|
**kwargs: T.Any,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Reset the agent to its initial state.
|
||||||
|
This method should be called before starting a new run.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def step(self) -> T.AsyncGenerator[AgentResponse, None]:
|
||||||
|
"""
|
||||||
|
Process a single step of the agent.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def done(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the agent has completed its task.
|
||||||
|
Returns True if the agent is done, False otherwise.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||||
|
"""
|
||||||
|
Get the final observation from the agent.
|
||||||
|
This method should be called after the agent is done.
|
||||||
|
"""
|
||||||
|
...
|
||||||
334
astrbot/core/agent/runners/tool_loop_agent_runner.py
Normal file
334
astrbot/core/agent/runners/tool_loop_agent_runner.py
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import typing as T
|
||||||
|
from .base import BaseAgentRunner, AgentResponse, AgentState
|
||||||
|
from ..hooks import BaseAgentRunHooks
|
||||||
|
from ..tool_executor import BaseFunctionToolExecutor
|
||||||
|
from ..run_context import ContextWrapper, TContext
|
||||||
|
from ..response import AgentResponseData
|
||||||
|
from astrbot.core.provider.provider import Provider
|
||||||
|
from astrbot.core.message.message_event_result import (
|
||||||
|
MessageChain,
|
||||||
|
)
|
||||||
|
from astrbot.core.provider.entities import (
|
||||||
|
ProviderRequest,
|
||||||
|
LLMResponse,
|
||||||
|
ToolCallMessageSegment,
|
||||||
|
AssistantMessageSegment,
|
||||||
|
ToolCallsResult,
|
||||||
|
)
|
||||||
|
from mcp.types import (
|
||||||
|
TextContent,
|
||||||
|
ImageContent,
|
||||||
|
EmbeddedResource,
|
||||||
|
TextResourceContents,
|
||||||
|
BlobResourceContents,
|
||||||
|
CallToolResult,
|
||||||
|
)
|
||||||
|
from astrbot import logger
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
|
from typing import override
|
||||||
|
else:
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
|
||||||
|
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||||
|
@override
|
||||||
|
async def reset(
|
||||||
|
self,
|
||||||
|
provider: Provider,
|
||||||
|
request: ProviderRequest,
|
||||||
|
run_context: ContextWrapper[TContext],
|
||||||
|
tool_executor: BaseFunctionToolExecutor[TContext],
|
||||||
|
agent_hooks: BaseAgentRunHooks[TContext],
|
||||||
|
**kwargs: T.Any,
|
||||||
|
) -> None:
|
||||||
|
self.req = request
|
||||||
|
self.streaming = kwargs.get("streaming", False)
|
||||||
|
self.provider = provider
|
||||||
|
self.final_llm_resp = None
|
||||||
|
self._state = AgentState.IDLE
|
||||||
|
self.tool_executor = tool_executor
|
||||||
|
self.agent_hooks = agent_hooks
|
||||||
|
self.run_context = run_context
|
||||||
|
|
||||||
|
def _transition_state(self, new_state: AgentState) -> None:
|
||||||
|
"""转换 Agent 状态"""
|
||||||
|
if self._state != new_state:
|
||||||
|
logger.debug(f"Agent state transition: {self._state} -> {new_state}")
|
||||||
|
self._state = new_state
|
||||||
|
|
||||||
|
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
||||||
|
"""Yields chunks *and* a final LLMResponse."""
|
||||||
|
if self.streaming:
|
||||||
|
stream = self.provider.text_chat_stream(**self.req.__dict__)
|
||||||
|
async for resp in stream: # type: ignore
|
||||||
|
yield resp
|
||||||
|
else:
|
||||||
|
yield await self.provider.text_chat(**self.req.__dict__)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def step(self):
|
||||||
|
"""
|
||||||
|
Process a single step of the agent.
|
||||||
|
This method should return the result of the step.
|
||||||
|
"""
|
||||||
|
if not self.req:
|
||||||
|
raise ValueError("Request is not set. Please call reset() first.")
|
||||||
|
|
||||||
|
if self._state == AgentState.IDLE:
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_agent_begin(self.run_context)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in on_agent_begin hook: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# 开始处理,转换到运行状态
|
||||||
|
self._transition_state(AgentState.RUNNING)
|
||||||
|
llm_resp_result = None
|
||||||
|
|
||||||
|
async for llm_response in self._iter_llm_responses():
|
||||||
|
assert isinstance(llm_response, LLMResponse)
|
||||||
|
if llm_response.is_chunk:
|
||||||
|
if llm_response.result_chain:
|
||||||
|
yield AgentResponse(
|
||||||
|
type="streaming_delta",
|
||||||
|
data=AgentResponseData(chain=llm_response.result_chain),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield AgentResponse(
|
||||||
|
type="streaming_delta",
|
||||||
|
data=AgentResponseData(
|
||||||
|
chain=MessageChain().message(llm_response.completion_text)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
llm_resp_result = llm_response
|
||||||
|
break # got final response
|
||||||
|
|
||||||
|
if not llm_resp_result:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 处理 LLM 响应
|
||||||
|
llm_resp = llm_resp_result
|
||||||
|
|
||||||
|
if llm_resp.role == "err":
|
||||||
|
# 如果 LLM 响应错误,转换到错误状态
|
||||||
|
self.final_llm_resp = llm_resp
|
||||||
|
self._transition_state(AgentState.ERROR)
|
||||||
|
yield AgentResponse(
|
||||||
|
type="err",
|
||||||
|
data=AgentResponseData(
|
||||||
|
chain=MessageChain().message(
|
||||||
|
f"LLM 响应错误: {llm_resp.completion_text or '未知错误'}"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not llm_resp.tools_call_name:
|
||||||
|
# 如果没有工具调用,转换到完成状态
|
||||||
|
self.final_llm_resp = llm_resp
|
||||||
|
self._transition_state(AgentState.DONE)
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# 返回 LLM 结果
|
||||||
|
if llm_resp.result_chain:
|
||||||
|
yield AgentResponse(
|
||||||
|
type="llm_result",
|
||||||
|
data=AgentResponseData(chain=llm_resp.result_chain),
|
||||||
|
)
|
||||||
|
elif llm_resp.completion_text:
|
||||||
|
yield AgentResponse(
|
||||||
|
type="llm_result",
|
||||||
|
data=AgentResponseData(
|
||||||
|
chain=MessageChain().message(llm_resp.completion_text)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果有工具调用,还需处理工具调用
|
||||||
|
if llm_resp.tools_call_name:
|
||||||
|
tool_call_result_blocks = []
|
||||||
|
for tool_call_name in llm_resp.tools_call_name:
|
||||||
|
yield AgentResponse(
|
||||||
|
type="tool_call",
|
||||||
|
data=AgentResponseData(
|
||||||
|
chain=MessageChain().message(f"🔨 调用工具: {tool_call_name}")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async for result in self._handle_function_tools(self.req, llm_resp):
|
||||||
|
if isinstance(result, list):
|
||||||
|
tool_call_result_blocks = result
|
||||||
|
elif isinstance(result, MessageChain):
|
||||||
|
yield AgentResponse(
|
||||||
|
type="tool_call_result",
|
||||||
|
data=AgentResponseData(chain=result),
|
||||||
|
)
|
||||||
|
# 将结果添加到上下文中
|
||||||
|
tool_calls_result = ToolCallsResult(
|
||||||
|
tool_calls_info=AssistantMessageSegment(
|
||||||
|
role="assistant",
|
||||||
|
tool_calls=llm_resp.to_openai_tool_calls(),
|
||||||
|
content=llm_resp.completion_text,
|
||||||
|
),
|
||||||
|
tool_calls_result=tool_call_result_blocks,
|
||||||
|
)
|
||||||
|
self.req.append_tool_calls_result(tool_calls_result)
|
||||||
|
|
||||||
|
async def _handle_function_tools(
|
||||||
|
self,
|
||||||
|
req: ProviderRequest,
|
||||||
|
llm_response: LLMResponse,
|
||||||
|
) -> T.AsyncGenerator[MessageChain | list[ToolCallMessageSegment], None]:
|
||||||
|
"""处理函数工具调用。"""
|
||||||
|
tool_call_result_blocks: list[ToolCallMessageSegment] = []
|
||||||
|
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
|
||||||
|
|
||||||
|
# 执行函数调用
|
||||||
|
for func_tool_name, func_tool_args, func_tool_id in zip(
|
||||||
|
llm_response.tools_call_name,
|
||||||
|
llm_response.tools_call_args,
|
||||||
|
llm_response.tools_call_ids,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if not req.func_tool:
|
||||||
|
return
|
||||||
|
func_tool = req.func_tool.get_func(func_tool_name)
|
||||||
|
logger.info(f"使用工具:{func_tool_name},参数:{func_tool_args}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_tool_start(
|
||||||
|
self.run_context, func_tool, func_tool_args
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in on_tool_start hook: {e}", exc_info=True)
|
||||||
|
|
||||||
|
executor = self.tool_executor.execute(
|
||||||
|
tool=func_tool,
|
||||||
|
run_context=self.run_context,
|
||||||
|
**func_tool_args,
|
||||||
|
)
|
||||||
|
async for resp in executor:
|
||||||
|
if isinstance(resp, CallToolResult):
|
||||||
|
res = resp
|
||||||
|
if isinstance(res.content[0], TextContent):
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content=res.content[0].text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield MessageChain().message(res.content[0].text)
|
||||||
|
elif isinstance(res.content[0], ImageContent):
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content="返回了图片(已直接发送给用户)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield MessageChain(type="tool_direct_result").base64_image(
|
||||||
|
res.content[0].data
|
||||||
|
)
|
||||||
|
elif isinstance(res.content[0], EmbeddedResource):
|
||||||
|
resource = res.content[0].resource
|
||||||
|
if isinstance(resource, TextResourceContents):
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content=resource.text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield MessageChain().message(resource.text)
|
||||||
|
elif (
|
||||||
|
isinstance(resource, BlobResourceContents)
|
||||||
|
and resource.mimeType
|
||||||
|
and resource.mimeType.startswith("image/")
|
||||||
|
):
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content="返回了图片(已直接发送给用户)",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield MessageChain(
|
||||||
|
type="tool_direct_result"
|
||||||
|
).base64_image(res.content[0].data)
|
||||||
|
else:
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content="返回的数据类型不受支持",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield MessageChain().message("返回的数据类型不受支持。")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_tool_end(
|
||||||
|
self.run_context,
|
||||||
|
func_tool_name,
|
||||||
|
func_tool_args,
|
||||||
|
resp,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error in on_tool_end hook: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
elif resp is None:
|
||||||
|
# Tool 直接请求发送消息给用户
|
||||||
|
# 这里我们将直接结束 Agent Loop。
|
||||||
|
self._transition_state(AgentState.DONE)
|
||||||
|
if res := self.run_context.event.get_result():
|
||||||
|
if res.chain:
|
||||||
|
yield MessageChain(
|
||||||
|
chain=res.chain, type="tool_direct_result"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_tool_end(
|
||||||
|
self.run_context, func_tool_name, func_tool_args, None
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error in on_tool_end hook: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Tool 返回了不支持的类型: {type(resp)},将忽略。"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_tool_end(
|
||||||
|
self.run_context, func_tool_name, func_tool_args, None
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error in on_tool_end hook: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.run_context.event.clear_result()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(traceback.format_exc())
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content=f"error: {str(e)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理函数调用响应
|
||||||
|
if tool_call_result_blocks:
|
||||||
|
yield tool_call_result_blocks
|
||||||
|
|
||||||
|
def done(self) -> bool:
|
||||||
|
"""检查 Agent 是否已完成工作"""
|
||||||
|
return self._state in (AgentState.DONE, AgentState.ERROR)
|
||||||
|
|
||||||
|
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||||
|
return self.final_llm_resp
|
||||||
256
astrbot/core/agent/tool.py
Normal file
256
astrbot/core/agent/tool.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from deprecated import deprecated
|
||||||
|
from typing import Awaitable, Literal, Any, Optional
|
||||||
|
from .mcp_client import MCPClient
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FunctionTool:
|
||||||
|
"""A class representing a function tool that can be used in function calling."""
|
||||||
|
|
||||||
|
name: str | None = None
|
||||||
|
parameters: dict | None = None
|
||||||
|
description: str | None = None
|
||||||
|
handler: Awaitable | None = None
|
||||||
|
"""处理函数, 当 origin 为 mcp 时,这个为空"""
|
||||||
|
handler_module_path: str | None = None
|
||||||
|
"""处理函数的模块路径,当 origin 为 mcp 时,这个为空
|
||||||
|
|
||||||
|
必须要保留这个字段, handler 在初始化会被 functools.partial 包装,导致 handler 的 __module__ 为 functools
|
||||||
|
"""
|
||||||
|
active: bool = True
|
||||||
|
"""是否激活"""
|
||||||
|
|
||||||
|
origin: Literal["local", "mcp"] = "local"
|
||||||
|
"""函数工具的来源, local 为本地函数工具, mcp 为 MCP 服务"""
|
||||||
|
|
||||||
|
# MCP 相关字段
|
||||||
|
mcp_server_name: str | None = None
|
||||||
|
"""MCP 服务名称,当 origin 为 mcp 时有效"""
|
||||||
|
mcp_client: MCPClient | None = None
|
||||||
|
"""MCP 客户端,当 origin 为 mcp 时有效"""
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description}, active={self.active}, origin={self.origin})"
|
||||||
|
|
||||||
|
def __dict__(self) -> dict[str, Any]:
|
||||||
|
"""将 FunctionTool 转换为字典格式"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"parameters": self.parameters,
|
||||||
|
"description": self.description,
|
||||||
|
"active": self.active,
|
||||||
|
"origin": self.origin,
|
||||||
|
"mcp_server_name": self.mcp_server_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ToolSet:
|
||||||
|
"""A set of function tools that can be used in function calling.
|
||||||
|
|
||||||
|
This class provides methods to add, remove, and retrieve tools, as well as
|
||||||
|
convert the tools to different API formats (OpenAI, Anthropic, Google GenAI)."""
|
||||||
|
|
||||||
|
def __init__(self, tools: list[FunctionTool] = None):
|
||||||
|
self.tools: list[FunctionTool] = tools or []
|
||||||
|
|
||||||
|
def empty(self) -> bool:
|
||||||
|
"""Check if the tool set is empty."""
|
||||||
|
return len(self.tools) == 0
|
||||||
|
|
||||||
|
def add_tool(self, tool: FunctionTool):
|
||||||
|
"""Add a tool to the set."""
|
||||||
|
# 检查是否已存在同名工具
|
||||||
|
for i, existing_tool in enumerate(self.tools):
|
||||||
|
if existing_tool.name == tool.name:
|
||||||
|
self.tools[i] = tool
|
||||||
|
return
|
||||||
|
self.tools.append(tool)
|
||||||
|
|
||||||
|
def remove_tool(self, name: str):
|
||||||
|
"""Remove a tool by its name."""
|
||||||
|
self.tools = [tool for tool in self.tools if tool.name != name]
|
||||||
|
|
||||||
|
def get_tool(self, name: str) -> Optional[FunctionTool]:
|
||||||
|
"""Get a tool by its name."""
|
||||||
|
for tool in self.tools:
|
||||||
|
if tool.name == name:
|
||||||
|
return tool
|
||||||
|
return None
|
||||||
|
|
||||||
|
@deprecated(reason="Use add_tool() instead", version="4.0.0")
|
||||||
|
def add_func(self, name: str, func_args: list, desc: str, handler: Awaitable):
|
||||||
|
"""Add a function tool to the set."""
|
||||||
|
params = {
|
||||||
|
"type": "object", # hard-coded here
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
for param in func_args:
|
||||||
|
params["properties"][param["name"]] = {
|
||||||
|
"type": param["type"],
|
||||||
|
"description": param["description"],
|
||||||
|
}
|
||||||
|
_func = FunctionTool(
|
||||||
|
name=name,
|
||||||
|
parameters=params,
|
||||||
|
description=desc,
|
||||||
|
handler=handler,
|
||||||
|
)
|
||||||
|
self.add_tool(_func)
|
||||||
|
|
||||||
|
@deprecated(reason="Use remove_tool() instead", version="4.0.0")
|
||||||
|
def remove_func(self, name: str):
|
||||||
|
"""Remove a function tool by its name."""
|
||||||
|
self.remove_tool(name)
|
||||||
|
|
||||||
|
@deprecated(reason="Use get_tool() instead", version="4.0.0")
|
||||||
|
def get_func(self, name: str) -> list[FunctionTool]:
|
||||||
|
"""Get all function tools."""
|
||||||
|
return self.get_tool(name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def func_list(self) -> list[FunctionTool]:
|
||||||
|
"""Get the list of function tools."""
|
||||||
|
return self.tools
|
||||||
|
|
||||||
|
def openai_schema(self, omit_empty_parameter_field: bool = False) -> list[dict]:
|
||||||
|
"""Convert tools to OpenAI API function calling schema format."""
|
||||||
|
result = []
|
||||||
|
for tool in self.tools:
|
||||||
|
func_def = {
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool.name,
|
||||||
|
"description": tool.description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if tool.parameters.get("properties") or not omit_empty_parameter_field:
|
||||||
|
func_def["function"]["parameters"] = tool.parameters
|
||||||
|
|
||||||
|
result.append(func_def)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def anthropic_schema(self) -> list[dict]:
|
||||||
|
"""Convert tools to Anthropic API format."""
|
||||||
|
result = []
|
||||||
|
for tool in self.tools:
|
||||||
|
tool_def = {
|
||||||
|
"name": tool.name,
|
||||||
|
"description": tool.description,
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": tool.parameters.get("properties", {}),
|
||||||
|
"required": tool.parameters.get("required", []),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result.append(tool_def)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def google_schema(self) -> dict:
|
||||||
|
"""Convert tools to Google GenAI API format."""
|
||||||
|
|
||||||
|
def convert_schema(schema: dict) -> dict:
|
||||||
|
"""Convert schema to Gemini API format."""
|
||||||
|
supported_types = {
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"integer",
|
||||||
|
"boolean",
|
||||||
|
"array",
|
||||||
|
"object",
|
||||||
|
"null",
|
||||||
|
}
|
||||||
|
supported_formats = {
|
||||||
|
"string": {"enum", "date-time"},
|
||||||
|
"integer": {"int32", "int64"},
|
||||||
|
"number": {"float", "double"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if "anyOf" in schema:
|
||||||
|
return {"anyOf": [convert_schema(s) for s in schema["anyOf"]]}
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if "type" in schema and schema["type"] in supported_types:
|
||||||
|
result["type"] = schema["type"]
|
||||||
|
if "format" in schema and schema["format"] in supported_formats.get(
|
||||||
|
result["type"], set()
|
||||||
|
):
|
||||||
|
result["format"] = schema["format"]
|
||||||
|
else:
|
||||||
|
result["type"] = "null"
|
||||||
|
|
||||||
|
support_fields = {
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"enum",
|
||||||
|
"minimum",
|
||||||
|
"maximum",
|
||||||
|
"maxItems",
|
||||||
|
"minItems",
|
||||||
|
"nullable",
|
||||||
|
"required",
|
||||||
|
}
|
||||||
|
result.update({k: schema[k] for k in support_fields if k in schema})
|
||||||
|
|
||||||
|
if "properties" in schema:
|
||||||
|
properties = {}
|
||||||
|
for key, value in schema["properties"].items():
|
||||||
|
prop_value = convert_schema(value)
|
||||||
|
if "default" in prop_value:
|
||||||
|
del prop_value["default"]
|
||||||
|
properties[key] = prop_value
|
||||||
|
|
||||||
|
if properties:
|
||||||
|
result["properties"] = properties
|
||||||
|
|
||||||
|
if "items" in schema:
|
||||||
|
result["items"] = convert_schema(schema["items"])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
{
|
||||||
|
"name": tool.name,
|
||||||
|
"description": tool.description,
|
||||||
|
"parameters": convert_schema(tool.parameters),
|
||||||
|
}
|
||||||
|
for tool in self.tools
|
||||||
|
]
|
||||||
|
|
||||||
|
declarations = {}
|
||||||
|
if tools:
|
||||||
|
declarations["function_declarations"] = tools
|
||||||
|
return declarations
|
||||||
|
|
||||||
|
@deprecated(reason="Use openai_schema() instead", version="4.0.0")
|
||||||
|
def get_func_desc_openai_style(self, omit_empty_parameter_field: bool = False):
|
||||||
|
return self.openai_schema(omit_empty_parameter_field)
|
||||||
|
|
||||||
|
@deprecated(reason="Use anthropic_schema() instead", version="4.0.0")
|
||||||
|
def get_func_desc_anthropic_style(self):
|
||||||
|
return self.anthropic_schema()
|
||||||
|
|
||||||
|
@deprecated(reason="Use google_schema() instead", version="4.0.0")
|
||||||
|
def get_func_desc_google_genai_style(self):
|
||||||
|
return self.google_schema()
|
||||||
|
|
||||||
|
def names(self) -> list[str]:
|
||||||
|
"""获取所有工具的名称列表"""
|
||||||
|
return [tool.name for tool in self.tools]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.tools)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return len(self.tools) > 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.tools)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ToolSet(tools={self.tools})"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"ToolSet(tools={self.tools})"
|
||||||
11
astrbot/core/agent/tool_executor.py
Normal file
11
astrbot/core/agent/tool_executor.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import mcp
|
||||||
|
from typing import Any, Generic, AsyncGenerator
|
||||||
|
from .run_context import TContext, ContextWrapper
|
||||||
|
from .tool import FunctionTool
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFunctionToolExecutor(Generic[TContext]):
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls, tool: FunctionTool, run_context: ContextWrapper[TContext], **tool_args
|
||||||
|
) -> AsyncGenerator[Any | mcp.types.CallToolResult, None]: ...
|
||||||
11
astrbot/core/astr_agent_context.py
Normal file
11
astrbot/core/astr_agent_context.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from astrbot.core.provider import Provider
|
||||||
|
from astrbot.core.provider.entities import ProviderRequest
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AstrAgentContext:
|
||||||
|
provider: Provider
|
||||||
|
first_provider_request: ProviderRequest
|
||||||
|
curr_provider_request: ProviderRequest
|
||||||
|
streaming: bool
|
||||||
276
astrbot/core/astrbot_config_mgr.py
Normal file
276
astrbot/core/astrbot_config_mgr.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from astrbot.core import AstrBotConfig, logger
|
||||||
|
from astrbot.core.utils.shared_preferences import SharedPreferences
|
||||||
|
from astrbot.core.config.astrbot_config import ASTRBOT_CONFIG_PATH
|
||||||
|
from astrbot.core.config.default import DEFAULT_CONFIG
|
||||||
|
from astrbot.core.platform.message_session import MessageSession
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_config_path
|
||||||
|
from typing import TypeVar, TypedDict
|
||||||
|
|
||||||
|
_VT = TypeVar("_VT")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfInfo(TypedDict):
|
||||||
|
"""Configuration information for a specific session or platform."""
|
||||||
|
|
||||||
|
id: str # UUID of the configuration or "default"
|
||||||
|
umop: list[str] # Unified Message Origin Pattern
|
||||||
|
name: str
|
||||||
|
path: str # File name to the configuration file
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_CONF_INFO = ConfInfo(
|
||||||
|
id="default",
|
||||||
|
umop=["::"],
|
||||||
|
name="default",
|
||||||
|
path=ASTRBOT_CONFIG_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AstrBotConfigManager:
|
||||||
|
"""A class to manage the system configuration of AstrBot, aka ACM"""
|
||||||
|
|
||||||
|
def __init__(self, default_config: AstrBotConfig, sp: SharedPreferences):
|
||||||
|
self.sp = sp
|
||||||
|
self.confs: dict[str, AstrBotConfig] = {}
|
||||||
|
"""uuid / "default" -> AstrBotConfig"""
|
||||||
|
self.confs["default"] = default_config
|
||||||
|
self._load_all_configs()
|
||||||
|
|
||||||
|
def _load_all_configs(self):
|
||||||
|
"""Load all configurations from the shared preferences."""
|
||||||
|
abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
for uuid_, meta in abconf_data.items():
|
||||||
|
filename = meta["path"]
|
||||||
|
conf_path = os.path.join(get_astrbot_config_path(), filename)
|
||||||
|
if os.path.exists(conf_path):
|
||||||
|
conf = AstrBotConfig(config_path=conf_path)
|
||||||
|
self.confs[uuid_] = conf
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Config file {conf_path} for UUID {uuid_} does not exist, skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
def _is_umo_match(self, p1: str, p2: str) -> bool:
|
||||||
|
"""判断 p2 umo 是否逻辑包含于 p1 umo"""
|
||||||
|
p1_ls = p1.split(":")
|
||||||
|
p2_ls = p2.split(":")
|
||||||
|
|
||||||
|
if len(p1_ls) != 3 or len(p2_ls) != 3:
|
||||||
|
return False # 非法格式
|
||||||
|
|
||||||
|
return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls))
|
||||||
|
|
||||||
|
def _load_conf_mapping(self, umo: str | MessageSession) -> ConfInfo:
|
||||||
|
"""获取指定 umo 的配置文件 uuid, 如果不存在则返回默认配置(返回 "default")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ConfInfo: 包含配置文件的 uuid, 路径和名称等信息, 是一个 dict 类型
|
||||||
|
"""
|
||||||
|
# uuid -> { "umop": list, "path": str, "name": str }
|
||||||
|
abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
if isinstance(umo, MessageSession):
|
||||||
|
umo = str(umo)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
umo = str(MessageSession.from_str(umo)) # validate
|
||||||
|
except Exception:
|
||||||
|
return DEFAULT_CONFIG_CONF_INFO
|
||||||
|
|
||||||
|
for uuid_, meta in abconf_data.items():
|
||||||
|
for pattern in meta["umop"]:
|
||||||
|
if self._is_umo_match(pattern, umo):
|
||||||
|
return ConfInfo(**meta, id=uuid_)
|
||||||
|
|
||||||
|
return DEFAULT_CONFIG_CONF_INFO
|
||||||
|
|
||||||
|
def _save_conf_mapping(
|
||||||
|
self,
|
||||||
|
abconf_path: str,
|
||||||
|
abconf_id: str,
|
||||||
|
umo_parts: list[str] | list[MessageSession],
|
||||||
|
abconf_name: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""保存配置文件的映射关系"""
|
||||||
|
for part in umo_parts:
|
||||||
|
if isinstance(part, MessageSession):
|
||||||
|
part = str(part)
|
||||||
|
elif not isinstance(part, str):
|
||||||
|
raise ValueError(
|
||||||
|
"umo_parts must be a list of strings or MessageSession instances"
|
||||||
|
)
|
||||||
|
abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
random_word = abconf_name or uuid.uuid4().hex[:8]
|
||||||
|
abconf_data[abconf_id] = {
|
||||||
|
"umop": umo_parts,
|
||||||
|
"path": abconf_path,
|
||||||
|
"name": random_word,
|
||||||
|
}
|
||||||
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
|
||||||
|
def get_conf(self, umo: str | MessageSession | None) -> AstrBotConfig:
|
||||||
|
"""获取指定 umo 的配置文件。如果不存在,则 fallback 到默认配置文件。"""
|
||||||
|
if not umo:
|
||||||
|
return self.confs["default"]
|
||||||
|
if isinstance(umo, MessageSession):
|
||||||
|
umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
|
||||||
|
|
||||||
|
uuid_ = self._load_conf_mapping(umo)["id"]
|
||||||
|
|
||||||
|
conf = self.confs.get(uuid_)
|
||||||
|
if not conf:
|
||||||
|
conf = self.confs["default"] # default MUST exists
|
||||||
|
|
||||||
|
return conf
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_conf(self) -> AstrBotConfig:
|
||||||
|
"""获取默认配置文件"""
|
||||||
|
return self.confs["default"]
|
||||||
|
|
||||||
|
def get_conf_info(self, umo: str | MessageSession) -> ConfInfo:
|
||||||
|
"""获取指定 umo 的配置文件元数据"""
|
||||||
|
if isinstance(umo, MessageSession):
|
||||||
|
umo = f"{umo.platform_id}:{umo.message_type}:{umo.session_id}"
|
||||||
|
|
||||||
|
return self._load_conf_mapping(umo)
|
||||||
|
|
||||||
|
def get_conf_list(self) -> list[ConfInfo]:
|
||||||
|
"""获取所有配置文件的元数据列表"""
|
||||||
|
conf_list = []
|
||||||
|
conf_list.append(DEFAULT_CONFIG_CONF_INFO)
|
||||||
|
abconf_mapping = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
for uuid_, meta in abconf_mapping.items():
|
||||||
|
conf_list.append(ConfInfo(**meta, id=uuid_))
|
||||||
|
return conf_list
|
||||||
|
|
||||||
|
def create_conf(
|
||||||
|
self,
|
||||||
|
umo_parts: list[str] | list[MessageSession],
|
||||||
|
config: dict = DEFAULT_CONFIG,
|
||||||
|
name: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
umo 由三个部分组成 [platform_id]:[message_type]:[session_id]。
|
||||||
|
|
||||||
|
umo_parts 可以是 "::" (代表所有), 可以是 "[platform_id]::" (代表指定平台下的所有类型消息和会话)。
|
||||||
|
"""
|
||||||
|
conf_uuid = str(uuid.uuid4())
|
||||||
|
conf_file_name = f"abconf_{conf_uuid}.json"
|
||||||
|
conf_path = os.path.join(get_astrbot_config_path(), conf_file_name)
|
||||||
|
conf = AstrBotConfig(config_path=conf_path, default_config=config)
|
||||||
|
conf.save_config()
|
||||||
|
self._save_conf_mapping(conf_file_name, conf_uuid, umo_parts, abconf_name=name)
|
||||||
|
self.confs[conf_uuid] = conf
|
||||||
|
return conf_uuid
|
||||||
|
|
||||||
|
def delete_conf(self, conf_id: str) -> bool:
|
||||||
|
"""删除指定配置文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conf_id: 配置文件的 UUID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 删除是否成功
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 如果试图删除默认配置文件
|
||||||
|
"""
|
||||||
|
if conf_id == "default":
|
||||||
|
raise ValueError("不能删除默认配置文件")
|
||||||
|
|
||||||
|
# 从映射中移除
|
||||||
|
abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
if conf_id not in abconf_data:
|
||||||
|
logger.warning(f"配置文件 {conf_id} 不存在于映射中")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取配置文件路径
|
||||||
|
conf_path = os.path.join(
|
||||||
|
get_astrbot_config_path(), abconf_data[conf_id]["path"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 删除配置文件
|
||||||
|
try:
|
||||||
|
if os.path.exists(conf_path):
|
||||||
|
os.remove(conf_path)
|
||||||
|
logger.info(f"已删除配置文件: {conf_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除配置文件 {conf_path} 失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 从内存中移除
|
||||||
|
if conf_id in self.confs:
|
||||||
|
del self.confs[conf_id]
|
||||||
|
|
||||||
|
# 从映射中移除
|
||||||
|
del abconf_data[conf_id]
|
||||||
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
|
||||||
|
logger.info(f"成功删除配置文件 {conf_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_conf_info(
|
||||||
|
self, conf_id: str, name: str | None = None, umo_parts: list[str] | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""更新配置文件信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conf_id: 配置文件的 UUID
|
||||||
|
name: 新的配置文件名称 (可选)
|
||||||
|
umo_parts: 新的 UMO 部分列表 (可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 更新是否成功
|
||||||
|
"""
|
||||||
|
if conf_id == "default":
|
||||||
|
raise ValueError("不能更新默认配置文件的信息")
|
||||||
|
|
||||||
|
abconf_data = self.sp.get(
|
||||||
|
"abconf_mapping", {}, scope="global", scope_id="global"
|
||||||
|
)
|
||||||
|
if conf_id not in abconf_data:
|
||||||
|
logger.warning(f"配置文件 {conf_id} 不存在于映射中")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 更新名称
|
||||||
|
if name is not None:
|
||||||
|
abconf_data[conf_id]["name"] = name
|
||||||
|
|
||||||
|
# 更新 UMO 部分
|
||||||
|
if umo_parts is not None:
|
||||||
|
# 验证 UMO 部分格式
|
||||||
|
for part in umo_parts:
|
||||||
|
if isinstance(part, MessageSession):
|
||||||
|
part = str(part)
|
||||||
|
elif not isinstance(part, str):
|
||||||
|
raise ValueError(
|
||||||
|
"umo_parts must be a list of strings or MessageSession instances"
|
||||||
|
)
|
||||||
|
abconf_data[conf_id]["umop"] = umo_parts
|
||||||
|
|
||||||
|
# 保存更新
|
||||||
|
self.sp.put("abconf_mapping", abconf_data, scope="global", scope_id="global")
|
||||||
|
logger.info(f"成功更新配置文件 {conf_id} 的信息")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def g(
|
||||||
|
self, umo: str | None = None, key: str | None = None, default: _VT = None
|
||||||
|
) -> _VT:
|
||||||
|
"""获取配置项。umo 为 None 时使用默认配置"""
|
||||||
|
if umo is None:
|
||||||
|
return self.confs["default"].get(key, default)
|
||||||
|
conf = self.get_conf(umo)
|
||||||
|
return conf.get(key, default)
|
||||||
9
astrbot/core/config/__init__.py
Normal file
9
astrbot/core/config/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .default import DEFAULT_CONFIG, VERSION, DB_PATH
|
||||||
|
from .astrbot_config import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DEFAULT_CONFIG",
|
||||||
|
"VERSION",
|
||||||
|
"DB_PATH",
|
||||||
|
"AstrBotConfig",
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user