Compare commits
2156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a59e894e1 | ||
|
|
7a8d65d37d | ||
|
|
fb10faa2dc | ||
|
|
23129a9ba2 | ||
|
|
e4df0e83ed | ||
|
|
7f791e730b | ||
|
|
4a22664b8e | ||
|
|
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 | ||
|
|
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']
|
||||||
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: '🥳 发布插件'
|
||||||
|
title: "[Plugin] 插件名"
|
||||||
|
about: 提交插件到插件市场
|
||||||
|
labels: [ "plugin-publish" ]
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
欢迎发布插件到插件市场!
|
||||||
|
|
||||||
|
## 插件基本信息
|
||||||
|
|
||||||
|
请将插件信息填写到下方的 Json 代码块中。`tags`(插件标签)和 `social_link`(社交链接)选填。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "插件名",
|
||||||
|
"desc": "插件介绍",
|
||||||
|
"author": "作者名",
|
||||||
|
"repo": "插件仓库链接",
|
||||||
|
"tags": [],
|
||||||
|
"social_link": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 检查
|
||||||
|
|
||||||
|
- [ ] 我的插件经过完整的测试
|
||||||
|
- [ ] 我的插件不包含恶意代码
|
||||||
|
- [ ] 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||||
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` 文件相应位置。
|
||||||
|
- [ ] 😮 我的更改没有引入恶意代码
|
||||||
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@v4
|
||||||
|
|
||||||
|
- 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@v4
|
||||||
|
|
||||||
|
- 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@v4
|
||||||
|
|
||||||
|
# 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@v4
|
||||||
|
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 }}
|
||||||
35
.github/workflows/dashboard_ci.yml
vendored
Normal file
35
.github/workflows/dashboard_ci.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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@v4
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Archive production artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-without-markdown
|
||||||
|
path: |
|
||||||
|
dashboard/dist
|
||||||
|
!dist/**/*.md
|
||||||
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@v4
|
||||||
- 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'
|
||||||
|
|||||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -1,12 +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
|
||||||
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
|
||||||
|
|||||||
260
README.md
260
README.md
@@ -1,51 +1,249 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
|
|
||||||
<img width="806" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/c6f057d9-46d7-4144-8116-00a962941746">
|

|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/Soulter/AstrBot/releases/latest)
|
_✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
||||||
<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>
|
|
||||||
|
|
||||||
<a href="https://astrbot.soulter.top/center">项目部署</a> |
|
<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>
|
||||||
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a> |
|
|
||||||
<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">插件开发</a>
|
[](https://github.com/Soulter/AstrBot/releases/latest)
|
||||||
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" 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?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 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
|
||||||
|
|
||||||
🌍 支持的消息平台
|
|
||||||
- QQ 群、QQ 频道(OneBot、QQ 官方接口)
|
|
||||||
- Telegram(由 [astrbot_plugin_telegram](https://github.com/Soulter/astrbot_plugin_telegram) 插件支持)
|
|
||||||
- WeChat(微信) (由 [astrbot_plugin_vchat](https://github.com/z2z63/astrbot_plugin_vchat) 插件支持)
|
|
||||||
|
|
||||||
🌍 支持的大模型一览:
|
<!-- [](https://codecov.io/gh/Soulter/AstrBot)
|
||||||
|
-->
|
||||||
|
|
||||||
- OpenAI GPT、DallE 系列
|
> [!WARNING]
|
||||||
- Claude(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
>
|
||||||
- HuggingChat(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
> 请务必修改默认密码以及保证 AstrBot 版本 >= 3.5.13。
|
||||||
- Gemini(由[LLMs插件](https://github.com/Soulter/llms)支持)
|
|
||||||
|
|
||||||
🌍 机器人支持的能力一览:
|
## ✨ 近期更新
|
||||||
- 大模型对话、人格、网页搜索
|
|
||||||
- 可视化管理面板
|
|
||||||
- 同时处理多平台消息
|
|
||||||
- 精确到个人的会话隔离
|
|
||||||
- 插件支持
|
|
||||||
- 文本转图片回复(Markdown)
|
|
||||||
|
|
||||||
## 🧩 插件支持
|
<details><summary>1. AstrBot 现已自带知识库能力</summary>
|
||||||
|
|
||||||
有关插件的使用和列表请移步:[AstrBot 文档 - 插件](https://astrbot.soulter.top/center/docs/%E4%BD%BF%E7%94%A8/%E6%8F%92%E4%BB%B6)
|
📚 详见[文档](https://astrbot.app/use/knowledge-base.html)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
2. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器!
|
||||||
|
|
||||||
|
## ✨ 主要功能
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 🪧 我们正基于前沿科研成果,设计并实现适用于角色扮演和情感陪伴的长短期记忆模型及情绪控制模型,旨在提升对话的真实性与情感表达能力。敬请期待 `v3.6.0` 版本!
|
||||||
|
|
||||||
|
1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。
|
||||||
|
2. **多消息平台接入**。支持接入 QQ(OneBot、QQ 官方机器人平台)、QQ 频道、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK、VoceChat。支持速率限制、白名单、关键词过滤、百度内容审核。
|
||||||
|
3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。
|
||||||
|
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。
|
||||||
|
5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。
|
||||||
|
6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> WebUI 在线体验 Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
|
||||||
|
>
|
||||||
|
> 用户名: `astrbot`, 密码: `astrbot`。
|
||||||
|
|
||||||
|
## ✨ 使用方式
|
||||||
|
|
||||||
|
#### 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 一键安装器部署
|
||||||
|
|
||||||
|
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
|
||||||
|
|
||||||
|
#### 宝塔面板部署
|
||||||
|
|
||||||
|
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.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
|
||||||
|
```
|
||||||
|
|
||||||
|
或者,直接通过 uvx 安装 AstrBot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir astrbot && cd astrbot
|
||||||
|
uvx astrbot init
|
||||||
|
# uvx astrbot run
|
||||||
|
```
|
||||||
|
|
||||||
|
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
||||||
|
|
||||||
|
#### 在 Replit 上部署
|
||||||
|
|
||||||
|
[](https://repl.it/github/Soulter/AstrBot)
|
||||||
|
|
||||||
|
#### 在 雨云 上部署
|
||||||
|
|
||||||
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
|
|
||||||
|
## ⚡ 消息平台支持情况
|
||||||
|
|
||||||
|
| 平台 | 支持性 |
|
||||||
|
| -------- | ------- |
|
||||||
|
| 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
|
## ✨ Demo
|
||||||
|
|
||||||
<img width="900" alt="image" src="https://github.com/Soulter/AstrBot/assets/37870767/824d1ff3-7b85-481c-b795-8e62dedb9fd7">
|
<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>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## ❤️ Special Thanks
|
||||||
|
|
||||||
|
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
|
||||||
|
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
此外,本项目的诞生离不开以下开源项目:
|
||||||
|
|
||||||
|
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
|
||||||
|
- [wechatpy/wechatpy](https://github.com/wechatpy/wechatpy)
|
||||||
|
|
||||||
|
## ⭐ Star History
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star,这是我维护这个开源项目的动力 <3
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://star-history.com/#soulter/astrbot&Date)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
|||||||
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,ae as p,B as n,af as o,j as f}from"./index-5ac7c267.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-5ac7c267.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-1875d383.js";import{_}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.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-5ac7c267.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 d,R as k,F as t,ac as h,O as p,t as m,a as V,ad as f,i as C,q as x,k as v,A as U}from"./index-5ac7c267.js";import{_ as w}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.js";const S={__name:"ConfigDetailCard",props:{config:Array},setup(s){return(y,B)=>(l(!0),o(t,null,c(s.config,r=>(l(),n(w,{key:r.name,title:r.name,style:{"margin-bottom":"16px"}},{default:u(()=>[g(d("a",null,"No data",512),[[k,s.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},[d("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(()=>[d("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 b}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.js";import{x as h,o,c as u,w as t,a,a8 as y,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,ab as j,T as l}from"./index-5ac7c267.js";import{_ as m}from"./ConfigDetailCard-756c045d.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(y,{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:b,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 @@
|
|||||||
.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}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{_ as a}from"./_plugin-vue_export-helper-c27b6911.js";import{o,c,w as s,V as i,a as t,b as e,d as l,e as r,f as d}from"./index-5ac7c267.js";const n="/assets/img-error-bg-41f65efa.svg",_="/assets/img-error-blue-f50c8e77.svg",m="/assets/img-error-text-630dc36d.svg",g="/assets/img-error-purple-b97a483b.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,[r("The page you are looking was moved, removed, "),e("br"),r("renamed, or might never exist! ")])],-1);function x(b,V){return o(),c(i,{"no-gutters":"",class:"h-100vh"},{default:s(()=>[t(d,{class:"d-flex align-center justify-center"},{default:s(()=>[e("div",u,[f,h,v,t(l,{variant:"flat",color:"primary",class:"mt-4",to:"/","prepend-icon":"mdi-home"},{default:s(()=>[r(" Home")]),_:1})])]),_:1})]),_:1})}const C=a(p,[["render",x]]);export{C as default};
|
|
||||||
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{aw 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,ax as h}from"./index-5ac7c267.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-1875d383.js";import{_ as i}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.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-5ac7c267.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 @@
|
|||||||
.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 B}from"./LogoDark.vue_vue_type_script_setup_true_lang-d555e5be.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,aq as q,av as A,F as E,c as F,N as T,J as V,L as P}from"./index-5ac7c267.js";const z="/assets/social-google-9b2fa67a.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(E,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(A,{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(q,{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(),F(r,{class:"h-100vh","no-gutters":""},{default:a(()=>[e(d,{cols:"12",class:"d-flex align-center bg-lightprimary"},{default:a(()=>[e(T,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 @@
|
|||||||
import{_ as c}from"./BaseBreadcrumb.vue_vue_type_style_index_0_lang-1875d383.js";import{_ as f}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.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-5ac7c267.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-1875d383.js";import{_ as n}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.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-5ac7c267.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-1875d383.js";import{_ as v}from"./UiParentCard.vue_vue_type_script_setup_true_lang-b40a2daa.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-5ac7c267.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-5ac7c267.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-5ac7c267.js
vendored
720
addons/dashboard/dist/assets/index-5ac7c267.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-086248bf.js
vendored
9
addons/dashboard/dist/assets/md5-086248bf.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="go-cqhttp",
|
|
||||||
description="",
|
|
||||||
body=[
|
|
||||||
DashBoardConfig(
|
|
||||||
config_type="item",
|
|
||||||
val_type="bool",
|
|
||||||
name="启用",
|
|
||||||
description="",
|
|
||||||
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,504 +0,0 @@
|
|||||||
import util.plugin_util as putil
|
|
||||||
import websockets
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
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, _reboot
|
|
||||||
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, global_object)
|
|
||||||
logger.info(f"安装插件 {repo_url} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="安装成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"/api/extensions/install: {traceback.format_exc()}")
|
|
||||||
return Response(
|
|
||||||
status="error",
|
|
||||||
message=e.__str__(),
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
|
|
||||||
@self.dashboard_be.post("/api/extensions/upload-install")
|
|
||||||
def upload_install_plugin():
|
|
||||||
try:
|
|
||||||
file = request.files['file']
|
|
||||||
print(file.filename)
|
|
||||||
logger.info(f"正在安装用户上传的插件 {file.filename}")
|
|
||||||
# save file to temp/
|
|
||||||
file_path = f"temp/{uuid.uuid4()}.zip"
|
|
||||||
file.save(file_path)
|
|
||||||
putil.install_plugin_from_file(file_path, global_object)
|
|
||||||
logger.info(f"安装插件 {file.filename} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="安装成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"/api/extensions/upload-install: {traceback.format_exc()}")
|
|
||||||
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, global_object)
|
|
||||||
logger.info(f"卸载插件 {plugin_name} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="卸载成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"/api/extensions/uninstall: {traceback.format_exc()}")
|
|
||||||
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, global_object)
|
|
||||||
logger.info(f"更新插件 {plugin_name} 成功")
|
|
||||||
return Response(
|
|
||||||
status="success",
|
|
||||||
message="更新成功~",
|
|
||||||
data=None
|
|
||||||
).__dict__
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"/api/extensions/update: {traceback.format_exc()}")
|
|
||||||
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:
|
|
||||||
logger.error(f"/api/check_update: {traceback.format_exc()}")
|
|
||||||
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(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:
|
|
||||||
logger.error(f"/api/update_project: {traceback.format_exc()}")
|
|
||||||
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)
|
|
||||||
_reboot()
|
|
||||||
|
|
||||||
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": "go-cqhttp",
|
|
||||||
"desc": "第三方 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,10 +0,0 @@
|
|||||||
# helloworld
|
|
||||||
|
|
||||||
AstrBot 插件模板
|
|
||||||
|
|
||||||
A template plugin for AstrBot plugin feature
|
|
||||||
|
|
||||||
# 支持
|
|
||||||
|
|
||||||
[帮助文档](https://astrbot.soulter.top/center/docs/%E5%BC%80%E5%8F%91/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91/
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://github.com/Soulter/helloworld
|
|
||||||
@@ -1,64 +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:
|
|
||||||
pass
|
|
||||||
|
|
||||||
"""
|
|
||||||
机器人程序会调用此函数。
|
|
||||||
"""
|
|
||||||
def run(self, ame: AstrMessageEvent):
|
|
||||||
if ame.message_str.startswith("helloworld"): # 如果消息文本以"helloworld"开头
|
|
||||||
return CommandResult(
|
|
||||||
hit=True, # 代表插件会响应此消息
|
|
||||||
success=True, # 插件响应类型为成功响应
|
|
||||||
message_chain=[Plain("Hello World!!")], # 消息链
|
|
||||||
command_name="helloworld" # 指令名
|
|
||||||
)
|
|
||||||
return CommandResult(
|
|
||||||
hit=False, # 插件不会响应此消息
|
|
||||||
success=False,
|
|
||||||
message_chain=None
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
插件元信息。
|
|
||||||
当用户输入 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",
|
|
||||||
"repo": "https://github.com/Soulter/helloworld"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
name: helloworld # 这是你的插件的唯一识别名。
|
|
||||||
desc: 这是 AstrBot 的默认插件,支持关键词回复。 # 插件简短描述
|
|
||||||
help: 输入 /keyword 查看关键词回复帮助。 # 插件的帮助信息
|
|
||||||
version: v1.3 # 插件版本号。格式:v1.1.1 或者 v1.1
|
|
||||||
author: Soulter # 作者
|
|
||||||
repo: https://github.com/Soulter/helloworld # 插件的仓库地址
|
|
||||||
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")
|
||||||
7
astrbot/api/__init__.py
Normal file
7
astrbot/api/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
__all__ = ["AstrBotConfig", "logger", "html_renderer", "llm_tool", "sp"]
|
||||||
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
|
||||||
230
astrbot/cli/utils/plugin.py
Normal file
230
astrbot/cli/utils/plugin.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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 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
|
||||||
415
astrbot/core.py
415
astrbot/core.py
@@ -1,415 +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 type.config import *
|
|
||||||
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
|
|
||||||
|
|
||||||
# 语言模型
|
|
||||||
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
|
|
||||||
|
|
||||||
# 全局对象
|
|
||||||
_global_object: GlobalObject = None
|
|
||||||
|
|
||||||
|
|
||||||
def privider_chooser(cfg):
|
|
||||||
l = []
|
|
||||||
if 'openai' in cfg and len(cfg['openai']['key']) and cfg['openai']['key'][0]:
|
|
||||||
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
|
|
||||||
|
|
||||||
init_astrbot_config_items()
|
|
||||||
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
|
|
||||||
_global_object.logger = logger
|
|
||||||
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 not nick_qq:
|
|
||||||
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)
|
|
||||||
if ok:
|
|
||||||
logger.info(
|
|
||||||
f"成功载入 {len(_global_object.cached_plugins)} 个插件")
|
|
||||||
else:
|
|
||||||
logger.error(err)
|
|
||||||
|
|
||||||
if chosen_provider is None:
|
|
||||||
llm_command_instance[NONE_LLM] = _command
|
|
||||||
chosen_provider = NONE_LLM
|
|
||||||
|
|
||||||
logger.info("正在载入机器人消息平台")
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# 初始化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。")
|
|
||||||
logger.info(f"🎉 项目启动完成")
|
|
||||||
|
|
||||||
dashboard_thread.join()
|
|
||||||
|
|
||||||
|
|
||||||
def run_qqchan_bot(cfg: dict, global_object: GlobalObject):
|
|
||||||
'''
|
|
||||||
运行 QQ_OFFICIAL 机器人
|
|
||||||
'''
|
|
||||||
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/。")
|
|
||||||
|
|
||||||
|
|
||||||
def run_gocq_bot(cfg: dict, _global_object: GlobalObject):
|
|
||||||
'''
|
|
||||||
运行 QQ_GOCQ 机器人
|
|
||||||
'''
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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 = message.message_str
|
|
||||||
hit = False # 是否命中指令
|
|
||||||
command_result = () # 调用指令返回的结果
|
|
||||||
llm_result_str = ""
|
|
||||||
|
|
||||||
# 获取平台实例
|
|
||||||
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)
|
|
||||||
|
|
||||||
if not message_str:
|
|
||||||
return MessageResult("Hi~")
|
|
||||||
|
|
||||||
# 检查发言频率
|
|
||||||
if not check_frequency(message.sender.user_id):
|
|
||||||
return MessageResult(f'你的发言超过频率限制(╯▔皿▔)╯。\n管理员设置{frequency_time}秒内只能提问{frequency_count}次。')
|
|
||||||
|
|
||||||
# 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 += "\n" + 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 hit:
|
|
||||||
# 有指令或者插件触发
|
|
||||||
# command_result 是一个元组:(指令调用是否成功, 指令返回的文本结果, 指令类型)
|
|
||||||
if not command_result:
|
|
||||||
return
|
|
||||||
if not command_result[0]:
|
|
||||||
return MessageResult(f"指令调用错误: \n{str(command_result[1])}")
|
|
||||||
if isinstance(command_result[1], (list, str)):
|
|
||||||
return MessageResult(command_result[1])
|
|
||||||
|
|
||||||
# 敏感过滤
|
|
||||||
# 过滤不合适的词
|
|
||||||
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}")
|
|
||||||
# 发送信息
|
|
||||||
return MessageResult(llm_result_str)
|
|
||||||
30
astrbot/core/__init__.py
Normal file
30
astrbot/core/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
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()
|
||||||
|
# 文件令牌服务
|
||||||
|
file_token_service = FileTokenService()
|
||||||
|
pip_installer = PipInstaller(
|
||||||
|
astrbot_config.get("pip_install_arg", ""),
|
||||||
|
astrbot_config.get("pypi_index_url", None),
|
||||||
|
)
|
||||||
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",
|
||||||
|
]
|
||||||
170
astrbot/core/config/astrbot_config.py
Normal file
170
astrbot/core/config/astrbot_config.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import enum
|
||||||
|
from .default import DEFAULT_CONFIG, DEFAULT_VALUE_MAP
|
||||||
|
from typing import Dict
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
|
ASTRBOT_CONFIG_PATH = os.path.join(get_astrbot_data_path(), "cmd_config.json")
|
||||||
|
logger = logging.getLogger("astrbot")
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitStrategy(enum.Enum):
|
||||||
|
STALL = "stall"
|
||||||
|
DISCARD = "discard"
|
||||||
|
|
||||||
|
|
||||||
|
class AstrBotConfig(dict):
|
||||||
|
"""从配置文件中加载的配置,支持直接通过点号操作符访问根配置项。
|
||||||
|
|
||||||
|
- 初始化时会将传入的 default_config 与配置文件进行比对,如果配置文件中缺少配置项则会自动插入默认值并进行一次写入操作。会递归检查配置项。
|
||||||
|
- 如果配置文件路径对应的文件不存在,则会自动创建并写入默认配置。
|
||||||
|
- 如果传入了 schema,将会通过 schema 解析出 default_config,此时传入的 default_config 会被忽略。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config_path: str = ASTRBOT_CONFIG_PATH,
|
||||||
|
default_config: dict = DEFAULT_CONFIG,
|
||||||
|
schema: dict = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件
|
||||||
|
object.__setattr__(self, "config_path", config_path)
|
||||||
|
object.__setattr__(self, "default_config", default_config)
|
||||||
|
object.__setattr__(self, "schema", schema)
|
||||||
|
|
||||||
|
if schema:
|
||||||
|
default_config = self._config_schema_to_default_config(schema)
|
||||||
|
|
||||||
|
if not self.check_exist():
|
||||||
|
"""不存在时载入默认配置"""
|
||||||
|
with open(config_path, "w", encoding="utf-8-sig") as f:
|
||||||
|
json.dump(default_config, f, indent=4, ensure_ascii=False)
|
||||||
|
object.__setattr__(self, "first_deploy", True) # 标记第一次部署
|
||||||
|
|
||||||
|
with open(config_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
conf_str = f.read()
|
||||||
|
conf = json.loads(conf_str)
|
||||||
|
|
||||||
|
# 检查配置完整性,并插入
|
||||||
|
has_new = self.check_config_integrity(default_config, conf)
|
||||||
|
self.update(conf)
|
||||||
|
if has_new:
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
self.update(conf)
|
||||||
|
|
||||||
|
def _config_schema_to_default_config(self, schema: dict) -> dict:
|
||||||
|
"""将 Schema 转换成 Config"""
|
||||||
|
conf = {}
|
||||||
|
|
||||||
|
def _parse_schema(schema: dict, conf: dict):
|
||||||
|
for k, v in schema.items():
|
||||||
|
if v["type"] not in DEFAULT_VALUE_MAP:
|
||||||
|
raise TypeError(
|
||||||
|
f"不受支持的配置类型 {v['type']}。支持的类型有:{DEFAULT_VALUE_MAP.keys()}"
|
||||||
|
)
|
||||||
|
if "default" in v:
|
||||||
|
default = v["default"]
|
||||||
|
else:
|
||||||
|
default = DEFAULT_VALUE_MAP[v["type"]]
|
||||||
|
|
||||||
|
if v["type"] == "object":
|
||||||
|
conf[k] = {}
|
||||||
|
_parse_schema(v["items"], conf[k])
|
||||||
|
else:
|
||||||
|
conf[k] = default
|
||||||
|
|
||||||
|
_parse_schema(schema, conf)
|
||||||
|
|
||||||
|
return conf
|
||||||
|
|
||||||
|
def check_config_integrity(self, refer_conf: Dict, conf: Dict, path=""):
|
||||||
|
"""检查配置完整性,如果有新的配置项或顺序不一致则返回 True"""
|
||||||
|
has_new = False
|
||||||
|
|
||||||
|
# 创建一个新的有序字典以保持参考配置的顺序
|
||||||
|
new_conf = {}
|
||||||
|
|
||||||
|
# 先按照参考配置的顺序添加配置项
|
||||||
|
for key, value in refer_conf.items():
|
||||||
|
if key not in conf:
|
||||||
|
# 配置项不存在,插入默认值
|
||||||
|
path_ = path + "." + key if path else key
|
||||||
|
logger.info(f"检查到配置项 {path_} 不存在,已插入默认值 {value}")
|
||||||
|
new_conf[key] = value
|
||||||
|
has_new = True
|
||||||
|
else:
|
||||||
|
if conf[key] is None:
|
||||||
|
# 配置项为 None,使用默认值
|
||||||
|
new_conf[key] = value
|
||||||
|
has_new = True
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
# 递归检查子配置项
|
||||||
|
if not isinstance(conf[key], dict):
|
||||||
|
# 类型不匹配,使用默认值
|
||||||
|
new_conf[key] = value
|
||||||
|
has_new = True
|
||||||
|
else:
|
||||||
|
# 递归检查并同步顺序
|
||||||
|
child_has_new = self.check_config_integrity(
|
||||||
|
value, conf[key], path + "." + key if path else key
|
||||||
|
)
|
||||||
|
new_conf[key] = conf[key]
|
||||||
|
has_new |= child_has_new
|
||||||
|
else:
|
||||||
|
# 直接使用现有配置
|
||||||
|
new_conf[key] = conf[key]
|
||||||
|
|
||||||
|
# 检查是否存在参考配置中没有的配置项
|
||||||
|
for key in list(conf.keys()):
|
||||||
|
if key not in refer_conf:
|
||||||
|
path_ = path + "." + key if path else key
|
||||||
|
logger.info(f"检查到配置项 {path_} 不存在,将从当前配置中删除")
|
||||||
|
has_new = True
|
||||||
|
|
||||||
|
# 顺序不一致也算作变更
|
||||||
|
if list(conf.keys()) != list(new_conf.keys()):
|
||||||
|
if path:
|
||||||
|
logger.info(f"检查到配置项 {path} 的子项顺序不一致,已重新排序")
|
||||||
|
else:
|
||||||
|
logger.info("检查到配置项顺序不一致,已重新排序")
|
||||||
|
has_new = True
|
||||||
|
|
||||||
|
# 更新原始配置
|
||||||
|
conf.clear()
|
||||||
|
conf.update(new_conf)
|
||||||
|
|
||||||
|
return has_new
|
||||||
|
|
||||||
|
def save_config(self, replace_config: Dict = None):
|
||||||
|
"""将配置写入文件
|
||||||
|
|
||||||
|
如果传入 replace_config,则将配置替换为 replace_config
|
||||||
|
"""
|
||||||
|
if replace_config:
|
||||||
|
self.update(replace_config)
|
||||||
|
with open(self.config_path, "w", encoding="utf-8-sig") as f:
|
||||||
|
json.dump(self, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
try:
|
||||||
|
return self[item]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __delattr__(self, key):
|
||||||
|
try:
|
||||||
|
del self[key]
|
||||||
|
self.save_config()
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(f"没有找到 Key: '{key}'")
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def check_exist(self) -> bool:
|
||||||
|
return os.path.exists(self.config_path)
|
||||||
1919
astrbot/core/config/default.py
Normal file
1919
astrbot/core/config/default.py
Normal file
File diff suppressed because it is too large
Load Diff
209
astrbot/core/conversation_mgr.py
Normal file
209
astrbot/core/conversation_mgr.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
"""
|
||||||
|
AstrBot 会话-对话管理器, 维护两个本地存储, 其中一个是 json 格式的shared_preferences, 另外一个是数据库
|
||||||
|
|
||||||
|
在 AstrBot 中, 会话和对话是独立的, 会话用于标记对话窗口, 例如群聊"123456789"可以建立一个会话,
|
||||||
|
在一个会话中可以建立多个对话, 并且支持对话的切换和删除
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from astrbot.core import sp
|
||||||
|
from typing import Dict, List
|
||||||
|
from astrbot.core.db import BaseDatabase
|
||||||
|
from astrbot.core.db.po import Conversation
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationManager:
|
||||||
|
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
|
||||||
|
|
||||||
|
def __init__(self, db_helper: BaseDatabase):
|
||||||
|
# session_conversations 字典记录会话ID-对话ID 映射关系
|
||||||
|
self.session_conversations: Dict[str, str] = sp.get("session_conversation", {})
|
||||||
|
self.db = db_helper
|
||||||
|
self.save_interval = 60 # 每 60 秒保存一次
|
||||||
|
self._start_periodic_save()
|
||||||
|
|
||||||
|
def _start_periodic_save(self):
|
||||||
|
"""启动定时保存任务"""
|
||||||
|
asyncio.create_task(self._periodic_save())
|
||||||
|
|
||||||
|
async def _periodic_save(self):
|
||||||
|
"""定时保存会话对话映射关系到存储中"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(self.save_interval)
|
||||||
|
self._save_to_storage()
|
||||||
|
|
||||||
|
def _save_to_storage(self):
|
||||||
|
"""保存会话对话映射关系到存储中"""
|
||||||
|
sp.put("session_conversation", self.session_conversations)
|
||||||
|
|
||||||
|
async def new_conversation(self, unified_msg_origin: str) -> str:
|
||||||
|
"""新建对话,并将当前会话的对话转移到新对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
Returns:
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
"""
|
||||||
|
conversation_id = str(uuid.uuid4())
|
||||||
|
self.db.new_conversation(user_id=unified_msg_origin, cid=conversation_id)
|
||||||
|
self.session_conversations[unified_msg_origin] = conversation_id
|
||||||
|
sp.put("session_conversation", self.session_conversations)
|
||||||
|
return conversation_id
|
||||||
|
|
||||||
|
async def switch_conversation(self, unified_msg_origin: str, conversation_id: str):
|
||||||
|
"""切换会话的对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
"""
|
||||||
|
self.session_conversations[unified_msg_origin] = conversation_id
|
||||||
|
sp.put("session_conversation", self.session_conversations)
|
||||||
|
|
||||||
|
async def delete_conversation(
|
||||||
|
self, unified_msg_origin: str, conversation_id: str = None
|
||||||
|
):
|
||||||
|
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
"""
|
||||||
|
conversation_id = self.session_conversations.get(unified_msg_origin)
|
||||||
|
if conversation_id:
|
||||||
|
self.db.delete_conversation(user_id=unified_msg_origin, cid=conversation_id)
|
||||||
|
del self.session_conversations[unified_msg_origin]
|
||||||
|
sp.put("session_conversation", self.session_conversations)
|
||||||
|
|
||||||
|
async def get_curr_conversation_id(self, unified_msg_origin: str) -> str:
|
||||||
|
"""获取会话当前的对话 ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
Returns:
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
"""
|
||||||
|
return self.session_conversations.get(unified_msg_origin, None)
|
||||||
|
|
||||||
|
async def get_conversation(
|
||||||
|
self,
|
||||||
|
unified_msg_origin: str,
|
||||||
|
conversation_id: str,
|
||||||
|
create_if_not_exists: bool = False,
|
||||||
|
) -> Conversation:
|
||||||
|
"""获取会话的对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
Returns:
|
||||||
|
conversation (Conversation): 对话对象
|
||||||
|
"""
|
||||||
|
conv = self.db.get_conversation_by_user_id(unified_msg_origin, conversation_id)
|
||||||
|
if not conv and create_if_not_exists:
|
||||||
|
# 如果对话不存在且需要创建,则新建一个对话
|
||||||
|
conversation_id = await self.new_conversation(unified_msg_origin)
|
||||||
|
return self.db.get_conversation_by_user_id(
|
||||||
|
unified_msg_origin, conversation_id
|
||||||
|
)
|
||||||
|
return self.db.get_conversation_by_user_id(unified_msg_origin, conversation_id)
|
||||||
|
|
||||||
|
async def get_conversations(self, unified_msg_origin: str) -> List[Conversation]:
|
||||||
|
"""获取会话的所有对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
Returns:
|
||||||
|
conversations (List[Conversation]): 对话对象列表
|
||||||
|
"""
|
||||||
|
return self.db.get_conversations(unified_msg_origin)
|
||||||
|
|
||||||
|
async def update_conversation(
|
||||||
|
self, unified_msg_origin: str, conversation_id: str, history: List[Dict]
|
||||||
|
):
|
||||||
|
"""更新会话的对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
history (List[Dict]): 对话历史记录, 是一个字典列表, 每个字典包含 role 和 content 字段
|
||||||
|
"""
|
||||||
|
if conversation_id:
|
||||||
|
self.db.update_conversation(
|
||||||
|
user_id=unified_msg_origin,
|
||||||
|
cid=conversation_id,
|
||||||
|
history=json.dumps(history),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_conversation_title(self, unified_msg_origin: str, title: str):
|
||||||
|
"""更新会话的对话标题
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
title (str): 对话标题
|
||||||
|
"""
|
||||||
|
conversation_id = self.session_conversations.get(unified_msg_origin)
|
||||||
|
if conversation_id:
|
||||||
|
self.db.update_conversation_title(
|
||||||
|
user_id=unified_msg_origin, cid=conversation_id, title=title
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_conversation_persona_id(
|
||||||
|
self, unified_msg_origin: str, persona_id: str
|
||||||
|
):
|
||||||
|
"""更新会话的对话 Persona ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
persona_id (str): 对话 Persona ID
|
||||||
|
"""
|
||||||
|
conversation_id = self.session_conversations.get(unified_msg_origin)
|
||||||
|
if conversation_id:
|
||||||
|
self.db.update_conversation_persona_id(
|
||||||
|
user_id=unified_msg_origin, cid=conversation_id, persona_id=persona_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_human_readable_context(
|
||||||
|
self, unified_msg_origin, conversation_id, page=1, page_size=10
|
||||||
|
):
|
||||||
|
"""获取人类可读的上下文
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||||
|
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||||
|
page (int): 页码
|
||||||
|
page_size (int): 每页大小
|
||||||
|
"""
|
||||||
|
conversation = await self.get_conversation(unified_msg_origin, conversation_id)
|
||||||
|
history = json.loads(conversation.history)
|
||||||
|
|
||||||
|
contexts = []
|
||||||
|
temp_contexts = []
|
||||||
|
for record in history:
|
||||||
|
if record["role"] == "user":
|
||||||
|
temp_contexts.append(f"User: {record['content']}")
|
||||||
|
elif record["role"] == "assistant":
|
||||||
|
if "content" in record and record["content"]:
|
||||||
|
temp_contexts.append(f"Assistant: {record['content']}")
|
||||||
|
elif "tool_calls" in record:
|
||||||
|
tool_calls_str = json.dumps(
|
||||||
|
record["tool_calls"], ensure_ascii=False
|
||||||
|
)
|
||||||
|
temp_contexts.append(f"Assistant: [函数调用] {tool_calls_str}")
|
||||||
|
else:
|
||||||
|
temp_contexts.append("Assistant: [未知的内容]")
|
||||||
|
contexts.insert(0, temp_contexts)
|
||||||
|
temp_contexts = []
|
||||||
|
|
||||||
|
# 展平 contexts 列表
|
||||||
|
contexts = [item for sublist in contexts for item in sublist]
|
||||||
|
|
||||||
|
# 计算分页
|
||||||
|
paged_contexts = contexts[(page - 1) * page_size : page * page_size]
|
||||||
|
total_pages = len(contexts) // page_size
|
||||||
|
if len(contexts) % page_size != 0:
|
||||||
|
total_pages += 1
|
||||||
|
|
||||||
|
return paged_contexts, total_pages
|
||||||
229
astrbot/core/core_lifecycle.py
Normal file
229
astrbot/core/core_lifecycle.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
"""
|
||||||
|
Astrbot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
||||||
|
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus等。
|
||||||
|
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
||||||
|
|
||||||
|
工作流程:
|
||||||
|
1. 初始化所有组件
|
||||||
|
2. 启动事件总线和任务, 所有任务都在这里运行
|
||||||
|
3. 执行启动完成事件钩子
|
||||||
|
"""
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import os
|
||||||
|
from .event_bus import EventBus
|
||||||
|
from . import astrbot_config
|
||||||
|
from asyncio import Queue
|
||||||
|
from typing import List
|
||||||
|
from astrbot.core.pipeline.scheduler import PipelineScheduler, PipelineContext
|
||||||
|
from astrbot.core.star import PluginManager
|
||||||
|
from astrbot.core.platform.manager import PlatformManager
|
||||||
|
from astrbot.core.star.context import Context
|
||||||
|
from astrbot.core.provider.manager import ProviderManager
|
||||||
|
from astrbot.core import LogBroker
|
||||||
|
from astrbot.core.db import BaseDatabase
|
||||||
|
from astrbot.core.updator import AstrBotUpdator
|
||||||
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.config.default import VERSION
|
||||||
|
from astrbot.core.conversation_mgr import ConversationManager
|
||||||
|
from astrbot.core.star.star_handler import star_handlers_registry, EventType
|
||||||
|
from astrbot.core.star.star_handler import star_map
|
||||||
|
|
||||||
|
|
||||||
|
class AstrBotCoreLifecycle:
|
||||||
|
"""
|
||||||
|
AstrBot 核心生命周期管理类, 负责管理 AstrBot 的启动、停止、重启等操作。
|
||||||
|
该类负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、
|
||||||
|
EventBus 等。
|
||||||
|
该类还负责加载和执行插件, 以及处理事件总线的分发。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, log_broker: LogBroker, db: BaseDatabase):
|
||||||
|
self.log_broker = log_broker # 初始化日志代理
|
||||||
|
self.astrbot_config = astrbot_config # 初始化配置
|
||||||
|
self.db = db # 初始化数据库
|
||||||
|
|
||||||
|
# 设置代理
|
||||||
|
if self.astrbot_config.get("http_proxy", ""):
|
||||||
|
os.environ["https_proxy"] = self.astrbot_config["http_proxy"]
|
||||||
|
os.environ["http_proxy"] = self.astrbot_config["http_proxy"]
|
||||||
|
if proxy := os.environ.get("https_proxy"):
|
||||||
|
logger.debug(f"Using proxy: {proxy}")
|
||||||
|
os.environ["no_proxy"] = "localhost"
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""
|
||||||
|
初始化 AstrBot 核心生命周期管理类, 负责初始化各个组件, 包括 ProviderManager、PlatformManager、ConversationManager、PluginManager、PipelineScheduler、EventBus、AstrBotUpdator等。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 初始化日志代理
|
||||||
|
logger.info("AstrBot v" + VERSION)
|
||||||
|
if os.environ.get("TESTING", ""):
|
||||||
|
logger.setLevel("DEBUG") # 测试模式下设置日志级别为 DEBUG
|
||||||
|
else:
|
||||||
|
logger.setLevel(self.astrbot_config["log_level"]) # 设置日志级别
|
||||||
|
|
||||||
|
# 初始化事件队列
|
||||||
|
self.event_queue = Queue()
|
||||||
|
|
||||||
|
# 初始化供应商管理器
|
||||||
|
self.provider_manager = ProviderManager(self.astrbot_config, self.db)
|
||||||
|
|
||||||
|
# 初始化平台管理器
|
||||||
|
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue)
|
||||||
|
|
||||||
|
# 初始化对话管理器
|
||||||
|
self.conversation_manager = ConversationManager(self.db)
|
||||||
|
|
||||||
|
# 初始化提供给插件的上下文
|
||||||
|
self.star_context = Context(
|
||||||
|
self.event_queue,
|
||||||
|
self.astrbot_config,
|
||||||
|
self.db,
|
||||||
|
self.provider_manager,
|
||||||
|
self.platform_manager,
|
||||||
|
self.conversation_manager,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化插件管理器
|
||||||
|
self.plugin_manager = PluginManager(self.star_context, self.astrbot_config)
|
||||||
|
|
||||||
|
# 扫描、注册插件、实例化插件类
|
||||||
|
await self.plugin_manager.reload()
|
||||||
|
|
||||||
|
# 根据配置实例化各个 Provider
|
||||||
|
await self.provider_manager.initialize()
|
||||||
|
|
||||||
|
# 初始化消息事件流水线调度器
|
||||||
|
self.pipeline_scheduler = PipelineScheduler(
|
||||||
|
PipelineContext(self.astrbot_config, self.plugin_manager)
|
||||||
|
)
|
||||||
|
await self.pipeline_scheduler.initialize()
|
||||||
|
|
||||||
|
# 初始化更新器
|
||||||
|
self.astrbot_updator = AstrBotUpdator()
|
||||||
|
|
||||||
|
# 初始化事件总线
|
||||||
|
self.event_bus = EventBus(self.event_queue, self.pipeline_scheduler)
|
||||||
|
|
||||||
|
# 记录启动时间
|
||||||
|
self.start_time = int(time.time())
|
||||||
|
|
||||||
|
# 初始化当前任务列表
|
||||||
|
self.curr_tasks: List[asyncio.Task] = []
|
||||||
|
|
||||||
|
# 根据配置实例化各个平台适配器
|
||||||
|
await self.platform_manager.initialize()
|
||||||
|
|
||||||
|
# 初始化关闭控制面板的事件
|
||||||
|
self.dashboard_shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""加载事件总线和任务并初始化"""
|
||||||
|
|
||||||
|
# 创建一个异步任务来执行事件总线的 dispatch() 方法
|
||||||
|
# dispatch是一个无限循环的协程, 从事件队列中获取事件并处理
|
||||||
|
event_bus_task = asyncio.create_task(
|
||||||
|
self.event_bus.dispatch(), name="event_bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 把插件中注册的所有协程函数注册到事件总线中并执行
|
||||||
|
extra_tasks = []
|
||||||
|
for task in self.star_context._register_tasks:
|
||||||
|
extra_tasks.append(asyncio.create_task(task, name=task.__name__))
|
||||||
|
|
||||||
|
tasks_ = [event_bus_task, *extra_tasks]
|
||||||
|
for task in tasks_:
|
||||||
|
self.curr_tasks.append(
|
||||||
|
asyncio.create_task(self._task_wrapper(task), name=task.get_name())
|
||||||
|
)
|
||||||
|
|
||||||
|
self.start_time = int(time.time())
|
||||||
|
|
||||||
|
async def _task_wrapper(self, task: asyncio.Task):
|
||||||
|
"""异步任务包装器, 用于处理异步任务执行中出现的各种异常
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task (asyncio.Task): 要执行的异步任务
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass # 任务被取消, 静默处理
|
||||||
|
except Exception as e:
|
||||||
|
# 获取完整的异常堆栈信息, 按行分割并记录到日志中
|
||||||
|
logger.error(f"------- 任务 {task.get_name()} 发生错误: {e}")
|
||||||
|
for line in traceback.format_exc().split("\n"):
|
||||||
|
logger.error(f"| {line}")
|
||||||
|
logger.error("-------")
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""启动 AstrBot 核心生命周期管理类, 用load加载事件总线和任务并初始化, 执行启动完成事件钩子"""
|
||||||
|
self._load()
|
||||||
|
logger.info("AstrBot 启动完成。")
|
||||||
|
|
||||||
|
# 执行启动完成事件钩子
|
||||||
|
handlers = star_handlers_registry.get_handlers_by_event_type(
|
||||||
|
EventType.OnAstrBotLoadedEvent
|
||||||
|
)
|
||||||
|
for handler in handlers:
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
f"hook(on_astrbot_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
|
||||||
|
)
|
||||||
|
await handler.handler()
|
||||||
|
except BaseException:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# 同时运行curr_tasks中的所有任务
|
||||||
|
await asyncio.gather(*self.curr_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器"""
|
||||||
|
# 请求停止所有正在运行的异步任务
|
||||||
|
for task in self.curr_tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
for plugin in self.plugin_manager.context.get_all_stars():
|
||||||
|
try:
|
||||||
|
await self.plugin_manager._terminate_plugin(plugin)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(traceback.format_exc())
|
||||||
|
logger.warning(
|
||||||
|
f"插件 {plugin.name} 未被正常终止 {e!s}, 可能会导致资源泄露等问题。"
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.provider_manager.terminate()
|
||||||
|
await self.platform_manager.terminate()
|
||||||
|
self.dashboard_shutdown_event.set()
|
||||||
|
|
||||||
|
# 再次遍历curr_tasks等待每个任务真正结束
|
||||||
|
for task in self.curr_tasks:
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务 {task.get_name()} 发生错误: {e}")
|
||||||
|
|
||||||
|
async def restart(self):
|
||||||
|
"""重启 AstrBot 核心生命周期管理类, 终止各个管理器并重新加载平台实例"""
|
||||||
|
await self.provider_manager.terminate()
|
||||||
|
await self.platform_manager.terminate()
|
||||||
|
self.dashboard_shutdown_event.set()
|
||||||
|
threading.Thread(
|
||||||
|
target=self.astrbot_updator._reboot, name="restart", daemon=True
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def load_platform(self) -> List[asyncio.Task]:
|
||||||
|
"""加载平台实例并返回所有平台实例的异步任务列表"""
|
||||||
|
tasks = []
|
||||||
|
platform_insts = self.platform_manager.get_insts()
|
||||||
|
for platform_inst in platform_insts:
|
||||||
|
tasks.append(
|
||||||
|
asyncio.create_task(platform_inst.run(), name=platform_inst.meta().name)
|
||||||
|
)
|
||||||
|
return tasks
|
||||||
161
astrbot/core/db/__init__.py
Normal file
161
astrbot/core/db/__init__.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import abc
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict, Any, Tuple
|
||||||
|
from astrbot.core.db.po import Stats, LLMHistory, ATRIVision, Conversation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseDatabase(abc.ABC):
|
||||||
|
"""
|
||||||
|
数据库基类
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_base_metrics(self, metrics: dict):
|
||||||
|
"""插入基础指标数据"""
|
||||||
|
self.insert_platform_metrics(metrics["platform_stats"])
|
||||||
|
self.insert_plugin_metrics(metrics["plugin_stats"])
|
||||||
|
self.insert_command_metrics(metrics["command_stats"])
|
||||||
|
self.insert_llm_metrics(metrics["llm_stats"])
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_platform_metrics(self, metrics: dict):
|
||||||
|
"""插入平台指标数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_plugin_metrics(self, metrics: dict):
|
||||||
|
"""插入插件指标数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_command_metrics(self, metrics: dict):
|
||||||
|
"""插入指令指标数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_llm_metrics(self, metrics: dict):
|
||||||
|
"""插入 LLM 指标数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_llm_history(self, session_id: str, content: str, provider_type: str):
|
||||||
|
"""更新 LLM 历史记录。当不存在 session_id 时插入"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_llm_history(
|
||||||
|
self, session_id: str = None, provider_type: str = None
|
||||||
|
) -> List[LLMHistory]:
|
||||||
|
"""获取 LLM 历史记录, 如果 session_id 为 None, 返回所有"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||||
|
"""获取基础统计数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_total_message_count(self) -> int:
|
||||||
|
"""获取总消息数"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_grouped_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||||
|
"""获取基础统计数据(合并)"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def insert_atri_vision_data(self, vision_data: ATRIVision):
|
||||||
|
"""插入 ATRI 视觉数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_atri_vision_data(self) -> List[ATRIVision]:
|
||||||
|
"""获取 ATRI 视觉数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_atri_vision_data_by_path_or_id(
|
||||||
|
self, url_or_path: str, id: str
|
||||||
|
) -> ATRIVision:
|
||||||
|
"""通过 url 或 path 获取 ATRI 视觉数据"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_conversation_by_user_id(self, user_id: str, cid: str) -> Conversation:
|
||||||
|
"""通过 user_id 和 cid 获取 Conversation"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def new_conversation(self, user_id: str, cid: str):
|
||||||
|
"""新建 Conversation"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_conversations(self, user_id: str) -> List[Conversation]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_conversation(self, user_id: str, cid: str, history: str):
|
||||||
|
"""更新 Conversation"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def delete_conversation(self, user_id: str, cid: str):
|
||||||
|
"""删除 Conversation"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_conversation_title(self, user_id: str, cid: str, title: str):
|
||||||
|
"""更新 Conversation 标题"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def update_conversation_persona_id(self, user_id: str, cid: str, persona_id: str):
|
||||||
|
"""更新 Conversation Persona ID"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_all_conversations(
|
||||||
|
self, page: int = 1, page_size: int = 20
|
||||||
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
"""获取所有对话,支持分页
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码,从1开始
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[Dict[str, Any]], int]: 返回一个元组,包含对话列表和总对话数
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_filtered_conversations(
|
||||||
|
self,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
platforms: List[str] = None,
|
||||||
|
message_types: List[str] = None,
|
||||||
|
search_query: str = None,
|
||||||
|
exclude_ids: List[str] = None,
|
||||||
|
exclude_platforms: List[str] = None,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
"""获取筛选后的对话列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
platforms: 平台筛选列表
|
||||||
|
message_types: 消息类型筛选列表
|
||||||
|
search_query: 搜索关键词
|
||||||
|
exclude_ids: 排除的用户ID列表
|
||||||
|
exclude_platforms: 排除的平台列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[List[Dict[str, Any]], int]: 返回一个元组,包含对话列表和总对话数
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
89
astrbot/core/db/po.py
Normal file
89
astrbot/core/db/po.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""指标数据"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Platform:
|
||||||
|
"""平台使用统计数据"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Provider:
|
||||||
|
"""供应商使用统计数据"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Plugin:
|
||||||
|
"""插件使用统计数据"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Command:
|
||||||
|
"""命令使用统计数据"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
count: int
|
||||||
|
timestamp: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Stats:
|
||||||
|
platform: List[Platform] = field(default_factory=list)
|
||||||
|
command: List[Command] = field(default_factory=list)
|
||||||
|
llm: List[Provider] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LLMHistory:
|
||||||
|
"""LLM 聊天时持久化的信息"""
|
||||||
|
|
||||||
|
provider_type: str
|
||||||
|
session_id: str
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ATRIVision:
|
||||||
|
"""Deprecated"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
url_or_path: str
|
||||||
|
caption: str
|
||||||
|
is_meme: bool
|
||||||
|
keywords: List[str]
|
||||||
|
platform_name: str
|
||||||
|
session_id: str
|
||||||
|
sender_nickname: str
|
||||||
|
timestamp: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Conversation:
|
||||||
|
"""LLM 对话存储
|
||||||
|
|
||||||
|
对于网页聊天,history 存储了包括指令、回复、图片等在内的所有消息。
|
||||||
|
对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_id: str
|
||||||
|
cid: str
|
||||||
|
history: str = ""
|
||||||
|
"""字符串格式的列表。"""
|
||||||
|
created_at: int = 0
|
||||||
|
updated_at: int = 0
|
||||||
|
title: str = ""
|
||||||
|
persona_id: str = ""
|
||||||
567
astrbot/core/db/sqlite.py
Normal file
567
astrbot/core/db/sqlite.py
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from astrbot.core.db.po import Platform, Stats, LLMHistory, ATRIVision, Conversation
|
||||||
|
from . import BaseDatabase
|
||||||
|
from typing import Tuple, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class SQLiteDatabase(BaseDatabase):
|
||||||
|
def __init__(self, db_path: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
with open(
|
||||||
|
os.path.dirname(__file__) + "/sqlite_init.sql", "r", encoding="utf-8"
|
||||||
|
) as f:
|
||||||
|
sql = f.read()
|
||||||
|
|
||||||
|
# 初始化数据库
|
||||||
|
self.conn = self._get_conn(self.db_path)
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.executescript(sql)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
# 检查 webchat_conversation 的 title 字段是否存在
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
PRAGMA table_info(webchat_conversation)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
res = c.fetchall()
|
||||||
|
has_title = False
|
||||||
|
has_persona_id = False
|
||||||
|
for row in res:
|
||||||
|
if row[1] == "title":
|
||||||
|
has_title = True
|
||||||
|
if row[1] == "persona_id":
|
||||||
|
has_persona_id = True
|
||||||
|
if not has_title:
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE webchat_conversation ADD COLUMN title TEXT;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
if not has_persona_id:
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
ALTER TABLE webchat_conversation ADD COLUMN persona_id TEXT;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
def _get_conn(self, db_path: str) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.text_factory = str
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _exec_sql(self, sql: str, params: Tuple = None):
|
||||||
|
conn = self.conn
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
conn = self._get_conn(self.db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if params:
|
||||||
|
c.execute(sql, params)
|
||||||
|
c.close()
|
||||||
|
else:
|
||||||
|
c.execute(sql)
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def insert_platform_metrics(self, metrics: dict):
|
||||||
|
for k, v in metrics.items():
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO platform(name, count, timestamp) VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(k, v, int(time.time())),
|
||||||
|
)
|
||||||
|
|
||||||
|
def insert_plugin_metrics(self, metrics: dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def insert_command_metrics(self, metrics: dict):
|
||||||
|
for k, v in metrics.items():
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO command(name, count, timestamp) VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(k, v, int(time.time())),
|
||||||
|
)
|
||||||
|
|
||||||
|
def insert_llm_metrics(self, metrics: dict):
|
||||||
|
for k, v in metrics.items():
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO llm(name, count, timestamp) VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(k, v, int(time.time())),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_llm_history(self, session_id: str, content: str, provider_type: str):
|
||||||
|
res = self.get_llm_history(session_id, provider_type)
|
||||||
|
if res:
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
UPDATE llm_history SET content = ? WHERE session_id = ? AND provider_type = ?
|
||||||
|
""",
|
||||||
|
(content, session_id, provider_type),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO llm_history(provider_type, session_id, content) VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(provider_type, session_id, content),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_llm_history(
|
||||||
|
self, session_id: str = None, provider_type: str = None
|
||||||
|
) -> Tuple:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if session_id:
|
||||||
|
conditions.append("session_id = ?")
|
||||||
|
params.append(session_id)
|
||||||
|
|
||||||
|
if provider_type:
|
||||||
|
conditions.append("provider_type = ?")
|
||||||
|
params.append(provider_type)
|
||||||
|
|
||||||
|
sql = "SELECT * FROM llm_history"
|
||||||
|
if conditions:
|
||||||
|
sql += " WHERE " + " AND ".join(conditions)
|
||||||
|
|
||||||
|
c.execute(sql, params)
|
||||||
|
|
||||||
|
res = c.fetchall()
|
||||||
|
histories = []
|
||||||
|
for row in res:
|
||||||
|
histories.append(LLMHistory(*row))
|
||||||
|
c.close()
|
||||||
|
return histories
|
||||||
|
|
||||||
|
def get_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||||
|
"""获取 offset_sec 秒前到现在的基础统计数据"""
|
||||||
|
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM platform
|
||||||
|
"""
|
||||||
|
+ where_clause
|
||||||
|
)
|
||||||
|
|
||||||
|
platform = []
|
||||||
|
for row in c.fetchall():
|
||||||
|
platform.append(Platform(*row))
|
||||||
|
|
||||||
|
# c.execute(
|
||||||
|
# '''
|
||||||
|
# SELECT * FROM command
|
||||||
|
# ''' + where_clause
|
||||||
|
# )
|
||||||
|
|
||||||
|
# command = []
|
||||||
|
# for row in c.fetchall():
|
||||||
|
# command.append(Command(*row))
|
||||||
|
|
||||||
|
# c.execute(
|
||||||
|
# '''
|
||||||
|
# SELECT * FROM llm
|
||||||
|
# ''' + where_clause
|
||||||
|
# )
|
||||||
|
|
||||||
|
# llm = []
|
||||||
|
# for row in c.fetchall():
|
||||||
|
# llm.append(Provider(*row))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
return Stats(platform, [], [])
|
||||||
|
|
||||||
|
def get_total_message_count(self) -> int:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT SUM(count) FROM platform
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
res = c.fetchone()
|
||||||
|
c.close()
|
||||||
|
return res[0]
|
||||||
|
|
||||||
|
def get_grouped_base_stats(self, offset_sec: int = 86400) -> Stats:
|
||||||
|
"""获取 offset_sec 秒前到现在的基础统计数据(合并)"""
|
||||||
|
where_clause = f" WHERE timestamp >= {int(time.time()) - offset_sec}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT name, SUM(count), timestamp FROM platform
|
||||||
|
"""
|
||||||
|
+ where_clause
|
||||||
|
+ " GROUP BY name"
|
||||||
|
)
|
||||||
|
|
||||||
|
platform = []
|
||||||
|
for row in c.fetchall():
|
||||||
|
platform.append(Platform(*row))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
return Stats(platform, [], [])
|
||||||
|
|
||||||
|
def get_conversation_by_user_id(self, user_id: str, cid: str) -> Conversation:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
||||||
|
""",
|
||||||
|
(user_id, cid),
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchone()
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return
|
||||||
|
|
||||||
|
return Conversation(*res)
|
||||||
|
|
||||||
|
def new_conversation(self, user_id: str, cid: str):
|
||||||
|
history = "[]"
|
||||||
|
updated_at = int(time.time())
|
||||||
|
created_at = updated_at
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO webchat_conversation(user_id, cid, history, updated_at, created_at) VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(user_id, cid, history, updated_at, created_at),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_conversations(self, user_id: str) -> Tuple:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT cid, created_at, updated_at, title, persona_id FROM webchat_conversation WHERE user_id = ? ORDER BY updated_at DESC
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchall()
|
||||||
|
c.close()
|
||||||
|
conversations = []
|
||||||
|
for row in res:
|
||||||
|
cid = row[0]
|
||||||
|
created_at = row[1]
|
||||||
|
updated_at = row[2]
|
||||||
|
title = row[3]
|
||||||
|
persona_id = row[4]
|
||||||
|
conversations.append(
|
||||||
|
Conversation("", cid, "[]", created_at, updated_at, title, persona_id)
|
||||||
|
)
|
||||||
|
return conversations
|
||||||
|
|
||||||
|
def update_conversation(self, user_id: str, cid: str, history: str):
|
||||||
|
"""更新对话,并且同时更新时间"""
|
||||||
|
updated_at = int(time.time())
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
UPDATE webchat_conversation SET history = ?, updated_at = ? WHERE user_id = ? AND cid = ?
|
||||||
|
""",
|
||||||
|
(history, updated_at, user_id, cid),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_conversation_title(self, user_id: str, cid: str, title: str):
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ?
|
||||||
|
""",
|
||||||
|
(title, user_id, cid),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_conversation_persona_id(self, user_id: str, cid: str, persona_id: str):
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ?
|
||||||
|
""",
|
||||||
|
(persona_id, user_id, cid),
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_conversation(self, user_id: str, cid: str):
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
||||||
|
""",
|
||||||
|
(user_id, cid),
|
||||||
|
)
|
||||||
|
|
||||||
|
def insert_atri_vision_data(self, vision: ATRIVision):
|
||||||
|
ts = int(time.time())
|
||||||
|
keywords = ",".join(vision.keywords)
|
||||||
|
self._exec_sql(
|
||||||
|
"""
|
||||||
|
INSERT INTO atri_vision(id, url_or_path, caption, is_meme, keywords, platform_name, session_id, sender_nickname, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
vision.id,
|
||||||
|
vision.url_or_path,
|
||||||
|
vision.caption,
|
||||||
|
vision.is_meme,
|
||||||
|
keywords,
|
||||||
|
vision.platform_name,
|
||||||
|
vision.session_id,
|
||||||
|
vision.sender_nickname,
|
||||||
|
ts,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_atri_vision_data(self) -> Tuple:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM atri_vision
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchall()
|
||||||
|
visions = []
|
||||||
|
for row in res:
|
||||||
|
visions.append(ATRIVision(*row))
|
||||||
|
c.close()
|
||||||
|
return visions
|
||||||
|
|
||||||
|
def get_atri_vision_data_by_path_or_id(
|
||||||
|
self, url_or_path: str, id: str
|
||||||
|
) -> ATRIVision:
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM atri_vision WHERE url_or_path = ? OR id = ?
|
||||||
|
""",
|
||||||
|
(url_or_path, id),
|
||||||
|
)
|
||||||
|
|
||||||
|
res = c.fetchone()
|
||||||
|
c.close()
|
||||||
|
if res:
|
||||||
|
return ATRIVision(*res)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_conversations(
|
||||||
|
self, page: int = 1, page_size: int = 20
|
||||||
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
"""获取所有对话,支持分页,按更新时间降序排序"""
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取总记录数
|
||||||
|
c.execute("""
|
||||||
|
SELECT COUNT(*) FROM webchat_conversation
|
||||||
|
""")
|
||||||
|
total_count = c.fetchone()[0]
|
||||||
|
|
||||||
|
# 计算偏移量
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
# 获取分页数据,按更新时间降序排序
|
||||||
|
c.execute(
|
||||||
|
"""
|
||||||
|
SELECT user_id, cid, created_at, updated_at, title, persona_id
|
||||||
|
FROM webchat_conversation
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""",
|
||||||
|
(page_size, offset),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = c.fetchall()
|
||||||
|
|
||||||
|
conversations = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
user_id, cid, created_at, updated_at, title, persona_id = row
|
||||||
|
# 确保 cid 是字符串类型且至少有8个字符,否则使用一个默认值
|
||||||
|
safe_cid = str(cid) if cid else "unknown"
|
||||||
|
display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid
|
||||||
|
|
||||||
|
conversations.append(
|
||||||
|
{
|
||||||
|
"user_id": user_id or "",
|
||||||
|
"cid": safe_cid,
|
||||||
|
"title": title or f"对话 {display_cid}",
|
||||||
|
"persona_id": persona_id or "",
|
||||||
|
"created_at": created_at or 0,
|
||||||
|
"updated_at": updated_at or 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return conversations, total_count
|
||||||
|
|
||||||
|
except Exception as _:
|
||||||
|
# 返回空列表和0,确保即使出错也有有效的返回值
|
||||||
|
return [], 0
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
def get_filtered_conversations(
|
||||||
|
self,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
platforms: List[str] = None,
|
||||||
|
message_types: List[str] = None,
|
||||||
|
search_query: str = None,
|
||||||
|
exclude_ids: List[str] = None,
|
||||||
|
exclude_platforms: List[str] = None,
|
||||||
|
) -> Tuple[List[Dict[str, Any]], int]:
|
||||||
|
"""获取筛选后的对话列表"""
|
||||||
|
try:
|
||||||
|
c = self.conn.cursor()
|
||||||
|
except sqlite3.ProgrammingError:
|
||||||
|
c = self._get_conn(self.db_path).cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 构建查询条件
|
||||||
|
where_clauses = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
# 平台筛选
|
||||||
|
if platforms and len(platforms) > 0:
|
||||||
|
platform_conditions = []
|
||||||
|
for platform in platforms:
|
||||||
|
platform_conditions.append("user_id LIKE ?")
|
||||||
|
params.append(f"{platform}:%")
|
||||||
|
|
||||||
|
if platform_conditions:
|
||||||
|
where_clauses.append(f"({' OR '.join(platform_conditions)})")
|
||||||
|
|
||||||
|
# 消息类型筛选
|
||||||
|
if message_types and len(message_types) > 0:
|
||||||
|
message_type_conditions = []
|
||||||
|
for msg_type in message_types:
|
||||||
|
message_type_conditions.append("user_id LIKE ?")
|
||||||
|
params.append(f"%:{msg_type}:%")
|
||||||
|
|
||||||
|
if message_type_conditions:
|
||||||
|
where_clauses.append(f"({' OR '.join(message_type_conditions)})")
|
||||||
|
|
||||||
|
# 搜索关键词
|
||||||
|
if search_query:
|
||||||
|
search_query = search_query.encode("unicode_escape").decode("utf-8")
|
||||||
|
where_clauses.append(
|
||||||
|
"(title LIKE ? OR user_id LIKE ? OR cid LIKE ? OR history LIKE ?)"
|
||||||
|
)
|
||||||
|
search_param = f"%{search_query}%"
|
||||||
|
params.extend([search_param, search_param, search_param, search_param])
|
||||||
|
|
||||||
|
# 排除特定用户ID
|
||||||
|
if exclude_ids and len(exclude_ids) > 0:
|
||||||
|
for exclude_id in exclude_ids:
|
||||||
|
where_clauses.append("user_id NOT LIKE ?")
|
||||||
|
params.append(f"{exclude_id}%")
|
||||||
|
|
||||||
|
# 排除特定平台
|
||||||
|
if exclude_platforms and len(exclude_platforms) > 0:
|
||||||
|
for exclude_platform in exclude_platforms:
|
||||||
|
where_clauses.append("user_id NOT LIKE ?")
|
||||||
|
params.append(f"{exclude_platform}:%")
|
||||||
|
|
||||||
|
# 构建完整的 WHERE 子句
|
||||||
|
where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
|
||||||
|
|
||||||
|
# 构建计数查询
|
||||||
|
count_sql = f"SELECT COUNT(*) FROM webchat_conversation{where_sql}"
|
||||||
|
|
||||||
|
# 获取总记录数
|
||||||
|
c.execute(count_sql, params)
|
||||||
|
total_count = c.fetchone()[0]
|
||||||
|
|
||||||
|
# 计算偏移量
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
# 构建分页数据查询
|
||||||
|
data_sql = f"""
|
||||||
|
SELECT user_id, cid, created_at, updated_at, title, persona_id
|
||||||
|
FROM webchat_conversation
|
||||||
|
{where_sql}
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
"""
|
||||||
|
query_params = params + [page_size, offset]
|
||||||
|
|
||||||
|
# 获取分页数据
|
||||||
|
c.execute(data_sql, query_params)
|
||||||
|
rows = c.fetchall()
|
||||||
|
|
||||||
|
conversations = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
user_id, cid, created_at, updated_at, title, persona_id = row
|
||||||
|
# 确保 cid 是字符串类型,否则使用一个默认值
|
||||||
|
safe_cid = str(cid) if cid else "unknown"
|
||||||
|
display_cid = safe_cid[:8] if len(safe_cid) >= 8 else safe_cid
|
||||||
|
|
||||||
|
conversations.append(
|
||||||
|
{
|
||||||
|
"user_id": user_id or "",
|
||||||
|
"cid": safe_cid,
|
||||||
|
"title": title or f"对话 {display_cid}",
|
||||||
|
"persona_id": persona_id or "",
|
||||||
|
"created_at": created_at or 0,
|
||||||
|
"updated_at": updated_at or 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return conversations, total_count
|
||||||
|
|
||||||
|
except Exception as _:
|
||||||
|
# 返回空列表和0,确保即使出错也有有效的返回值
|
||||||
|
return [], 0
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
50
astrbot/core/db/sqlite_init.sql
Normal file
50
astrbot/core/db/sqlite_init.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS platform(
|
||||||
|
name VARCHAR(32),
|
||||||
|
count INTEGER,
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS llm(
|
||||||
|
name VARCHAR(32),
|
||||||
|
count INTEGER,
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS plugin(
|
||||||
|
name VARCHAR(32),
|
||||||
|
count INTEGER,
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS command(
|
||||||
|
name VARCHAR(32),
|
||||||
|
count INTEGER,
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS llm_history(
|
||||||
|
provider_type VARCHAR(32),
|
||||||
|
session_id VARCHAR(32),
|
||||||
|
content TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ATRI
|
||||||
|
CREATE TABLE IF NOT EXISTS atri_vision(
|
||||||
|
id TEXT,
|
||||||
|
url_or_path TEXT,
|
||||||
|
caption TEXT,
|
||||||
|
is_meme BOOLEAN,
|
||||||
|
keywords TEXT,
|
||||||
|
platform_name VARCHAR(32),
|
||||||
|
session_id VARCHAR(32),
|
||||||
|
sender_nickname VARCHAR(32),
|
||||||
|
timestamp INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS webchat_conversation(
|
||||||
|
user_id TEXT, -- 会话 id
|
||||||
|
cid TEXT, -- 对话 id
|
||||||
|
history TEXT,
|
||||||
|
created_at INTEGER,
|
||||||
|
updated_at INTEGER,
|
||||||
|
title TEXT,
|
||||||
|
persona_id TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
PRAGMA encoding = 'UTF-8';
|
||||||
46
astrbot/core/db/vec_db/base.py
Normal file
46
astrbot/core/db/vec_db/base.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import abc
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
similarity: float
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVecDB:
|
||||||
|
async def initialize(self):
|
||||||
|
"""
|
||||||
|
初始化向量数据库
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def insert(self, content: str, metadata: dict = None, id: str = None) -> int:
|
||||||
|
"""
|
||||||
|
插入一条文本和其对应向量,自动生成 ID 并保持一致性。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def retrieve(self, query: str, top_k: int = 5) -> list[Result]:
|
||||||
|
"""
|
||||||
|
搜索最相似的文档。
|
||||||
|
Args:
|
||||||
|
query (str): 查询文本
|
||||||
|
top_k (int): 返回的最相似文档的数量
|
||||||
|
Returns:
|
||||||
|
List[Result]: 查询结果
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete(self, doc_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
删除指定文档。
|
||||||
|
Args:
|
||||||
|
doc_id (str): 要删除的文档 ID
|
||||||
|
Returns:
|
||||||
|
bool: 删除是否成功
|
||||||
|
"""
|
||||||
|
...
|
||||||
3
astrbot/core/db/vec_db/faiss_impl/__init__.py
Normal file
3
astrbot/core/db/vec_db/faiss_impl/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .vec_db import FaissVecDB
|
||||||
|
|
||||||
|
__all__ = ["FaissVecDB"]
|
||||||
121
astrbot/core/db/vec_db/faiss_impl/document_storage.py
Normal file
121
astrbot/core/db/vec_db/faiss_impl/document_storage.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import aiosqlite
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentStorage:
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
self.db_path = db_path
|
||||||
|
self.connection = None
|
||||||
|
self.sqlite_init_path = os.path.join(
|
||||||
|
os.path.dirname(__file__), "sqlite_init.sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""Initialize the SQLite database and create the documents table if it doesn't exist."""
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
await self.connect()
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
with open(self.sqlite_init_path, "r", encoding="utf-8") as f:
|
||||||
|
sql_script = f.read()
|
||||||
|
await cursor.executescript(sql_script)
|
||||||
|
await self.connection.commit()
|
||||||
|
else:
|
||||||
|
await self.connect()
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
"""Connect to the SQLite database."""
|
||||||
|
self.connection = await aiosqlite.connect(self.db_path)
|
||||||
|
|
||||||
|
async def get_documents(self, metadata_filters: dict, ids: list = None):
|
||||||
|
"""Retrieve documents by metadata filters and ids.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata_filters (dict): The metadata filters to apply.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The list of document IDs(primary key, not doc_id) that match the filters.
|
||||||
|
"""
|
||||||
|
# metadata filter -> SQL WHERE clause
|
||||||
|
where_clauses = []
|
||||||
|
values = []
|
||||||
|
for key, val in metadata_filters.items():
|
||||||
|
where_clauses.append(f"json_extract(metadata, '$.{key}') = ?")
|
||||||
|
values.append(val)
|
||||||
|
if ids is not None and len(ids) > 0:
|
||||||
|
ids = [str(i) for i in ids if i != -1]
|
||||||
|
where_clauses.append("id IN ({})".format(",".join("?" * len(ids))))
|
||||||
|
values.extend(ids)
|
||||||
|
where_sql = " AND ".join(where_clauses) or "1=1"
|
||||||
|
|
||||||
|
result = []
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
sql = "SELECT * FROM documents WHERE " + where_sql
|
||||||
|
await cursor.execute(sql, values)
|
||||||
|
for row in await cursor.fetchall():
|
||||||
|
result.append(await self.tuple_to_dict(row))
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_document_by_doc_id(self, doc_id: str):
|
||||||
|
"""Retrieve a document by its doc_id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc_id (str): The doc_id of the document to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The document data.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT * FROM documents WHERE doc_id = ?", (doc_id,))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return await self.tuple_to_dict(row)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_document_by_doc_id(self, doc_id: str, new_text: str):
|
||||||
|
"""Retrieve a document by its doc_id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc_id (str): The doc_id.
|
||||||
|
new_text (str): The new text to update the document with.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"UPDATE documents SET text = ? WHERE doc_id = ?", (new_text, doc_id)
|
||||||
|
)
|
||||||
|
await self.connection.commit()
|
||||||
|
|
||||||
|
async def get_user_ids(self) -> list[str]:
|
||||||
|
"""Retrieve all user IDs from the documents table.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of user IDs.
|
||||||
|
"""
|
||||||
|
async with self.connection.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT DISTINCT user_id FROM documents")
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
return [row[0] for row in rows]
|
||||||
|
|
||||||
|
async def tuple_to_dict(self, row):
|
||||||
|
"""Convert a tuple to a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
row (tuple): The row to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The converted dictionary.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": row[0],
|
||||||
|
"doc_id": row[1],
|
||||||
|
"text": row[2],
|
||||||
|
"metadata": row[3],
|
||||||
|
"created_at": row[4],
|
||||||
|
"updated_at": row[5],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close the connection to the SQLite database."""
|
||||||
|
if self.connection:
|
||||||
|
await self.connection.close()
|
||||||
|
self.connection = None
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user