mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-26 03:44:58 +08:00
Compare commits
1043 Commits
noReleaseP
...
v2.6.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a10d3213fd | ||
|
|
f52a0eb02f | ||
|
|
1ea8da69a2 | ||
|
|
5bbc38a7a3 | ||
|
|
aa433bd5ab | ||
|
|
2c5933da0b | ||
|
|
77bc6fbf59 | ||
|
|
701cb7be40 | ||
|
|
ab8d77c968 | ||
|
|
6c03fe678a | ||
|
|
41b30238c3 | ||
|
|
aa768459c0 | ||
|
|
28014512f7 | ||
|
|
f9a99eed66 | ||
|
|
461b574e09 | ||
|
|
36c192ff6b | ||
|
|
101625965c | ||
|
|
83177a3416 | ||
|
|
c3904786e1 | ||
|
|
b31c34905a | ||
|
|
41cbe91870 | ||
|
|
872b16b779 | ||
|
|
9f3cc9c293 | ||
|
|
2d148c4970 | ||
|
|
0869b57741 | ||
|
|
af225aa18f | ||
|
|
06f3c5d32b | ||
|
|
4e71a08b57 | ||
|
|
bf5ebc9245 | ||
|
|
fba81582ab | ||
|
|
b4645168f9 | ||
|
|
d00c68e329 | ||
|
|
cb636b96bf | ||
|
|
12468b5b15 | ||
|
|
6a5414b5fd | ||
|
|
db51fd0ad7 | ||
|
|
256bc4dc1e | ||
|
|
d2bd6e23b6 | ||
|
|
bb12b48887 | ||
|
|
a58e55daf3 | ||
|
|
23a05fe5b0 | ||
|
|
3a63630068 | ||
|
|
565066bbcd | ||
|
|
c10f72cf4c | ||
|
|
af8c21f3d4 | ||
|
|
6f6c3af302 | ||
|
|
61a47808c8 | ||
|
|
e02765bf95 | ||
|
|
b69f193a3e | ||
|
|
7c6526d1ea | ||
|
|
b8776fba65 | ||
|
|
38357dd68d | ||
|
|
d1c2453310 | ||
|
|
ebc1ac50c6 | ||
|
|
892610872f | ||
|
|
a990a40850 | ||
|
|
3f29464dbd | ||
|
|
998d07f3b4 | ||
|
|
949bc6268c | ||
|
|
2c03e5a77e | ||
|
|
aad62dfa6f | ||
|
|
08e27d07ea | ||
|
|
1fddd244e5 | ||
|
|
d85b4b1cf0 | ||
|
|
09fca2c292 | ||
|
|
feda3d18fb | ||
|
|
eb6e5d0756 | ||
|
|
7386daad28 | ||
|
|
3f290b2e1a | ||
|
|
43519ffe80 | ||
|
|
c8bb3d612a | ||
|
|
bc48b7e623 | ||
|
|
d59d5797f6 | ||
|
|
11d3c1e650 | ||
|
|
8cfd9e6694 | ||
|
|
d3f401c54d | ||
|
|
a889170d1a | ||
|
|
459e9f9322 | ||
|
|
707afdcdf9 | ||
|
|
ad1cf379c4 | ||
|
|
582277fe2d | ||
|
|
14b9f814c7 | ||
|
|
b11e5d99b0 | ||
|
|
9590718da4 | ||
|
|
8c2b53cffb | ||
|
|
5a85c073a8 | ||
|
|
2d2fbd0a8b | ||
|
|
1b25a05122 | ||
|
|
709cc1140b | ||
|
|
1730962636 | ||
|
|
a1de4f6f7a | ||
|
|
a5ccda5ed6 | ||
|
|
f035e654ba | ||
|
|
151d3e9f66 | ||
|
|
c79207e197 | ||
|
|
f9d461d9a1 | ||
|
|
3e17bbb90f | ||
|
|
549a7eff7f | ||
|
|
db2e366014 | ||
|
|
26e4215054 | ||
|
|
5f07ff8145 | ||
|
|
e396ba4649 | ||
|
|
d1dff6dedd | ||
|
|
419354cb07 | ||
|
|
7708eaa82c | ||
|
|
9fccf84987 | ||
|
|
0f59788184 | ||
|
|
0ad52bcd3f | ||
|
|
d7d710ec07 | ||
|
|
75a9a3e9af | ||
|
|
70503bedb7 | ||
|
|
7890eac3f8 | ||
|
|
e15f3595b3 | ||
|
|
eebd6a6ba3 | ||
|
|
0407f3e4ac | ||
|
|
5abca84437 | ||
|
|
d2776cc1e6 | ||
|
|
9fe0ee2b77 | ||
|
|
b68daac323 | ||
|
|
665de5dc43 | ||
|
|
e3b280758c | ||
|
|
374ae25d9c | ||
|
|
c86529ac99 | ||
|
|
6309f1fb78 | ||
|
|
c246fb6d8e | ||
|
|
ec6c041bcf | ||
|
|
2da5a9f3c7 | ||
|
|
4e0df52d7c | ||
|
|
71b8bf13e4 | ||
|
|
a8b1e6ce91 | ||
|
|
1419d7611d | ||
|
|
89c83ebf20 | ||
|
|
76d7db88ea | ||
|
|
67a208bc90 | ||
|
|
acbd55ded2 | ||
|
|
11a240a6d1 | ||
|
|
97c85abbe7 | ||
|
|
06a0cd2a3d | ||
|
|
572b215df8 | ||
|
|
2c542bf412 | ||
|
|
1576ba7a01 | ||
|
|
45e4096a12 | ||
|
|
8a1d4fe287 | ||
|
|
98f880ebc2 | ||
|
|
2b852853f3 | ||
|
|
c7a9988033 | ||
|
|
c475eebe1c | ||
|
|
0fe7355ae0 | ||
|
|
57de96e3a2 | ||
|
|
70571cef50 | ||
|
|
0b6deb3340 | ||
|
|
dcda85a825 | ||
|
|
9d3bff018b | ||
|
|
051376e0d2 | ||
|
|
a113785211 | ||
|
|
3f4ed4dc3c | ||
|
|
ac80764fae | ||
|
|
e43afd4891 | ||
|
|
f1aea1d495 | ||
|
|
0e2a5db104 | ||
|
|
3a4c9771fa | ||
|
|
f4f8ef9523 | ||
|
|
b9ace69a72 | ||
|
|
aef0b2a26e | ||
|
|
f7712d71ec | ||
|
|
e94b44e3b8 | ||
|
|
524e863c78 | ||
|
|
bbc80ac901 | ||
|
|
f969ddd6ca | ||
|
|
1cc9781333 | ||
|
|
a609801bae | ||
|
|
d8b606d372 | ||
|
|
572a440e65 | ||
|
|
6e4eeae9b7 | ||
|
|
1a73669df8 | ||
|
|
91ebaf1122 | ||
|
|
46703eb906 | ||
|
|
b9dd9d5193 | ||
|
|
884481a4ec | ||
|
|
9040b37a63 | ||
|
|
99d47b2fa2 | ||
|
|
6575359a94 | ||
|
|
a2fc726372 | ||
|
|
3bfce8ab51 | ||
|
|
ff9a9830f2 | ||
|
|
e2b59e8efe | ||
|
|
04dad9757f | ||
|
|
75ea1080ad | ||
|
|
e25b064319 | ||
|
|
5d0dbc40ce | ||
|
|
beae8de5eb | ||
|
|
c4ff30c722 | ||
|
|
6f4ecb101b | ||
|
|
9f9b0ef846 | ||
|
|
de6957062c | ||
|
|
0a9b43e6fa | ||
|
|
5b0edd9937 | ||
|
|
8a400d202a | ||
|
|
5a1e9f7fb2 | ||
|
|
e03af75cf8 | ||
|
|
0da4919255 | ||
|
|
914e566d1f | ||
|
|
6ec2b653fe | ||
|
|
ba0a088b9c | ||
|
|
478e83bcd9 | ||
|
|
386124a3b9 | ||
|
|
ff5e7c16d1 | ||
|
|
7ff7a66012 | ||
|
|
c99dfb8a86 | ||
|
|
10f9d4c6b3 | ||
|
|
d347813411 | ||
|
|
7a93898b3f | ||
|
|
c057ea900f | ||
|
|
512266e74f | ||
|
|
e36aee11c7 | ||
|
|
97421299f5 | ||
|
|
bc41e5aa80 | ||
|
|
2fa30e7def | ||
|
|
1c6a7d9ba5 | ||
|
|
47435c42a5 | ||
|
|
39a1b421e6 | ||
|
|
b5edf2295b | ||
|
|
fb650a3d7a | ||
|
|
521541f311 | ||
|
|
7020abadbf | ||
|
|
d95fb3b5be | ||
|
|
3e524dc790 | ||
|
|
a64940bff8 | ||
|
|
c739290f0b | ||
|
|
af292fe050 | ||
|
|
634c7fb302 | ||
|
|
33efb94013 | ||
|
|
549e4dc02e | ||
|
|
3d40909c02 | ||
|
|
1aef81e38f | ||
|
|
1b0ae8da58 | ||
|
|
7979a8e97f | ||
|
|
080e53d9a9 | ||
|
|
89bb364b16 | ||
|
|
3586cd941f | ||
|
|
054d0839ac | ||
|
|
dd75f98d85 | ||
|
|
ec23bb5268 | ||
|
|
bc99db4fc1 | ||
|
|
c8275fcfbf | ||
|
|
a345043c30 | ||
|
|
382d37d479 | ||
|
|
32c144a75d | ||
|
|
7ca2aa5e39 | ||
|
|
86cc4a23ac | ||
|
|
08d1e138bd | ||
|
|
a9fe86542f | ||
|
|
4e29776fcd | ||
|
|
ee3eae8f4d | ||
|
|
a84575858a | ||
|
|
ac472291c7 | ||
|
|
f304873c6a | ||
|
|
18caf8face | ||
|
|
d21115aaa8 | ||
|
|
a05ecd2e7f | ||
|
|
32a725126d | ||
|
|
0528690622 | ||
|
|
819339142e | ||
|
|
1d0573e7ff | ||
|
|
00623bc431 | ||
|
|
c872264456 | ||
|
|
1336d3cb9a | ||
|
|
d1459578cd | ||
|
|
8a67fcf40f | ||
|
|
7930370aa9 | ||
|
|
0b854bdcf1 | ||
|
|
cba6aab48d | ||
|
|
12a9ca7a77 | ||
|
|
a6cbd226e1 | ||
|
|
3577e62b41 | ||
|
|
f86e69fcd1 | ||
|
|
292e00b078 | ||
|
|
2a91497bcf | ||
|
|
b0cca0a4c2 | ||
|
|
a2bda85a9c | ||
|
|
20677cff86 | ||
|
|
c8af5d8445 | ||
|
|
2dbe984539 | ||
|
|
6b8fa664f1 | ||
|
|
2b9612e933 | ||
|
|
749d0219fb | ||
|
|
a11a152bd7 | ||
|
|
fc803a3742 | ||
|
|
13a1e15f24 | ||
|
|
3f41b94da5 | ||
|
|
0fb5bfda20 | ||
|
|
dc1fd73ebb | ||
|
|
161b694f71 | ||
|
|
45d1c89e45 | ||
|
|
e26664aa51 | ||
|
|
e29691efbd | ||
|
|
6d45327882 | ||
|
|
fbd41eef49 | ||
|
|
0a30c88322 | ||
|
|
4f5af0e8c8 | ||
|
|
df3f0fd159 | ||
|
|
f2493c79dd | ||
|
|
a86a035b6b | ||
|
|
7995793bfd | ||
|
|
a56b340646 | ||
|
|
7473cdfe16 | ||
|
|
24273ac158 | ||
|
|
fe6275000e | ||
|
|
5fbf369f82 | ||
|
|
4400475ffa | ||
|
|
796eb7c95d | ||
|
|
89a01378e7 | ||
|
|
f4735e5e30 | ||
|
|
f1bb3045aa | ||
|
|
96e474a555 | ||
|
|
833d29b101 | ||
|
|
dce6734ba2 | ||
|
|
0481167dc6 | ||
|
|
a002f93f7b | ||
|
|
3c894fe70e | ||
|
|
8c69b8a1d9 | ||
|
|
a9dae05303 | ||
|
|
ae6994e241 | ||
|
|
caa72fa40c | ||
|
|
46cc9220c3 | ||
|
|
ddb56d7a8e | ||
|
|
a0267416d7 | ||
|
|
56e1ef3602 | ||
|
|
b4fc1057d1 | ||
|
|
06037df607 | ||
|
|
dce134d08d | ||
|
|
cca471d068 | ||
|
|
ddb211b74a | ||
|
|
cef70751ff | ||
|
|
2d2219fc6e | ||
|
|
514a6b4192 | ||
|
|
7a552b3434 | ||
|
|
ecebd1b0e0 | ||
|
|
8dc34d2a88 | ||
|
|
d52644ceec | ||
|
|
3052510591 | ||
|
|
777a5617db | ||
|
|
e17c1087e9 | ||
|
|
633695175a | ||
|
|
9e78bf3d21 | ||
|
|
43aa68a55d | ||
|
|
b8308f8c57 | ||
|
|
466bfbddeb | ||
|
|
b6da07b225 | ||
|
|
2f2159239a | ||
|
|
67d1ca8a65 | ||
|
|
497a393e83 | ||
|
|
782c0e22ea | ||
|
|
2932fc6dfd | ||
|
|
0a9eab2113 | ||
|
|
50a673a8ec | ||
|
|
9e25d0f9e4 | ||
|
|
23cd7be711 | ||
|
|
025b9e33f1 | ||
|
|
bab2f64913 | ||
|
|
b00e09aa9c | ||
|
|
0b109fdc7a | ||
|
|
018fea2ddb | ||
|
|
f8a3cc4352 | ||
|
|
6ab853acc1 | ||
|
|
e825dea02f | ||
|
|
cf8740d16e | ||
|
|
9c4809e26f | ||
|
|
0a232fd9ef | ||
|
|
23016a0791 | ||
|
|
cdcc67ff23 | ||
|
|
92274bfc34 | ||
|
|
2fed6f61ba | ||
|
|
59b2cd26d2 | ||
|
|
f7b87e99d2 | ||
|
|
70bc985145 | ||
|
|
070dbe9108 | ||
|
|
a63fa6d955 | ||
|
|
c7703809b0 | ||
|
|
37eb74338f | ||
|
|
77d5585b7c | ||
|
|
6cab3ef029 | ||
|
|
820a7b78fc | ||
|
|
c51dffef3a | ||
|
|
983bc3da3c | ||
|
|
09be956a58 | ||
|
|
5eded50c53 | ||
|
|
6d8eebd314 | ||
|
|
19a0572b5f | ||
|
|
6272e98474 | ||
|
|
45042fe7d4 | ||
|
|
d85e840126 | ||
|
|
804889f1de | ||
|
|
919c996434 | ||
|
|
00823b3d62 | ||
|
|
af54efd24a | ||
|
|
b1c9b121f6 | ||
|
|
7b5649d153 | ||
|
|
52bf716d84 | ||
|
|
c149dd7b66 | ||
|
|
65d5a1ed63 | ||
|
|
5516754bbb | ||
|
|
08082f2ee3 | ||
|
|
8489266080 | ||
|
|
51c7e0b235 | ||
|
|
628b6b0bb4 | ||
|
|
7e024d860d | ||
|
|
c2f6273f70 | ||
|
|
96e401ec7b | ||
|
|
ae8ac65447 | ||
|
|
2d4f59f36e | ||
|
|
0e85467e02 | ||
|
|
eb41cf5481 | ||
|
|
b970a42d07 | ||
|
|
8c9d123e1c | ||
|
|
ab2a95e347 | ||
|
|
2184c558a4 | ||
|
|
83cb8588fd | ||
|
|
007e82c533 | ||
|
|
499f8580a7 | ||
|
|
a7dc3c5dab | ||
|
|
d01d3a3c53 | ||
|
|
580e062dbf | ||
|
|
c8cee8410c | ||
|
|
6bf331c2e3 | ||
|
|
4c4930737c | ||
|
|
9de01e9525 | ||
|
|
c6a16f5974 | ||
|
|
253ef44d17 | ||
|
|
15a1f00b73 | ||
|
|
b5fa2ea8b8 | ||
|
|
449e024771 | ||
|
|
1bee7a146b | ||
|
|
270a632789 | ||
|
|
418bb05b4c | ||
|
|
052b834151 | ||
|
|
58ee204a75 | ||
|
|
0a02ee8c04 | ||
|
|
950ef4a181 | ||
|
|
7b7cdd8adb | ||
|
|
471768e760 | ||
|
|
c7517d31a4 | ||
|
|
7d10d0398e | ||
|
|
a2bc25c08b | ||
|
|
3cb49fe2d8 | ||
|
|
5b96ac122f | ||
|
|
612033f478 | ||
|
|
48ee940d8e | ||
|
|
e74df0b37d | ||
|
|
640afdc49c | ||
|
|
6b39df5b9b | ||
|
|
e7e698765e | ||
|
|
43fea13dab | ||
|
|
bc899e5bd0 | ||
|
|
160086feb9 | ||
|
|
016391c976 | ||
|
|
91746448a3 | ||
|
|
5cb0543237 | ||
|
|
fac29a24a8 | ||
|
|
4d3a2a21d0 | ||
|
|
6d4f88041c | ||
|
|
18587d3690 | ||
|
|
423090dccd | ||
|
|
78e88baab3 | ||
|
|
6a276767b3 | ||
|
|
2cb26c7c70 | ||
|
|
ff66c88060 | ||
|
|
611e82b8f9 | ||
|
|
59bdee7137 | ||
|
|
e8dbd426ae | ||
|
|
40d6e809a0 | ||
|
|
236c540d18 | ||
|
|
d6ca059f6c | ||
|
|
52c06a60ca | ||
|
|
6353644ec3 | ||
|
|
20df9ded3d | ||
|
|
7569b18a4c | ||
|
|
b9da4f4951 | ||
|
|
89b9e29257 | ||
|
|
d605de9de4 | ||
|
|
d46c94d7c3 | ||
|
|
2db9c00530 | ||
|
|
66d8d159f9 | ||
|
|
9fa1446284 | ||
|
|
b3e4cb48c7 | ||
|
|
0bca7b2247 | ||
|
|
7812e03c9d | ||
|
|
7a852ae5af | ||
|
|
706d9e61c1 | ||
|
|
8f0ed4ff4b | ||
|
|
3415b6f121 | ||
|
|
256ba6fb86 | ||
|
|
d30b2b9afe | ||
|
|
be943ca1fc | ||
|
|
1ddab2a97a | ||
|
|
e15fd4695c | ||
|
|
ffa4b1b4a1 | ||
|
|
f8eee3a2a6 | ||
|
|
eeee7a8343 | ||
|
|
8447b73fcb | ||
|
|
2863945d5f | ||
|
|
cb1f8ca6f7 | ||
|
|
1d9964bcb1 | ||
|
|
15cb8016d3 | ||
|
|
895cc0a2c5 | ||
|
|
20bf349e4e | ||
|
|
e297763da1 | ||
|
|
e471970654 | ||
|
|
12faaaced8 | ||
|
|
083cbc55cc | ||
|
|
8aa7a3273d | ||
|
|
255e2c4385 | ||
|
|
9856306870 | ||
|
|
527ab8b8a7 | ||
|
|
f8e19ba9b3 | ||
|
|
7649dbfbbc | ||
|
|
81e734644d | ||
|
|
ae55cf5b1e | ||
|
|
af539546ef | ||
|
|
0031ce57d0 | ||
|
|
2f48a2ce57 | ||
|
|
6068ab7100 | ||
|
|
29a7dccef4 | ||
|
|
e2073da86e | ||
|
|
ae079526f7 | ||
|
|
947bae8e26 | ||
|
|
a68e29dff6 | ||
|
|
a588d7f960 | ||
|
|
66224e5a32 | ||
|
|
07abad6a14 | ||
|
|
83d02aaaac | ||
|
|
5a27ac165e | ||
|
|
bd9a523233 | ||
|
|
43959b158f | ||
|
|
d81b457bba | ||
|
|
b40d639785 | ||
|
|
0a8d8f4f66 | ||
|
|
d16cb25cde | ||
|
|
7aef1758e0 | ||
|
|
9758756fdd | ||
|
|
13ef35f96f | ||
|
|
6b8c1209b7 | ||
|
|
7184f3053a | ||
|
|
b83eac10e6 | ||
|
|
cb42eaef69 | ||
|
|
0dfd636a7e | ||
|
|
21ff0fd258 | ||
|
|
c2eaeb2c72 | ||
|
|
2a414a4bea | ||
|
|
fc0c38c8af | ||
|
|
595e6c8a0c | ||
|
|
ced16fd221 | ||
|
|
0817c3f148 | ||
|
|
fb40af81ac | ||
|
|
1c5ad05e89 | ||
|
|
86bef566c4 | ||
|
|
0983ccb61e | ||
|
|
a1d9f469c0 | ||
|
|
952124f783 | ||
|
|
6be12e8ace | ||
|
|
0799f380e1 | ||
|
|
f65270ee7e | ||
|
|
414910719c | ||
|
|
10a1e8faa6 | ||
|
|
4eea21927e | ||
|
|
48c7f659f9 | ||
|
|
b33333f4aa | ||
|
|
9edb32b081 | ||
|
|
c9b25fe806 | ||
|
|
b6ee3939be | ||
|
|
e5485cddd0 | ||
|
|
ac81597236 | ||
|
|
58d991df0a | ||
|
|
3f8e380da4 | ||
|
|
ae831a2654 | ||
|
|
ae72cf2283 | ||
|
|
8164f4b506 | ||
|
|
9617be0ca4 | ||
|
|
f079d7b9fa | ||
|
|
00afda452f | ||
|
|
70386abadd | ||
|
|
5865ac017c | ||
|
|
4061a92f8e | ||
|
|
d37c31b31c | ||
|
|
973ef0078f | ||
|
|
48dcd257da | ||
|
|
da03911610 | ||
|
|
aba9d945b5 | ||
|
|
b6f7f3b73f | ||
|
|
2050d20ea7 | ||
|
|
ac1fb4a63a | ||
|
|
ced38490e1 | ||
|
|
ad28b69198 | ||
|
|
8c67d3c58f | ||
|
|
7171817de8 | ||
|
|
73f9d674e1 | ||
|
|
5e046399f8 | ||
|
|
4966cd9ac7 | ||
|
|
da936ecfe3 | ||
|
|
89e10d43de | ||
|
|
3bf289af69 | ||
|
|
c7c9a6c5ca | ||
|
|
aee8446a23 | ||
|
|
2bb4f1fbb8 | ||
|
|
6e7b0ee4ff | ||
|
|
204f5b9a54 | ||
|
|
8c41e3506f | ||
|
|
c2c33e45b8 | ||
|
|
1acaf4e58b | ||
|
|
eca80d5a4c | ||
|
|
f538957be9 | ||
|
|
82a839a60a | ||
|
|
df494da9e4 | ||
|
|
1ea53f7f04 | ||
|
|
ac6d695f6d | ||
|
|
73dccb21f5 | ||
|
|
4221102ad5 | ||
|
|
b100f12e7f | ||
|
|
2069ba6836 | ||
|
|
ea57976808 | ||
|
|
4055d3542b | ||
|
|
0b0271a1f4 | ||
|
|
e03585ad4d | ||
|
|
11a385791e | ||
|
|
e228225178 | ||
|
|
1c96d971e1 | ||
|
|
b799de7995 | ||
|
|
b01d246555 | ||
|
|
9363b073cf | ||
|
|
12ca04ac6f | ||
|
|
51737c28bd | ||
|
|
50d5ec224a | ||
|
|
95a7397d14 | ||
|
|
aedac6d22c | ||
|
|
d522975ecc | ||
|
|
68fda8d7f3 | ||
|
|
b0cfec9913 | ||
|
|
ba8eba1581 | ||
|
|
f9eaed41c1 | ||
|
|
1202a62df7 | ||
|
|
8c1f7796f6 | ||
|
|
42aee35789 | ||
|
|
b628849caa | ||
|
|
031f08b0d4 | ||
|
|
fab6f9b93f | ||
|
|
564c5d937d | ||
|
|
2d3bb01487 | ||
|
|
607ea2d293 | ||
|
|
d817b53780 | ||
|
|
e8a2cbe06a | ||
|
|
d2b0577752 | ||
|
|
b4edd5cbad | ||
|
|
348477747e | ||
|
|
bb7ee174ea | ||
|
|
ab5add14ef | ||
|
|
44f4820cee | ||
|
|
8f1609b944 | ||
|
|
66b5b75631 | ||
|
|
17e293afe8 | ||
|
|
1cf35f59fd | ||
|
|
bb4b897934 | ||
|
|
0eaf1af2e3 | ||
|
|
f70c12540b | ||
|
|
479fe73c24 | ||
|
|
f6cad85476 | ||
|
|
888197e6ce | ||
|
|
e634305759 | ||
|
|
fe054211f4 | ||
|
|
f102a29ea0 | ||
|
|
2b8bd45bcd | ||
|
|
7f730c4be0 | ||
|
|
b6e31cac23 | ||
|
|
9fe4f218d5 | ||
|
|
cc38cc2676 | ||
|
|
f56c6876d1 | ||
|
|
196e424c88 | ||
|
|
9270dc2c52 | ||
|
|
14aec251b4 | ||
|
|
d2a7a57245 | ||
|
|
1964fc76c8 | ||
|
|
b8d4b490ce | ||
|
|
76891e4855 | ||
|
|
3d868b3a39 | ||
|
|
7b56bcf7a9 | ||
|
|
f96ae56bce | ||
|
|
d52108f4e1 | ||
|
|
5f07b7ad1f | ||
|
|
cda10cf1a6 | ||
|
|
d226b8ebc5 | ||
|
|
d08794579c | ||
|
|
7450494741 | ||
|
|
36dca7ae2f | ||
|
|
5dae777e79 | ||
|
|
e518d172d7 | ||
|
|
af29277acd | ||
|
|
79bfa0792d | ||
|
|
cf23c5d31c | ||
|
|
84418a296b | ||
|
|
5f83cc6bb7 | ||
|
|
cde168c93c | ||
|
|
fed24c0748 | ||
|
|
b45d11b3c3 | ||
|
|
84d9af69bb | ||
|
|
684d356646 | ||
|
|
975300c9fc | ||
|
|
ca349e33fc | ||
|
|
ccf62fe95c | ||
|
|
d056cb6769 | ||
|
|
b0016eebf9 | ||
|
|
0490ad9207 | ||
|
|
4a20ae236b | ||
|
|
9be1c7fc6f | ||
|
|
5621d32b30 | ||
|
|
b7642fe876 | ||
|
|
c842485d33 | ||
|
|
341444ef1c | ||
|
|
66f5a219d2 | ||
|
|
cf678aa345 | ||
|
|
d1549b3df0 | ||
|
|
002919fffe | ||
|
|
087d097204 | ||
|
|
ca4eeda6f0 | ||
|
|
94543a4708 | ||
|
|
d4738dfb46 | ||
|
|
3bdf6810aa | ||
|
|
f489c2f3b4 | ||
|
|
a724bfe155 | ||
|
|
179a372bfe | ||
|
|
651d765ab0 | ||
|
|
7ddc853f63 | ||
|
|
1bd1bfc725 | ||
|
|
f6ec0fda7a | ||
|
|
7be368ae8c | ||
|
|
f67db2617b | ||
|
|
ed5bf8100f | ||
|
|
0ef8a1c9ae | ||
|
|
32460cbf78 | ||
|
|
6f6c9c222c | ||
|
|
438d0ed1ea | ||
|
|
3ef1c71cad | ||
|
|
aaadf6b8ba | ||
|
|
6af614f319 | ||
|
|
c75dbd67df | ||
|
|
dc3d186e2a | ||
|
|
44550feddd | ||
|
|
a0810d5f63 | ||
|
|
cfc97fb22d | ||
|
|
d67dbe8062 | ||
|
|
e89035e11c | ||
|
|
2ea711e629 | ||
|
|
a716f071be | ||
|
|
3450a91824 | ||
|
|
d2c2b457e5 | ||
|
|
9cd7e49804 | ||
|
|
e9155e836f | ||
|
|
ed248539c7 | ||
|
|
54cc75506f | ||
|
|
4269c7927e | ||
|
|
064ac7f603 | ||
|
|
48ccf15273 | ||
|
|
b920ced6d4 | ||
|
|
69610a674c | ||
|
|
1828e34190 | ||
|
|
d53f4e3917 | ||
|
|
01706d5b4e | ||
|
|
8916b8a450 | ||
|
|
ed33af5638 | ||
|
|
c94a9e1ae6 | ||
|
|
e2e93afd06 | ||
|
|
a810158d5b | ||
|
|
5a5ebb95fc | ||
|
|
61dd9e29c0 | ||
|
|
ac65d81ba1 | ||
|
|
3aca987176 | ||
|
|
7288d3cb15 | ||
|
|
7477c7c67f | ||
|
|
453952859e | ||
|
|
85d46089e3 | ||
|
|
3b55f706de | ||
|
|
f448276423 | ||
|
|
830ee704da | ||
|
|
393369e446 | ||
|
|
2cc6a09905 | ||
|
|
d7d9d88e16 | ||
|
|
357d6aaf75 | ||
|
|
8059c422e3 | ||
|
|
b336e1334d | ||
|
|
12a0942ddb | ||
|
|
7e5a77f77e | ||
|
|
e0caeb5dd2 | ||
|
|
77076f3bdd | ||
|
|
2933d4843f | ||
|
|
c5de978098 | ||
|
|
8b9cfab072 | ||
|
|
ea5f3c222f | ||
|
|
36bcbca15b | ||
|
|
2b2060e71b | ||
|
|
451688f2df | ||
|
|
d993852de7 | ||
|
|
9d73770a4e | ||
|
|
2541acf9d2 | ||
|
|
a1bfbad24e | ||
|
|
8af4918048 | ||
|
|
49f4ab0ec8 | ||
|
|
85c623fb0f | ||
|
|
9e28298250 | ||
|
|
7a04ef0985 | ||
|
|
83005e9ba9 | ||
|
|
f0c78f0529 | ||
|
|
3f638adcf9 | ||
|
|
d9405d8d5d | ||
|
|
606713a418 | ||
|
|
52102f0d0a | ||
|
|
61c29829ed | ||
|
|
df30931aad | ||
|
|
5afcc03e8b | ||
|
|
fbeb4673f4 | ||
|
|
4aba319560 | ||
|
|
74f79e002c | ||
|
|
2668ef2b3f | ||
|
|
74c018e271 | ||
|
|
64776fd601 | ||
|
|
59877bf71d | ||
|
|
d2800ac58b | ||
|
|
ffef944119 | ||
|
|
651b291ef6 | ||
|
|
e4b581f197 | ||
|
|
4f3939e2d9 | ||
|
|
1048ca612d | ||
|
|
b1a2d21ee9 | ||
|
|
dd4e8bdc8b | ||
|
|
e28c9bae0c | ||
|
|
5c10f520fb | ||
|
|
f8abe90674 | ||
|
|
964ad42cb4 | ||
|
|
424b970469 | ||
|
|
792366e221 | ||
|
|
79e970c4c3 | ||
|
|
d12acd5f31 | ||
|
|
13e55e05a4 | ||
|
|
9a7490bc2f | ||
|
|
a610a9d3d3 | ||
|
|
56e906c83f | ||
|
|
101f26e5a3 | ||
|
|
0bba205cf2 | ||
|
|
cc3beb191f | ||
|
|
42f5092bb9 | ||
|
|
bc6728d123 | ||
|
|
754278f80f | ||
|
|
c9c980b6fe | ||
|
|
a457d13d2c | ||
|
|
7440e9e5d2 | ||
|
|
39d901a5cb | ||
|
|
2e1ebff985 | ||
|
|
b8ed9ba321 | ||
|
|
c89a8e1cd1 | ||
|
|
480d201c55 | ||
|
|
a4b7d4a012 | ||
|
|
7fe676712b | ||
|
|
552733129c | ||
|
|
a4d73090f8 | ||
|
|
7d39b72800 | ||
|
|
f1e12563e9 | ||
|
|
0ac5e5b35e | ||
|
|
6b3f74a39a | ||
|
|
3c3e2e86c3 | ||
|
|
204a778db2 | ||
|
|
3594e64bfc | ||
|
|
c23d114094 | ||
|
|
6cb3fdc7c9 | ||
|
|
c57642bd4e | ||
|
|
891ee0fac8 | ||
|
|
1b69f0b668 | ||
|
|
46b310ceb9 | ||
|
|
85fe44ec92 | ||
|
|
fdcec0fbf7 | ||
|
|
2664ea8622 | ||
|
|
862724da74 | ||
|
|
a1c167fb7f | ||
|
|
adc2290fc1 | ||
|
|
8713fd8130 | ||
|
|
77df3d1ae5 | ||
|
|
2234e9db0e | ||
|
|
dd3d403de8 | ||
|
|
5364c36a79 | ||
|
|
118fbe3f7d | ||
|
|
61ec8e96f2 | ||
|
|
19289527ae | ||
|
|
77fdd6ddb8 | ||
|
|
f7830b5e9d | ||
|
|
13e5d76a44 | ||
|
|
7b8ad2e315 | ||
|
|
623f094e5b | ||
|
|
fd25d61b56 | ||
|
|
6f5802551f | ||
|
|
cbab824fd0 | ||
|
|
0c3d911e74 | ||
|
|
e161343d72 | ||
|
|
4984896c95 | ||
|
|
28d1f5ead9 | ||
|
|
5044f757fb | ||
|
|
aa28b5aead | ||
|
|
5ada507c2b | ||
|
|
48be080fe0 | ||
|
|
cc2442e761 | ||
|
|
a5560823d9 | ||
|
|
9cd313c4df | ||
|
|
173f05a8ae | ||
|
|
29819668e3 | ||
|
|
9cbb9734f2 | ||
|
|
cf897410ee | ||
|
|
bf1896f959 | ||
|
|
e7c79a5156 | ||
|
|
eb60c1b0a0 | ||
|
|
6b1b69c741 | ||
|
|
352930694a | ||
|
|
215ed7ab0e | ||
|
|
2da5883d7a | ||
|
|
b6d731cf87 | ||
|
|
bef918749d | ||
|
|
96b7674644 | ||
|
|
d0bcf6940a | ||
|
|
c35fd9c4b7 | ||
|
|
0bb5923257 | ||
|
|
41752aff60 | ||
|
|
e873d81b63 | ||
|
|
7489a11ab3 | ||
|
|
86b7a8482c | ||
|
|
731dedf155 | ||
|
|
131297d859 | ||
|
|
7ce9687702 | ||
|
|
d56163b19b | ||
|
|
e0f8a04f8e | ||
|
|
3c08741cb6 | ||
|
|
c902822723 | ||
|
|
13f31d3fae | ||
|
|
70268c0cbb | ||
|
|
04eaf9f3e9 | ||
|
|
fd57b7df18 | ||
|
|
d72c364962 | ||
|
|
618487947b | ||
|
|
e75140d732 | ||
|
|
e09f6105a1 | ||
|
|
d3a6928e3a | ||
|
|
8b2128b4dc | ||
|
|
0773490c77 | ||
|
|
c2610a32e4 | ||
|
|
aaf72de552 | ||
|
|
65664ae178 | ||
|
|
742600fc4f | ||
|
|
6531aae617 | ||
|
|
842748947f | ||
|
|
b733f8f55b | ||
|
|
b7ae1fa516 | ||
|
|
64c587c17d | ||
|
|
133d8bbeef | ||
|
|
7d3bc4203e | ||
|
|
24a10265f3 | ||
|
|
351039dc3b | ||
|
|
3ed9f1a532 | ||
|
|
99282100a0 | ||
|
|
057d8a05d7 | ||
|
|
71b69a3226 | ||
|
|
d1a5c9a090 | ||
|
|
2b20d946e6 | ||
|
|
3c96e1298c | ||
|
|
4e54c24bf0 | ||
|
|
2894309fa6 | ||
|
|
fbd53dae7c | ||
|
|
ba2c362082 | ||
|
|
680085d16f | ||
|
|
2319c7eae2 | ||
|
|
645099ecf2 | ||
|
|
d51a0a644a | ||
|
|
37abc79551 | ||
|
|
20bdc7de58 | ||
|
|
690e542f37 | ||
|
|
33f80c8d16 | ||
|
|
e01cc09a28 | ||
|
|
120ec98ba7 | ||
|
|
b4938ba1fb | ||
|
|
41d0082cee | ||
|
|
0e786660b4 | ||
|
|
6af55d8a1d | ||
|
|
2c9e7f70f2 | ||
|
|
42819daf0f | ||
|
|
08d86dbd30 | ||
|
|
32e8f08398 | ||
|
|
78c73def8a | ||
|
|
82d845b5c8 | ||
|
|
a6bda0dec7 | ||
|
|
40fd9b0579 | ||
|
|
3eda4382b2 | ||
|
|
fd2812a30b | ||
|
|
fd27a7c999 | ||
|
|
870aba0560 | ||
|
|
37153e7360 | ||
|
|
f06b16437c | ||
|
|
e582780195 | ||
|
|
df9e89deb7 | ||
|
|
b4033b2902 | ||
|
|
023ed21363 | ||
|
|
52d6721ae2 | ||
|
|
fa967c3c89 | ||
|
|
6d81821557 | ||
|
|
56664f9fbc | ||
|
|
eb1564a3dd | ||
|
|
d5c6d43ddf | ||
|
|
da5b1cf3fa | ||
|
|
4232ab6f47 | ||
|
|
78c1ad16ce | ||
|
|
9962a6ebcc | ||
|
|
36def20a07 | ||
|
|
c7689d3c89 | ||
|
|
dfa8621a1a | ||
|
|
f884313d72 | ||
|
|
7afe5f39bf | ||
|
|
01bc529b93 | ||
|
|
77bf1c7d8e | ||
|
|
9d31d8b071 | ||
|
|
5256d3c718 | ||
|
|
c662e2c4e3 | ||
|
|
06264354cf | ||
|
|
2dcbe87986 | ||
|
|
8506cdae8f | ||
|
|
bf7487fafe | ||
|
|
0afc2d5903 | ||
|
|
2d62b5937e | ||
|
|
97ddb10ff5 | ||
|
|
2a74c8e053 | ||
|
|
bd920cedf5 | ||
|
|
ee76929fee | ||
|
|
8bb8a72060 | ||
|
|
dcc5d40a04 | ||
|
|
3bebeb4d99 | ||
|
|
d2922afce2 | ||
|
|
5dfd0a9b50 | ||
|
|
67cfe654b8 | ||
|
|
7ca8dcfb6a | ||
|
|
948b0f4df9 | ||
|
|
584cacba6c | ||
|
|
45ed06be64 | ||
|
|
3dec627d40 |
64
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: 漏洞反馈
|
||||||
|
description: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭
|
||||||
|
title: "[Bug]: "
|
||||||
|
labels: ["bug?"]
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 部署方式
|
||||||
|
description: "主程序使用的部署方式"
|
||||||
|
options:
|
||||||
|
- 手动部署
|
||||||
|
- 安装器部署
|
||||||
|
- 一键安装包部署
|
||||||
|
- Docker部署
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 登录框架
|
||||||
|
description: "连接QQ使用的框架"
|
||||||
|
options:
|
||||||
|
- Mirai
|
||||||
|
- go-cqhttp
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 系统环境
|
||||||
|
description: 操作系统、系统架构、**主机地理位置**,地理位置最好写清楚,涉及网络问题排查。
|
||||||
|
placeholder: 例如: CentOS x64 中国大陆、Windows11 美国
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Python环境
|
||||||
|
description: 运行程序的Python版本
|
||||||
|
placeholder: 例如: Python 3.10
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: QChatGPT版本
|
||||||
|
description: QChatGPT版本号
|
||||||
|
placeholder: 例如: v2.6.0,可以使用`!version`命令查看
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 异常情况
|
||||||
|
description: 完整描述异常情况,什么时候发生的、发生了什么,尽可能详细
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 日志信息
|
||||||
|
description: 请提供完整的 **登录框架 和 QChatGPT控制台**的相关日志信息(若有),不提供日志信息**无法**为您排查问题,请尽可能详细
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 启用的插件
|
||||||
|
description: 有些情况可能和插件功能有关,建议提供插件启用情况。可以使用`!plugin`命令查看已启用的插件
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
21
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
21
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: 需求建议
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["改进"]
|
||||||
|
description: "新功能或现有功能优化请使用这个模板;不符合类别的issue将被直接关闭"
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: 这是一个?
|
||||||
|
description: 新功能建议还是现有功能优化
|
||||||
|
options:
|
||||||
|
- 新功能
|
||||||
|
- 现有功能优化
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 详细描述
|
||||||
|
description: 详细描述,越详细越好
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
24
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/submit-plugin.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: 提交新插件
|
||||||
|
title: "[Plugin]: 请求登记新插件"
|
||||||
|
labels: ["独立插件"]
|
||||||
|
description: "本模板供且仅供提交新插件使用"
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 插件名称
|
||||||
|
description: 填写插件的名称
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 插件代码库地址
|
||||||
|
description: 仅支持 Github
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 插件简介
|
||||||
|
description: 插件的简介
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
22
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
22
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
@@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: 漏洞反馈
|
|
||||||
about: 报错或漏洞请使用这个模板创建
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: 'bug'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**运行环境**
|
|
||||||
- 部署方式:
|
|
||||||
手动部署/自动部署/Docker部署
|
|
||||||
- 系统环境:
|
|
||||||
例如: Centos x64
|
|
||||||
- Python环境(仅手动部署填写):
|
|
||||||
例如: Python 3.10.9
|
|
||||||
|
|
||||||
**描述漏洞**
|
|
||||||
什么时候发生的,mirai还是主程序,越详细越好
|
|
||||||
|
|
||||||
**完整报错信息**
|
|
||||||
完整的报错信息
|
|
||||||
10
.github/ISSUE_TEMPLATE/需求建议.md
vendored
10
.github/ISSUE_TEMPLATE/需求建议.md
vendored
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: 需求建议
|
|
||||||
about: 软件优化建议请使用这个模板创建
|
|
||||||
title: "[ENHANCE]"
|
|
||||||
labels: 'enhancement'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
不是需求建议请勿填写此模板!!!!
|
|
||||||
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
allow:
|
||||||
|
- dependency-name: "yiri-mirai-rc"
|
||||||
|
- dependency-name: "dulwich"
|
||||||
|
- dependency-name: "openai"
|
||||||
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
## 概述
|
||||||
|
|
||||||
|
实现/解决/优化的内容:
|
||||||
|
|
||||||
|
### 事务
|
||||||
|
|
||||||
|
- [ ] 已阅读仓库[贡献指引](https://github.com/RockChinQ/QChatGPT/blob/master/CONTRIBUTING.md)
|
||||||
|
- [ ] 已与维护者在issues或其他平台沟通此PR大致内容
|
||||||
|
|
||||||
|
## 以下内容可在起草PR后、合并PR前逐步完成
|
||||||
|
|
||||||
|
### 功能
|
||||||
|
|
||||||
|
- [ ] 已编写完善的配置文件字段说明(若有新增)
|
||||||
|
- [ ] 已编写面向用户的新功能说明(若有必要)
|
||||||
|
- [ ] 已测试新功能或更改
|
||||||
|
|
||||||
|
### 兼容性
|
||||||
|
|
||||||
|
- [ ] 已处理版本兼容性
|
||||||
|
- [ ] 已处理插件兼容问题
|
||||||
|
|
||||||
|
### 风险
|
||||||
|
|
||||||
|
可能导致或已知的问题:
|
||||||
38
.github/workflows/build_docker_image.yml
vendored
Normal file
38
.github/workflows/build_docker_image.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
on:
|
||||||
|
#防止fork乱用action设置只能手动触发构建
|
||||||
|
workflow_dispatch:
|
||||||
|
## 发布release的时候会自动构建
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
jobs:
|
||||||
|
publish-docker-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Build image
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: judge has env GITHUB_REF # 如果没有GITHUB_REF环境变量,则把github.ref变量赋值给GITHUB_REF
|
||||||
|
run: |
|
||||||
|
if [ -z "$GITHUB_REF" ]; then
|
||||||
|
export GITHUB_REF=${{ github.ref }}
|
||||||
|
fi
|
||||||
|
- name: Check GITHUB_REF env
|
||||||
|
run: echo $GITHUB_REF
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
|
||||||
|
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
||||||
|
- name: Build # image name: rockchin/qchatgpt:<VERSION>
|
||||||
|
run: docker build --network=host -t rockchin/qchatgpt:${{ steps.get_version.outputs.VERSION }} -t rockchin/qchatgpt:latest .
|
||||||
|
- name: Login to Registry
|
||||||
|
run: docker login --username=${{ secrets.DOCKER_USERNAME }} --password ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
|
||||||
|
run: docker push rockchin/qchatgpt:${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|
||||||
|
- name: Push latest image
|
||||||
|
if: (startsWith(env.GITHUB_REF, 'refs/tags/')||startsWith(github.ref, 'refs/tags/')) && startsWith(github.repository, 'RockChinQ/QChatGPT')
|
||||||
|
run: docker push rockchin/qchatgpt:latest
|
||||||
43
.github/workflows/sync-wiki.yml
vendored
Normal file
43
.github/workflows/sync-wiki.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Update Wiki
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'res/wiki/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-wiki:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Git
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Clone Wiki Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: RockChinQ/QChatGPT.wiki
|
||||||
|
path: wiki
|
||||||
|
- name: Delete old wiki content
|
||||||
|
run: |
|
||||||
|
rm -rf wiki/*
|
||||||
|
- name: Copy res/wiki content to wiki
|
||||||
|
run: |
|
||||||
|
cp -r res/wiki/* wiki/
|
||||||
|
- name: Check for changes
|
||||||
|
run: |
|
||||||
|
cd wiki
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
- name: Commit and Push Changes
|
||||||
|
run: |
|
||||||
|
cd wiki
|
||||||
|
git add .
|
||||||
|
git commit -m "Update wiki"
|
||||||
|
git push
|
||||||
80
.github/workflows/test-pr.yml
vendored
Normal file
80
.github/workflows/test-pr.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Test Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [ready_for_review]
|
||||||
|
paths:
|
||||||
|
# 任何py文件改动都会触发
|
||||||
|
- '**.py'
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
# 允许手动触发
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
perform-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# 如果事件为pull_request_review且review状态为approved,则执行
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request' ||
|
||||||
|
(github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED') ||
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event_name == 'issue_comment' && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/test') && github.event.comment.user.login == 'RockChinQ')
|
||||||
|
steps:
|
||||||
|
# 签出测试工程仓库代码
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# 仓库地址
|
||||||
|
repository: RockChinQ/qcg-tester
|
||||||
|
# 仓库路径
|
||||||
|
path: qcg-tester
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cd qcg-tester
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Get PR details
|
||||||
|
id: get-pr
|
||||||
|
if: github.event_name == 'issue_comment'
|
||||||
|
uses: octokit/request-action@v2.x
|
||||||
|
with:
|
||||||
|
route: GET /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set PR source branch as env variable
|
||||||
|
if: github.event_name == 'issue_comment'
|
||||||
|
run: |
|
||||||
|
PR_SOURCE_BRANCH=$(echo '${{ steps.get-pr.outputs.data }}' | jq -r '.head.ref')
|
||||||
|
echo "BRANCH=$PR_SOURCE_BRANCH" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set PR Branch as bash env
|
||||||
|
if: github.event_name != 'issue_comment'
|
||||||
|
run: |
|
||||||
|
echo "BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||||
|
- name: Set OpenAI API Key from Secrets
|
||||||
|
run: |
|
||||||
|
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV
|
||||||
|
- name: Set OpenAI Reverse Proxy URL from Secrets
|
||||||
|
run: |
|
||||||
|
echo "OPENAI_REVERSE_PROXY=${{ secrets.OPENAI_REVERSE_PROXY }}" >> $GITHUB_ENV
|
||||||
|
- name: Run test
|
||||||
|
run: |
|
||||||
|
cd qcg-tester
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
run: |
|
||||||
|
cd qcg-tester/resource/QChatGPT
|
||||||
|
curl -Os https://uploader.codecov.io/latest/linux/codecov
|
||||||
|
chmod +x codecov
|
||||||
|
./codecov -t ${{ secrets.CODECOV_TOKEN }}
|
||||||
58
.github/workflows/update-cmdpriv-template.yml
vendored
Normal file
58
.github/workflows/update-cmdpriv-template.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: Update cmdpriv-template
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'pkg/qqbot/cmds/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
paths:
|
||||||
|
- 'pkg/qqbot/cmds/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-cmdpriv-template:
|
||||||
|
if: github.event.pull_request.merged == true || github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.10.13
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade yiri-mirai-rc openai>=1.0.0 colorlog func_timeout dulwich Pillow CallingGPT tiktoken
|
||||||
|
python -m pip install -U openai>=1.0.0
|
||||||
|
|
||||||
|
- name: Copy Scripts
|
||||||
|
run: |
|
||||||
|
cp res/scripts/generate_cmdpriv_template.py .
|
||||||
|
|
||||||
|
- name: Generate Files
|
||||||
|
run: |
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
- name: Run generate_cmdpriv_template.py
|
||||||
|
run: python3 generate_cmdpriv_template.py
|
||||||
|
|
||||||
|
- name: Check for changes in cmdpriv-template.json
|
||||||
|
id: check_changes
|
||||||
|
run: |
|
||||||
|
if git diff --name-only | grep -q "res/templates/cmdpriv-template.json"; then
|
||||||
|
echo "::set-output name=changes_detected::true"
|
||||||
|
else
|
||||||
|
echo "::set-output name=changes_detected::false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit changes to cmdpriv-template.json
|
||||||
|
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.name "GitHub Actions Bot"
|
||||||
|
git config --global user.email "<github-actions@github.com>"
|
||||||
|
git add res/templates/cmdpriv-template.json
|
||||||
|
git commit -m "Update cmdpriv-template.json"
|
||||||
|
git push
|
||||||
52
.github/workflows/update-override-all.yml
vendored
Normal file
52
.github/workflows/update-override-all.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Check and Update override_all
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'config-template.py'
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'config-template.py'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-override-all:
|
||||||
|
name: check and update
|
||||||
|
if: github.event.pull_request.merged == true || github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.x
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Copy Scripts
|
||||||
|
run: |
|
||||||
|
cp res/scripts/generate_override_all.py .
|
||||||
|
|
||||||
|
- name: Run generate_override_all.py
|
||||||
|
run: python3 generate_override_all.py
|
||||||
|
|
||||||
|
- name: Check for changes in override-all.json
|
||||||
|
id: check_changes
|
||||||
|
run: |
|
||||||
|
git diff --exit-code override-all.json || echo "::set-output name=changes_detected::true"
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: steps.check_changes.outputs.changes_detected == 'true'
|
||||||
|
run: |
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git config --global user.name "GitHub Actions"
|
||||||
|
git add override-all.json
|
||||||
|
git commit -m "Update override-all.json"
|
||||||
|
git push
|
||||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,6 +1,35 @@
|
|||||||
config.py
|
/config.py
|
||||||
.idea/
|
.idea/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
database.db
|
database.db
|
||||||
qchatgpt.log
|
qchatgpt.log
|
||||||
config.py
|
/banlist.py
|
||||||
|
plugins/
|
||||||
|
!plugins/__init__.py
|
||||||
|
/revcfg.py
|
||||||
|
prompts/
|
||||||
|
logs/
|
||||||
|
sensitive.json
|
||||||
|
temp/
|
||||||
|
current_tag
|
||||||
|
scenario/
|
||||||
|
!scenario/default-template.json
|
||||||
|
override.json
|
||||||
|
cookies.json
|
||||||
|
res/announcement_saved
|
||||||
|
res/announcement_saved.json
|
||||||
|
cmdpriv.json
|
||||||
|
tips.py
|
||||||
|
.venv
|
||||||
|
bin/
|
||||||
|
.vscode
|
||||||
|
test_*
|
||||||
|
venv/
|
||||||
|
hugchat.json
|
||||||
|
qcapi
|
||||||
|
claude.json
|
||||||
|
bard.json
|
||||||
|
/*yaml
|
||||||
|
!/docker-compose.yaml
|
||||||
|
res/instance_id.json
|
||||||
|
.DS_Store
|
||||||
|
|||||||
26
CONTRIBUTING.md
Normal file
26
CONTRIBUTING.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
## 参与项目
|
||||||
|
|
||||||
|
欢迎为此项目贡献代码或其他支持,以使您的点子或众人期待的功能成为现实,助力社区成长。
|
||||||
|
|
||||||
|
### 贡献形式
|
||||||
|
|
||||||
|
- 提交PR,解决issues中提到的bug或期待的功能
|
||||||
|
- 提交PR,实现您设想的功能(请先提出issue与作者沟通)
|
||||||
|
- 优化代码架构,使各个模块的组织更加整洁优雅
|
||||||
|
- 在issues中提出发现的bug或者期待的功能
|
||||||
|
- 为本项目在其他社交平台撰写文章、制作视频等
|
||||||
|
- 为本项目的衍生项目作出贡献,或开发插件增加功能
|
||||||
|
|
||||||
|
### 如何开始
|
||||||
|
|
||||||
|
- 加入本项目交流群,一同探讨项目相关事务
|
||||||
|
- 解决本项目或衍生项目的issues中亟待解决的问题
|
||||||
|
- 阅读并完善本项目文档
|
||||||
|
- 在各个社交媒体撰写本项目教程等
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- 代码中的注解`务必`符合Google风格的规范
|
||||||
|
- 模块顶部的引入代码请遵循`系统模块`、`第三方库模块`、`自定义模块`的顺序进行引入
|
||||||
|
- `不要`直接引入模块的特定属性,而是引入这个模块,再通过`xxx.yyy`的形式使用属性
|
||||||
|
- 任何作用域的字段`必须`先声明后使用,并在声明处注明类型提示
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.10.13-bullseye
|
||||||
|
WORKDIR /QChatGPT
|
||||||
|
|
||||||
|
COPY . /QChatGPT/
|
||||||
|
|
||||||
|
RUN ls
|
||||||
|
|
||||||
|
RUN python -m pip install -r requirements.txt && \
|
||||||
|
python -m pip install -U websockets==10.0 && \
|
||||||
|
python -m pip install -U httpcore httpx openai
|
||||||
|
|
||||||
|
# 生成配置文件
|
||||||
|
RUN python main.py
|
||||||
|
|
||||||
|
CMD [ "python", "main.py" ]
|
||||||
142
README.md
142
README.md
@@ -1,111 +1,51 @@
|
|||||||
# QChatGPT🤖
|
|
||||||
|
|
||||||
- 到[项目Wiki](https://github.com/RockChinQ/QChatGPT/wiki)可了解项目详细信息
|
<p align="center">
|
||||||
- 由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
|
<img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
|
||||||
- 测试号: 2196084348
|
</p>
|
||||||
- 交流、答疑群: 204785790
|
|
||||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
|
||||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
|
||||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
|
||||||
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
|
|
||||||
|
|
||||||
通过调用OpenAI GPT-3模型提供的Completion API来实现一个更加智能的QQ机器人
|
<div align="center">
|
||||||
|
|
||||||
## ✅功能
|
# QChatGPT
|
||||||
|
|
||||||
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
|
<blockquote> 🥳 QChatGPT 一周年啦,感谢大家的支持!欢迎前往<a href="https://github.com/RockChinQ/QChatGPT/discussions/627">讨论</a>。</blockquote>
|
||||||
|
|
||||||
## 🔩部署
|
[](https://github.com/RockChinQ/QChatGPT/releases/latest)
|
||||||
|
<a href="https://hub.docker.com/repository/docker/rockchin/qchatgpt">
|
||||||
|
<img src="https://img.shields.io/docker/pulls/rockchin/qchatgpt?color=blue" alt="docker pull">
|
||||||
|
</a>
|
||||||
|

|
||||||
|
<a href="https://codecov.io/gh/RockChinQ/QChatGPT" >
|
||||||
|
<img src="https://codecov.io/gh/RockChinQ/QChatGPT/graph/badge.svg?token=pjxYIL2kbC"/>
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
|
||||||
|
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=66-aWvn8cbP4c1ut_1YYkvvGVeEtyTH8&authKey=pTaKBK5C%2B8dFzQ4XlENf6MHTCLaHnlKcCRx7c14EeVVlpX2nRSaS8lJm8YeM4mCU&noverify=0&group_code=195992197">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/%E5%AE%98%E6%96%B9%E7%BE%A4-195992197-purple">
|
||||||
|
</a>
|
||||||
|
<a href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=nC80H57wmKPwRDLFeQrDDjVl81XuC21P&authKey=2wTUTfoQ5v%2BD4C5zfpuR%2BSPMDqdXgDXA%2FS2wHI1NxTfWIG%2B%2FqK08dgyjMMOzhXa9&noverify=0&group_code=738382634">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/%E7%A4%BE%E5%8C%BA%E7%BE%A4-738382634-purple">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.bilibili.com/video/BV14h4y1w7TC">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/%E8%A7%86%E9%A2%91%E6%95%99%E7%A8%8B-208647">
|
||||||
|
</a>
|
||||||
|
<a href="https://www.bilibili.com/video/BV11h4y1y74H">
|
||||||
|
<img alt="Static Badge" src="https://img.shields.io/badge/Linux%E9%83%A8%E7%BD%B2%E8%A7%86%E9%A2%91-208647">
|
||||||
|
</a>
|
||||||
|
|
||||||
**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
|
## 使用文档
|
||||||
|
|
||||||
### - 注册OpenAI账号
|
<a href="https://qchatgpt.rockchin.top">项目主页</a> |
|
||||||
|
<a href="https://qchatgpt.rockchin.top/posts/feature.html">功能介绍</a> |
|
||||||
|
<a href="https://qchatgpt.rockchin.top/posts/deploy/">部署文档</a> |
|
||||||
|
<a href="https://qchatgpt.rockchin.top/posts/error/">常见问题</a> |
|
||||||
|
<a href="https://qchatgpt.rockchin.top/posts/plugin/intro.html">插件介绍</a> |
|
||||||
|
<a href="https://github.com/RockChinQ/QChatGPT/issues/new?assignees=&labels=%E7%8B%AC%E7%AB%8B%E6%8F%92%E4%BB%B6&projects=&template=submit-plugin.yml&title=%5BPlugin%5D%3A+%E8%AF%B7%E6%B1%82%E7%99%BB%E8%AE%B0%E6%96%B0%E6%8F%92%E4%BB%B6">提交插件</a>
|
||||||
|
|
||||||
参考以下文章
|
## 相关链接
|
||||||
|
|
||||||
> [只需 1 元搞定 ChatGPT 注册](https://zhuanlan.zhihu.com/p/589470082)
|
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a> |
|
||||||
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
|
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a> |
|
||||||
|
<a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
|
||||||
|
|
||||||
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
|
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
|
||||||
完成注册后,使用以下自动化或手动部署步骤
|
</div>
|
||||||
|
|
||||||
### - 自动化部署
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>展开查看,以下方式二选一,Linux首选Docker,Windows首选安装器</summary>
|
|
||||||
|
|
||||||
#### Docker方式
|
|
||||||
|
|
||||||
请查看此仓库[mikumifa/QChatGPT-Docker-Installer](https://github.com/mikumifa/QChatGPT-Docker-Installer)
|
|
||||||
|
|
||||||
#### 安装器方式
|
|
||||||
使用[此安装器](https://github.com/RockChinQ/qcg-installer)(若无法访问请到[Gitee](https://gitee.com/RockChin/qcg-installer))进行部署
|
|
||||||
|
|
||||||
- 安装器目前仅支持部分平台,请到仓库文档查看,其他平台请手动部署
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### - 手动部署
|
|
||||||
<details>
|
|
||||||
<summary>手动部署适用于所有平台</summary>
|
|
||||||
|
|
||||||
- 请使用Python 3.9.x以上版本
|
|
||||||
- 请注意OpenAI账号额度消耗
|
|
||||||
- 每个账户仅有18美元免费额度,如未绑定银行卡,则会在超出时报错
|
|
||||||
- OpenAI收费标准:默认使用的`text-davinci-003`模型 0.02美元/千字
|
|
||||||
|
|
||||||
#### 配置Mirai
|
|
||||||
|
|
||||||
按照[此教程](https://yiri-mirai.wybxc.cc/tutorials/01/configuration)配置Mirai及YiriMirai
|
|
||||||
启动mirai-console后,使用`login`命令登录QQ账号,保持mirai-console运行状态
|
|
||||||
|
|
||||||
#### 配置主程序
|
|
||||||
|
|
||||||
1. 克隆此项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/RockChinQ/QChatGPT
|
|
||||||
cd QChatGPT
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip3 install yiri-mirai openai colorlog func_timeout
|
|
||||||
pip3 install dulwich
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 运行一次主程序,生成配置文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 编辑配置文件`config.py`
|
|
||||||
|
|
||||||
按照文件内注释填写配置信息
|
|
||||||
|
|
||||||
5. 运行主程序
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
无报错信息即为运行成功
|
|
||||||
|
|
||||||
**常见问题**
|
|
||||||
|
|
||||||
- mirai登录提示`QQ版本过低`,见[此issue](https://github.com/RockChinQ/QChatGPT/issues/38)
|
|
||||||
- 如提示安装`uvicorn`或`hypercorn`请*不要*安装,这两个不是必需的,目前存在未知原因bug
|
|
||||||
- 如报错`TypeError: As of 3.10, the *loop* parameter was removed from Lock() since it is no longer necessary`, 请参考 [此处](https://github.com/RockChinQ/QChatGPT/issues/5)
|
|
||||||
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 🚀使用
|
|
||||||
|
|
||||||
查看[Wiki功能使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
|
|
||||||
|
|
||||||
## 👍赞赏
|
|
||||||
|
|
||||||
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/>
|
|
||||||
|
|||||||
215
README_en.md
Normal file
215
README_en.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# QChatGPT🤖
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="res/social.png" alt="QChatGPT" width="640" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
English | [简体中文](README.md)
|
||||||
|
|
||||||
|
[](https://github.com/RockChinQ/QChatGPT/releases/latest)
|
||||||
|

|
||||||
|
|
||||||
|
- Refer to [Wiki](https://github.com/RockChinQ/QChatGPT/wiki) to get further information.
|
||||||
|
- Official QQ group: 656285629
|
||||||
|
- Community QQ group: 362515018
|
||||||
|
- QQ channel robot: [QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
|
||||||
|
- Any contribution is welcome, please refer to [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## 🍺List of supported models
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Details</summary>
|
||||||
|
|
||||||
|
### Chat
|
||||||
|
|
||||||
|
- OpenAI GPT-3.5 (ChatGPT API), default model
|
||||||
|
- OpenAI GPT-3, supported natively, switch to it in `config.py`
|
||||||
|
- OpenAI GPT-4, supported natively, qualification for internal testing required, switch to it in `config.py`
|
||||||
|
- ChatGPT website edition (GPT-3.5), see [revLibs plugin](https://github.com/RockChinQ/revLibs)
|
||||||
|
- ChatGPT website edition (GPT-4), ChatGPT plus subscription required, see [revLibs plugin](https://github.com/RockChinQ/revLibs)
|
||||||
|
- New Bing, see [revLibs plugin](https://github.com/RockChinQ/revLibs)
|
||||||
|
- HuggingChat, see [revLibs plugin](https://github.com/RockChinQ/revLibs), English only
|
||||||
|
|
||||||
|
### Story
|
||||||
|
|
||||||
|
- NovelAI API, see [QCPNovelAi plugin](https://github.com/dominoar/QCPNovelAi)
|
||||||
|
|
||||||
|
### Image
|
||||||
|
|
||||||
|
- OpenAI DALL·E, supported natively, see [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
|
||||||
|
- NovelAI API, see [QCPNovelAi plugin](https://github.com/dominoar/QCPNovelAi)
|
||||||
|
|
||||||
|
### Voice
|
||||||
|
|
||||||
|
- TTS+VITS, see [QChatPlugins](https://github.com/dominoar/QChatPlugins)
|
||||||
|
- Plachta/VITS-Umamusume-voice-synthesizer, see [chat_voice plugin](https://github.com/oliverkirk-sudo/chat_voice)
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Install this [plugin](https://github.com/RockChinQ/Switcher) to switch between different models.
|
||||||
|
|
||||||
|
## ✅Features
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Details</summary>
|
||||||
|
|
||||||
|
- ✅Sensitive word filtering, avoid being banned
|
||||||
|
- ✅Multiple responding rules, including regular expression matching
|
||||||
|
- ✅Multiple api-key management, automatic switching when exceeding
|
||||||
|
- ✅Support for customizing the preset prompt text
|
||||||
|
- ✅Chat, story, image, voice, etc. models are supported
|
||||||
|
- ✅Support for hot reloading and hot updating
|
||||||
|
- ✅Support for plugin loading
|
||||||
|
- ✅Blacklist mechanism for private chat and group chat
|
||||||
|
- ✅Excellent long message processing strategy
|
||||||
|
- ✅Reply rate limitation
|
||||||
|
- ✅Support for network proxy
|
||||||
|
- ✅Support for customizing the output format
|
||||||
|
</details>
|
||||||
|
|
||||||
|
More details, see [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E5%8A%9F%E8%83%BD%E7%82%B9%E5%88%97%E4%B8%BE)
|
||||||
|
|
||||||
|
## 🔩Deployment
|
||||||
|
|
||||||
|
**If you encounter any problems during deployment, please search in the issue of [QChatGPT](https://github.com/RockChinQ/QChatGPT/issues) or [qcg-installer](https://github.com/RockChinQ/qcg-installer/issues) first.**
|
||||||
|
|
||||||
|
### - Register OpenAI account
|
||||||
|
|
||||||
|
> If you want to use a model other than OpenAI (such as New Bing), you can skip this step and directly refer to following steps, and then configure it according to the relevant plugin documentation.
|
||||||
|
|
||||||
|
To register OpenAI account, please refer to the following articles(in Chinese):
|
||||||
|
|
||||||
|
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
|
||||||
|
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
|
||||||
|
|
||||||
|
Check your api-key in [personal center](https://beta.openai.com/account/api-keys) after registration, and then follow the following steps to deploy.
|
||||||
|
|
||||||
|
### - Deploy Automatically
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Details</summary>
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
|
||||||
|
See [this document(cn)](res/docs/docker_deploy.md)
|
||||||
|
Contributed by [@mikumifa](https://github.com/mikumifa)
|
||||||
|
|
||||||
|
#### Installer
|
||||||
|
|
||||||
|
Use [this installer](https://github.com/RockChinQ/qcg-installer) to deploy.
|
||||||
|
|
||||||
|
- The installer currently only supports some platforms, please refer to the repository document for details, and manually deploy for other platforms
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### - Deploy Manually
|
||||||
|
<details>
|
||||||
|
<summary>Manually deployment supports any platforms</summary>
|
||||||
|
|
||||||
|
- Python 3.9.x or higher
|
||||||
|
|
||||||
|
#### 配置QQ登录框架
|
||||||
|
|
||||||
|
Currently supports mirai and go-cqhttp, configure either one
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>mirai</summary>
|
||||||
|
|
||||||
|
Follow [this tutorial(cn)](https://yiri-mirai.wybxc.cc/tutorials/01/configuration) to configure Mirai and YiriMirai.
|
||||||
|
After starting mirai-console, use the `login` command to log in to the QQ account, and keep the mirai-console running.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>go-cqhttp</summary>
|
||||||
|
|
||||||
|
1. Follow [this tutorial(cn)](https://github.com/RockChinQ/QChatGPT/wiki/go-cqhttp%E9%85%8D%E7%BD%AE) to configure go-cqhttp.
|
||||||
|
2. Start go-cqhttp, make sure it is logged in and running.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### Configure QChatGPT
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/RockChinQ/QChatGPT
|
||||||
|
cd QChatGPT
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip3 install requests yiri-mirai-rc openai colorlog func_timeout dulwich Pillow nakuru-project-idk
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Generate `config.py`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Edit `config.py`
|
||||||
|
|
||||||
|
5. Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Any problems, please refer to the issues page.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🚀Usage
|
||||||
|
|
||||||
|
**After deployment, please read: [Commands(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E6%9C%BA%E5%99%A8%E4%BA%BA%E6%8C%87%E4%BB%A4)**
|
||||||
|
|
||||||
|
**For more details, please refer to the [Wiki(cn)](https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)**
|
||||||
|
|
||||||
|
|
||||||
|
## 🧩Plugin Ecosystem
|
||||||
|
|
||||||
|
Plugin [usage](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8) and [development](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91) are supported.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>List of plugins (cn)</summary>
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
在`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
|
||||||
|
|
||||||
|
- `cmdcn` - 主程序命令中文形式
|
||||||
|
- `hello_plugin` - 在收到消息`hello`时回复相应消息
|
||||||
|
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
|
||||||
|
|
||||||
|
### More Plugins
|
||||||
|
|
||||||
|
欢迎提交新的插件
|
||||||
|
|
||||||
|
- [revLibs](https://github.com/RockChinQ/revLibs) - 将ChatGPT网页版接入此项目,关于[官方接口和网页版有什么区别](https://github.com/RockChinQ/QChatGPT/wiki/%E5%AE%98%E6%96%B9%E6%8E%A5%E5%8F%A3%E4%B8%8EChatGPT%E7%BD%91%E9%A1%B5%E7%89%88)
|
||||||
|
- [Switcher](https://github.com/RockChinQ/Switcher) - 支持通过命令切换使用的模型
|
||||||
|
- [hello_plugin](https://github.com/RockChinQ/hello_plugin) - `hello_plugin` 的储存库形式,插件开发模板
|
||||||
|
- [dominoar/QChatPlugins](https://github.com/dominoar/QchatPlugins) - dominoar编写的诸多新功能插件(语音输出、Ranimg、屏蔽词规则等)
|
||||||
|
- [dominoar/QCP-NovelAi](https://github.com/dominoar/QCP-NovelAi) - NovelAI 故事叙述与绘画
|
||||||
|
- [oliverkirk-sudo/chat_voice](https://github.com/oliverkirk-sudo/chat_voice) - 文字转语音输出,使用HuggingFace上的[VITS-Umamusume-voice-synthesizer模型](https://huggingface.co/spaces/Plachta/VITS-Umamusume-voice-synthesizer)
|
||||||
|
- [RockChinQ/WaitYiYan](https://github.com/RockChinQ/WaitYiYan) - 实时获取百度`文心一言`等待列表人数
|
||||||
|
- [chordfish-k/QChartGPT_Emoticon_Plugin](https://github.com/chordfish-k/QChartGPT_Emoticon_Plugin) - 使机器人根据回复内容发送表情包
|
||||||
|
- [oliverkirk-sudo/ChatPoeBot](https://github.com/oliverkirk-sudo/ChatPoeBot) - 接入[Poe](https://poe.com/)上的机器人
|
||||||
|
- [lieyanqzu/WeatherPlugin](https://github.com/lieyanqzu/WeatherPlugin) - 天气查询插件
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 😘Thanks
|
||||||
|
|
||||||
|
- [@the-lazy-me](https://github.com/the-lazy-me) video tutorial creator
|
||||||
|
- [@mikumifa](https://github.com/mikumifa) Docker deployment
|
||||||
|
- [@dominoar](https://github.com/dominoar) Plugin development
|
||||||
|
- [@万神的星空](https://github.com/qq255204159) Packages publisher
|
||||||
|
- [@ljcduo](https://github.com/ljcduo) GPT-4 API internal test account
|
||||||
|
|
||||||
|
And all [contributors](https://github.com/RockChinQ/QChatGPT/graphs/contributors) and other friends who support this project.
|
||||||
|
|
||||||
|
<!-- ## 👍赞赏
|
||||||
|
|
||||||
|
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/> -->
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除
|
# 配置文件: 注释里标[必需]的参数必须修改, 其他参数根据需要修改, 但请勿删除
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# [必需] Mirai的配置
|
# 消息处理协议适配器
|
||||||
|
# 目前支持以下适配器:
|
||||||
|
# - "yirimirai": mirai的通信框架,YiriMirai框架适配器, 请同时填写下方mirai_http_api_config
|
||||||
|
# - "nakuru": go-cqhttp通信框架,请同时填写下方nakuru_config
|
||||||
|
msg_source_adapter = "yirimirai"
|
||||||
|
|
||||||
|
# [必需(与nakuru二选一,取决于msg_source_adapter)] Mirai的配置
|
||||||
# 请到配置mirai的步骤中的教程查看每个字段的信息
|
# 请到配置mirai的步骤中的教程查看每个字段的信息
|
||||||
# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter
|
# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter
|
||||||
# host: 运行mirai的主机地址
|
# host: 运行mirai的主机地址
|
||||||
@@ -18,124 +24,347 @@ mirai_http_api_config = {
|
|||||||
"qq": 1234567890
|
"qq": 1234567890
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# [必需(与mirai二选一,取决于msg_source_adapter)]
|
||||||
|
# 使用nakuru-project框架连接go-cqhttp的配置
|
||||||
|
nakuru_config = {
|
||||||
|
"host": "localhost", # go-cqhttp的地址
|
||||||
|
"port": 6700, # go-cqhttp的正向websocket端口
|
||||||
|
"http_port": 5700, # go-cqhttp的正向http端口
|
||||||
|
"token": "" # 若在go-cqhttp的config.yml设置了access_token, 则填写此处
|
||||||
|
}
|
||||||
|
|
||||||
# [必需] OpenAI的配置
|
# [必需] OpenAI的配置
|
||||||
# api_key: OpenAI的API Key
|
# api_key: OpenAI的API Key
|
||||||
|
# http_proxy: 请求OpenAI时使用的代理,None为不使用,https和socks5暂不能使用
|
||||||
# 若只有一个api-key,请直接修改以下内容中的"openai_api_key"为你的api-key
|
# 若只有一个api-key,请直接修改以下内容中的"openai_api_key"为你的api-key
|
||||||
|
#
|
||||||
# 如准备了多个api-key,可以以字典的形式填写,程序会自动选择可用的api-key
|
# 如准备了多个api-key,可以以字典的形式填写,程序会自动选择可用的api-key
|
||||||
# 例如{
|
# 例如
|
||||||
# "api0": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
# openai_config = {
|
||||||
# "api1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
# "api_key": {
|
||||||
# }
|
# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# },
|
||||||
|
# "http_proxy": "http://127.0.0.1:12345"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 现已支持反向代理,可以添加reverse_proxy字段以使用反向代理
|
||||||
|
# 使用反向代理可以在国内使用OpenAI的API,反向代理的配置请参考
|
||||||
|
# https://github.com/Ice-Hazymoon/openai-scf-proxy
|
||||||
|
#
|
||||||
|
# 反向代理填写示例:
|
||||||
|
# openai_config = {
|
||||||
|
# "api_key": {
|
||||||
|
# "default": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# "key1": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# "key2": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
# },
|
||||||
|
# "reverse_proxy": "http://example.com:12345/v1"
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 作者开设公用反向代理地址: https://api.openai.rockchin.top/v1
|
||||||
|
# 随时可能关闭,仅供测试使用,有条件建议使用正向代理或者自建反向代理
|
||||||
openai_config = {
|
openai_config = {
|
||||||
"api_key": {
|
"api_key": {
|
||||||
"default": "openai_api_key"
|
"default": "openai_api_key"
|
||||||
},
|
},
|
||||||
|
"http_proxy": None,
|
||||||
|
"reverse_proxy": None
|
||||||
}
|
}
|
||||||
|
|
||||||
# 管理员QQ号,用于接收报错等通知及执行管理员级别指令,为0时关闭此功能
|
# api-key切换策略
|
||||||
|
# active:每次请求时都会切换api-key
|
||||||
|
# passive:仅当api-key超额时才会切换api-key
|
||||||
|
switch_strategy = "active"
|
||||||
|
|
||||||
|
# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别命令
|
||||||
|
# 支持多个管理员,可以使用list形式设置,例如:
|
||||||
|
# admin_qq = [12345678, 87654321]
|
||||||
admin_qq = 0
|
admin_qq = 0
|
||||||
|
|
||||||
# 情景预设(机器人人格)
|
# 情景预设(机器人人格)
|
||||||
# 每个会话的预设信息,影响所有会话,无视指令重置
|
# 每个会话的预设信息,影响所有会话,无视命令重置
|
||||||
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
|
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
|
||||||
# 例如: 如果我之后想获取帮助,请你说“输入!help获取帮助”,
|
# 例如:
|
||||||
|
# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
|
||||||
# 这样用户在不知所措的时候机器人就会提示其输入!help获取帮助
|
# 这样用户在不知所措的时候机器人就会提示其输入!help获取帮助
|
||||||
# 可参考 https://github.com/PlexPt/awesome-chatgpt-prompts-zh
|
# 可参考 https://github.com/PlexPt/awesome-chatgpt-prompts-zh
|
||||||
default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
|
#
|
||||||
|
# 如果需要多个情景预设,并在运行期间方便切换,请使用字典的形式填写,例如
|
||||||
|
# default_prompt = {
|
||||||
|
# "default": "如果我之后想获取帮助,请你说“输入!help获取帮助”",
|
||||||
|
# "linux-terminal": "我想让你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。",
|
||||||
|
# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。",
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 在使用期间即可通过命令:
|
||||||
|
# !reset [名称]
|
||||||
|
# 来使用指定的情景预设重置会话
|
||||||
|
# 例如:
|
||||||
|
# !reset linux-terminal
|
||||||
|
# 若不指定名称,则使用默认情景预设
|
||||||
|
#
|
||||||
|
# 也可以使用命令:
|
||||||
|
# !default <名称>
|
||||||
|
# 将指定的情景预设设置为默认情景预设
|
||||||
|
# 例如:
|
||||||
|
# !default linux-terminal
|
||||||
|
# 之后的会话重置时若不指定名称,则使用linux-terminal情景预设
|
||||||
|
#
|
||||||
|
# 还可以加载文件中的预设文字,使用方法请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97
|
||||||
|
default_prompt = {
|
||||||
|
"default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 情景预设格式
|
||||||
|
# 参考值:默认方式:normal | 完整情景:full_scenario
|
||||||
|
# 默认方式 的格式为上述default_prompt中的内容,或prompts目录下的文件名
|
||||||
|
# 完整情景方式 的格式为JSON,在scenario目录下的JSON文件中列出对话的每个回合,编写方法见scenario/default-template.json
|
||||||
|
# 编写方法请查看:https://github.com/RockChinQ/QChatGPT/wiki/%E5%8A%9F%E8%83%BD%E4%BD%BF%E7%94%A8#%E9%A2%84%E8%AE%BE%E6%96%87%E5%AD%97full_scenario%E6%A8%A1%E5%BC%8F
|
||||||
|
preset_mode = "normal"
|
||||||
|
|
||||||
# 群内响应规则
|
# 群内响应规则
|
||||||
# 符合此消息的群内消息即使不包含at机器人也会响应
|
# 符合此消息的群内消息即使不包含at机器人也会响应
|
||||||
# 支持消息前缀匹配及正则表达式匹配
|
# 支持消息前缀匹配及正则表达式匹配
|
||||||
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式匹配的消息不会删除匹配的部分
|
# 支持设置是否响应at消息、随机响应概率
|
||||||
|
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分
|
||||||
# 前缀匹配优先级高于正则表达式匹配
|
# 前缀匹配优先级高于正则表达式匹配
|
||||||
# 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html
|
# 正则表达式简明教程:https://www.runoob.com/regexp/regexp-tutorial.html
|
||||||
|
#
|
||||||
|
# 支持针对不同群设置不同的响应规则,例如:
|
||||||
|
# response_rules = {
|
||||||
|
# "default": {
|
||||||
|
# "at": True,
|
||||||
|
# "prefix": ["/ai", "!ai", "!ai", "ai"],
|
||||||
|
# "regexp": [],
|
||||||
|
# "random_rate": 0.0,
|
||||||
|
# },
|
||||||
|
# "12345678": {
|
||||||
|
# "at": False,
|
||||||
|
# "prefix": ["/ai", "!ai", "!ai", "ai"],
|
||||||
|
# "regexp": [],
|
||||||
|
# "random_rate": 0.0,
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 以上设置将会在群号为12345678的群中关闭at响应
|
||||||
|
# 未单独设置的群将使用default规则
|
||||||
response_rules = {
|
response_rules = {
|
||||||
"prefix": ["/ai", "!ai", "!ai", "ai"],
|
"default": {
|
||||||
"regexp": [] # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
|
"at": True, # 是否响应at机器人的消息
|
||||||
|
"prefix": ["/ai", "!ai", "!ai", "ai"],
|
||||||
|
"regexp": [], # "为什么.*", "怎么?样.*", "怎么.*", "如何.*", "[Hh]ow to.*", "[Ww]hy not.*", "[Ww]hat is.*", ".*怎么办", ".*咋办"
|
||||||
|
"random_rate": 0.0, # 随机响应概率,0.0-1.0,0.0为不随机响应,1.0为响应所有消息, 仅在前几项判断不通过时生效
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# 单个api-key的费用警告阈值
|
|
||||||
# 当使用此api-key进行请求所消耗的费用估算达到此阈值时,会在控制台输出警告并通知管理员
|
|
||||||
# 若之后还有未使用超过此值的api-key,则会切换到新的api-key进行请求
|
|
||||||
# 单位:美元
|
|
||||||
api_key_fee_threshold = 18.0
|
|
||||||
|
|
||||||
# 是否根据估算的使用费用切换api-key
|
# 消息忽略规则
|
||||||
# 设置为False将只在接口报错超额时自动切换
|
# 适用于私聊及群聊
|
||||||
auto_switch_api_key = False
|
# 符合此规则的消息将不会被响应
|
||||||
|
# 支持消息前缀匹配及正则表达式匹配
|
||||||
|
# 此设置优先级高于response_rules
|
||||||
|
# 用以过滤mirai等其他层级的命令
|
||||||
|
# @see https://github.com/RockChinQ/QChatGPT/issues/165
|
||||||
|
ignore_rules = {
|
||||||
|
"prefix": ["/"],
|
||||||
|
"regexp": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 是否检查收到的消息中是否包含敏感词
|
||||||
|
# 若收到的消息无法通过下方指定的敏感词检查策略,则发送提示信息
|
||||||
|
income_msg_check = False
|
||||||
|
|
||||||
# 敏感词过滤开关,以同样数量的*代替敏感词回复
|
# 敏感词过滤开关,以同样数量的*代替敏感词回复
|
||||||
# 请在sensitive.json中添加敏感词
|
# 请在sensitive.json中添加敏感词
|
||||||
sensitive_word_filter = True
|
sensitive_word_filter = True
|
||||||
|
|
||||||
# 每次向OpenAI接口发送对话记录上下文的字符数
|
# 是否启用百度云内容安全审核
|
||||||
# 最大不超过(4096 - max_tokens)个字符,max_tokens为上述completion_api_params中的max_tokens
|
# 注册方式查看 https://cloud.baidu.com/doc/ANTIPORN/s/Wkhu9d5iy
|
||||||
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
|
baidu_check = False
|
||||||
prompt_submit_length = 1024
|
|
||||||
|
|
||||||
# OpenAI的completion API的参数
|
# 百度云API_KEY 24位英文数字字符串
|
||||||
|
baidu_api_key = ""
|
||||||
|
|
||||||
|
# 百度云SECRET_KEY 32位的英文数字字符串
|
||||||
|
baidu_secret_key = ""
|
||||||
|
|
||||||
|
# 不合规消息自定义返回
|
||||||
|
inappropriate_message_tips = "[百度云]请珍惜机器人,当前返回内容不合规"
|
||||||
|
|
||||||
|
# 启动时是否发送赞赏码
|
||||||
|
# 仅当使用量已经超过2048字时发送
|
||||||
|
encourage_sponsor_at_start = True
|
||||||
|
|
||||||
|
# 每次向OpenAI接口发送对话记录上下文的字符数
|
||||||
|
# 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens
|
||||||
|
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
|
||||||
|
prompt_submit_length = 3072
|
||||||
|
|
||||||
|
# 是否在token超限报错时自动重置会话
|
||||||
|
# 可在tips.py中编辑提示语
|
||||||
|
auto_reset = True
|
||||||
|
|
||||||
|
# OpenAI补全API的参数
|
||||||
|
# 请在下方填写模型,程序自动选择接口
|
||||||
|
# 模型文档:https://platform.openai.com/docs/models
|
||||||
|
# 现已支持的模型有:
|
||||||
|
#
|
||||||
|
# ChatCompletions 接口:
|
||||||
|
# # GPT 4 系列
|
||||||
|
# "gpt-4-1106-preview",
|
||||||
|
# "gpt-4-vision-preview",
|
||||||
|
# "gpt-4",
|
||||||
|
# "gpt-4-32k",
|
||||||
|
# "gpt-4-0613",
|
||||||
|
# "gpt-4-32k-0613",
|
||||||
|
# "gpt-4-0314", # legacy
|
||||||
|
# "gpt-4-32k-0314", # legacy
|
||||||
|
# # GPT 3.5 系列
|
||||||
|
# "gpt-3.5-turbo-1106",
|
||||||
|
# "gpt-3.5-turbo",
|
||||||
|
# "gpt-3.5-turbo-16k",
|
||||||
|
# "gpt-3.5-turbo-0613", # legacy
|
||||||
|
# "gpt-3.5-turbo-16k-0613", # legacy
|
||||||
|
# "gpt-3.5-turbo-0301", # legacy
|
||||||
|
#
|
||||||
|
# Completions接口:
|
||||||
|
# "gpt-3.5-turbo-instruct",
|
||||||
|
#
|
||||||
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
|
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/completions/create
|
||||||
|
# 请将内容修改到config.py中,请勿修改config-template.py
|
||||||
|
#
|
||||||
|
# 支持通过 One API 接入多种模型,请在上方的openai_config中设置One API的代理地址,
|
||||||
|
# 并在此填写您要使用的模型名称,详细请参考:https://github.com/songquanpeng/one-api
|
||||||
|
#
|
||||||
|
# 支持的 One API 模型:
|
||||||
|
# "SparkDesk",
|
||||||
|
# "chatglm_pro",
|
||||||
|
# "chatglm_std",
|
||||||
|
# "chatglm_lite",
|
||||||
|
# "qwen-v1",
|
||||||
|
# "qwen-plus-v1",
|
||||||
|
# "ERNIE-Bot",
|
||||||
|
# "ERNIE-Bot-turbo",
|
||||||
|
# "gemini-pro",
|
||||||
completion_api_params = {
|
completion_api_params = {
|
||||||
"model": "text-davinci-003",
|
"model": "gpt-3.5-turbo",
|
||||||
"temperature": 0.6, # 数值越低得到的回答越理性,取值范围[0, 1]
|
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
|
||||||
"max_tokens": 512, # 每次向OpenAI请求的最大字符数, 不高于4096
|
|
||||||
"top_p": 1, # 生成的文本的文本与要求的符合度, 取值范围[0, 1]
|
|
||||||
"frequency_penalty": 0.2,
|
|
||||||
"presence_penalty": 1.0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# OpenAI的Image API的参数
|
# OpenAI的Image API的参数
|
||||||
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/images/create
|
# 具体请查看OpenAI的文档: https://platform.openai.com/docs/api-reference/images/create
|
||||||
image_api_params = {
|
image_api_params = {
|
||||||
"size": "256x256", # 图片尺寸,支持256x256, 512x512, 1024x1024
|
"model": "dall-e-2", # 默认使用 dall-e-2 模型,也可以改为 dall-e-3
|
||||||
|
# 图片尺寸
|
||||||
|
# dall-e-2 模型支持 256x256, 512x512, 1024x1024
|
||||||
|
# dall-e-3 模型支持 1024x1024, 1792x1024, 1024x1792
|
||||||
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 回复消息时是否引用原消息
|
# 跟踪函数调用
|
||||||
quote_origin = True
|
# 为True时,在每次GPT进行Function Calling时都会输出发送一条回复给用户
|
||||||
|
# 同时,一次提问内所有的Function Calling和普通回复消息都会单独发送给用户
|
||||||
|
trace_function_calls = False
|
||||||
|
|
||||||
|
# 群内回复消息时是否引用原消息
|
||||||
|
quote_origin = False
|
||||||
|
|
||||||
|
# 群内回复消息时是否at发送者
|
||||||
|
at_sender = False
|
||||||
|
|
||||||
# 回复绘图时是否包含图片描述
|
# 回复绘图时是否包含图片描述
|
||||||
include_image_description = True
|
include_image_description = True
|
||||||
|
|
||||||
# 消息处理的超时时间,单位为秒
|
# 消息处理的超时时间,单位为秒
|
||||||
process_message_timeout = 15
|
process_message_timeout = 120
|
||||||
|
|
||||||
# 机器人的配置
|
|
||||||
# user_name: 管理员(主人)的名字
|
|
||||||
# bot_name: 机器人的名字
|
|
||||||
user_name = 'You'
|
|
||||||
bot_name = 'Bot'
|
|
||||||
|
|
||||||
# 回复消息时是否显示[GPT]前缀
|
# 回复消息时是否显示[GPT]前缀
|
||||||
show_prefix = False
|
show_prefix = False
|
||||||
|
|
||||||
|
# 回复前的强制延迟时间,降低机器人被腾讯风控概率
|
||||||
|
# *此机制对命令和消息、私聊及群聊均生效
|
||||||
|
# 每次处理时从以下的范围取一个随机秒数,
|
||||||
|
# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数
|
||||||
|
# 例如:[1.5, 3],则每次处理时会随机取一个1.5-3秒的随机数,若处理时间低于此随机数,则强制延迟至此随机秒数
|
||||||
|
# 若您不需要此功能,请将force_delay_range设置为[0, 0]
|
||||||
|
force_delay_range = [0, 0]
|
||||||
|
|
||||||
|
# 应用长消息处理策略的阈值
|
||||||
|
# 当回复消息长度超过此值时,将使用长消息处理策略
|
||||||
|
blob_message_threshold = 256
|
||||||
|
|
||||||
|
# 长消息处理策略
|
||||||
|
# - "image": 将长消息转换为图片发送
|
||||||
|
# - "forward": 将长消息转换为转发消息组件发送
|
||||||
|
blob_message_strategy = "forward"
|
||||||
|
|
||||||
|
# 允许等待
|
||||||
|
# 同一会话内,是否等待上一条消息处理完成后再处理下一条消息
|
||||||
|
# 若设置为False,若上一条未处理完时收到了新消息,将会丢弃新消息
|
||||||
|
# 丢弃消息时的提示信息可以在tips.py中修改
|
||||||
|
wait_last_done = True
|
||||||
|
|
||||||
|
# 文字转图片时使用的字体文件路径
|
||||||
|
# 当策略为"image"时生效
|
||||||
|
# 若在Windows系统下,程序会自动使用Windows自带的微软雅黑字体
|
||||||
|
# 若未填写或不存在且不是Windows,将禁用文字转图片功能,改为使用转发消息组件
|
||||||
|
font_path = ""
|
||||||
|
|
||||||
# 消息处理超时重试次数
|
# 消息处理超时重试次数
|
||||||
retry_times = 3
|
retry_times = 3
|
||||||
|
|
||||||
|
# 消息处理出错时是否向用户隐藏错误详细信息
|
||||||
|
# 设置为True时,仅向管理员发送错误详细信息
|
||||||
|
# 设置为False时,向用户及管理员发送错误详细信息
|
||||||
|
hide_exce_info_to_user = False
|
||||||
|
|
||||||
# 每个会话的过期时间,单位为秒
|
# 每个会话的过期时间,单位为秒
|
||||||
# 默认值20分钟
|
# 默认值20分钟
|
||||||
session_expire_time = 60 * 20
|
session_expire_time = 1200
|
||||||
|
|
||||||
|
# 会话限速
|
||||||
|
# 单会话内每分钟可进行的对话次数
|
||||||
|
# 若不需要限速,可以设置为一个很大的值
|
||||||
|
# 默认值60次,基本上不会触发限速
|
||||||
|
#
|
||||||
|
# 若要设置针对某特定群的限速,请使用如下格式:
|
||||||
|
# {
|
||||||
|
# "group_<群号>": 60,
|
||||||
|
# "default": 60,
|
||||||
|
# }
|
||||||
|
# 若要设置针对某特定用户私聊的限速,请使用如下格式:
|
||||||
|
# {
|
||||||
|
# "person_<用户QQ>": 60,
|
||||||
|
# "default": 60,
|
||||||
|
# }
|
||||||
|
# 同时设置多个群和私聊的限速,示例:
|
||||||
|
# {
|
||||||
|
# "group_12345678": 60,
|
||||||
|
# "group_87654321": 60,
|
||||||
|
# "person_234567890": 60,
|
||||||
|
# "person_345678901": 60,
|
||||||
|
# "default": 60,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# 注意: 未指定的都使用default的限速值,default不可删除
|
||||||
|
rate_limitation = {
|
||||||
|
"default": 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 会话限速策略
|
||||||
|
# - "wait": 每次对话获取到回复时,等待一定时间再发送回复,保证其不会超过限速均值
|
||||||
|
# - "drop": 此分钟内,若对话次数超过限速次数,则丢弃之后的对话,每自然分钟重置
|
||||||
|
rate_limit_strategy = "drop"
|
||||||
|
|
||||||
|
# 是否在启动时进行依赖库更新
|
||||||
|
upgrade_dependencies = False
|
||||||
|
|
||||||
# 是否上报统计信息
|
# 是否上报统计信息
|
||||||
# 用于统计机器人的使用情况,不会收集任何用户信息
|
# 用于统计机器人的使用情况,数据不公开,不会收集任何敏感信息。
|
||||||
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报
|
# 仅实例识别UUID、上报时间、字数使用量、绘图使用量、插件使用情况、用户信息,其他信息不会上报
|
||||||
report_usage = True
|
report_usage = True
|
||||||
|
|
||||||
# 日志级别
|
# 日志级别
|
||||||
logging_level = logging.INFO
|
logging_level = logging.INFO
|
||||||
|
|
||||||
# 定制帮助消息
|
|
||||||
help_message = """此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。
|
|
||||||
你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。
|
|
||||||
了解此项目请找QQ 1010553892 联系作者
|
|
||||||
请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题
|
|
||||||
每次会话最后一次交互后{}分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启
|
|
||||||
欢迎到github.com/RockChinQ/QChatGPT 给个star
|
|
||||||
|
|
||||||
帮助信息:
|
|
||||||
!help - 显示帮助
|
|
||||||
!reset - 重置会话
|
|
||||||
!last - 切换到前一次的对话
|
|
||||||
!next - 切换到后一次的对话
|
|
||||||
!prompt - 显示当前对话所有内容
|
|
||||||
!list - 列出所有历史会话
|
|
||||||
!usage - 列出各个api-key的使用量""".format(session_expire_time // 60)
|
|
||||||
|
|||||||
18
docker-compose.yaml
Normal file
18
docker-compose.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
qchatgpt:
|
||||||
|
image: rockchin/qchatgpt:latest
|
||||||
|
volumes:
|
||||||
|
- ./config.py:/QChatGPT/config.py
|
||||||
|
- ./banlist.py:/QChatGPT/banlist.py
|
||||||
|
- ./cmdpriv.json:/QChatGPT/cmdpriv.json
|
||||||
|
- ./sensitive.json:/QChatGPT/sensitive.json
|
||||||
|
- ./tips.py:/QChatGPT/tips.py
|
||||||
|
# 目录映射
|
||||||
|
- ./plugins:/QChatGPT/plugins
|
||||||
|
- ./scenario:/QChatGPT/scenario
|
||||||
|
- ./temp:/QChatGPT/temp
|
||||||
|
- ./logs:/QChatGPT/logs
|
||||||
|
restart: always
|
||||||
|
# 根据具体环境配置网络
|
||||||
528
main.py
528
main.py
@@ -1,4 +1,5 @@
|
|||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import threading
|
import threading
|
||||||
@@ -6,26 +7,80 @@ import time
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
sys.path.append(".")
|
||||||
|
|
||||||
|
|
||||||
|
def check_file():
|
||||||
|
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份
|
||||||
|
if not os.path.exists('banlist.py'):
|
||||||
|
shutil.copy('res/templates/banlist-template.py', 'banlist.py')
|
||||||
|
|
||||||
|
# 检查是否有sensitive.json
|
||||||
|
if not os.path.exists("sensitive.json"):
|
||||||
|
shutil.copy("res/templates/sensitive-template.json", "sensitive.json")
|
||||||
|
|
||||||
|
# 检查是否有scenario/default.json
|
||||||
|
if not os.path.exists("scenario/default.json"):
|
||||||
|
shutil.copy("scenario/default-template.json", "scenario/default.json")
|
||||||
|
|
||||||
|
# 检查cmdpriv.json
|
||||||
|
if not os.path.exists("cmdpriv.json"):
|
||||||
|
shutil.copy("res/templates/cmdpriv-template.json", "cmdpriv.json")
|
||||||
|
|
||||||
|
# 检查tips_custom
|
||||||
|
if not os.path.exists("tips.py"):
|
||||||
|
shutil.copy("tips-custom-template.py", "tips.py")
|
||||||
|
|
||||||
|
# 检查temp目录
|
||||||
|
if not os.path.exists("temp/"):
|
||||||
|
os.mkdir("temp/")
|
||||||
|
|
||||||
|
# 检查并创建plugins、prompts目录
|
||||||
|
check_path = ["plugins", "prompts"]
|
||||||
|
for path in check_path:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
os.mkdir(path)
|
||||||
|
|
||||||
|
# 配置文件存在性校验
|
||||||
|
if not os.path.exists('config.py'):
|
||||||
|
shutil.copy('config-template.py', 'config.py')
|
||||||
|
print('请先在config.py中填写配置')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
# 初始化相关文件
|
||||||
|
check_file()
|
||||||
|
|
||||||
|
from pkg.utils.log import init_runtime_log_file, reset_logging
|
||||||
|
from pkg.config import manager as config_mgr
|
||||||
|
from pkg.config.impls import pymodule as pymodule_cfg
|
||||||
|
|
||||||
import mirai.exceptions
|
|
||||||
import websockets.exceptions
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import colorlog
|
import colorlog
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("未安装colorlog,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
|
# 尝试安装
|
||||||
sys.exit(1)
|
import pkg.utils.pkgmgr as pkgmgr
|
||||||
|
try:
|
||||||
|
pkgmgr.install_requirements("requirements.txt")
|
||||||
|
import colorlog
|
||||||
|
except ImportError:
|
||||||
|
print("依赖不满足,请查看 https://github.com/RockChinQ/qcg-installer/issues/15")
|
||||||
|
sys.exit(1)
|
||||||
import colorlog
|
import colorlog
|
||||||
|
|
||||||
sys.path.append(".")
|
import requests
|
||||||
|
import websockets.exceptions
|
||||||
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
log_colors_config = {
|
|
||||||
'DEBUG': 'green', # cyan white
|
# 是否使用override.json覆盖配置
|
||||||
'INFO': 'white',
|
# 仅在启动时提供 --override 或 -r 参数时生效
|
||||||
'WARNING': 'yellow',
|
use_override = False
|
||||||
'ERROR': 'red',
|
|
||||||
'CRITICAL': 'bold_red',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -35,138 +90,335 @@ def init_db():
|
|||||||
database.initialize_database()
|
database.initialize_database()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dependencies():
|
||||||
|
import pkg.utils.pkgmgr as pkgmgr
|
||||||
|
pkgmgr.run_pip(["install", "openai", "Pillow", "nakuru-project-idk", "CallingGPT", "tiktoken", "--upgrade",
|
||||||
|
"-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||||
|
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
|
||||||
|
|
||||||
|
|
||||||
known_exception_caught = False
|
known_exception_caught = False
|
||||||
|
|
||||||
|
|
||||||
def main(first_time_init=False):
|
def override_config_manager():
|
||||||
|
config = pkg.utils.context.get_config_manager().data
|
||||||
|
|
||||||
|
if os.path.exists("override.json") and use_override:
|
||||||
|
override_json = json.load(open("override.json", "r", encoding="utf-8"))
|
||||||
|
overrided = []
|
||||||
|
for key in override_json:
|
||||||
|
if key in config:
|
||||||
|
config[key] = override_json[key]
|
||||||
|
# logging.info("覆写配置[{}]为[{}]".format(key, override_json[key]))
|
||||||
|
overrided.append(key)
|
||||||
|
else:
|
||||||
|
logging.error("无法覆写配置[{}]为[{}],该配置不存在,请检查override.json是否正确".format(key, override_json[key]))
|
||||||
|
if len(overrided) > 0:
|
||||||
|
logging.info("已根据override.json覆写配置项: {}".format(", ".join(overrided)))
|
||||||
|
|
||||||
|
|
||||||
|
def complete_tips():
|
||||||
|
"""根据tips-custom-template模块补全tips模块的属性"""
|
||||||
|
non_exist_keys = []
|
||||||
|
|
||||||
|
is_integrity = True
|
||||||
|
logging.debug("检查tips模块完整性.")
|
||||||
|
tips_template = importlib.import_module('tips-custom-template')
|
||||||
|
tips = importlib.import_module('tips')
|
||||||
|
for key in dir(tips_template):
|
||||||
|
if not key.startswith("__") and not hasattr(tips, key):
|
||||||
|
setattr(tips, key, getattr(tips_template, key))
|
||||||
|
# logging.warning("[{}]不存在".format(key))
|
||||||
|
non_exist_keys.append(key)
|
||||||
|
is_integrity = False
|
||||||
|
|
||||||
|
if not is_integrity:
|
||||||
|
logging.warning("以下提示语字段不存在: {}".format(", ".join(non_exist_keys)))
|
||||||
|
logging.warning("tips模块不完整,您可以依据tips-custom-template.py检查tips.py")
|
||||||
|
logging.warning("以上配置已被设为默认值,将在3秒后继续启动... ")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_process(first_time_init=False):
|
||||||
|
"""启动流程,reload之后会被执行"""
|
||||||
|
|
||||||
global known_exception_caught
|
global known_exception_caught
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
|
# 计算host和instance标识符
|
||||||
|
import pkg.audit.identifier
|
||||||
|
pkg.audit.identifier.init()
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
cfg_inst: pymodule_cfg.PythonModuleConfigFile = pymodule_cfg.PythonModuleConfigFile(
|
||||||
|
'config.py',
|
||||||
|
'config-template.py'
|
||||||
|
)
|
||||||
|
await config_mgr.ConfigManager(cfg_inst).load_config()
|
||||||
|
|
||||||
|
override_config_manager()
|
||||||
|
|
||||||
|
# 检查tips模块
|
||||||
|
complete_tips()
|
||||||
|
|
||||||
|
cfg = pkg.utils.context.get_config_manager().data
|
||||||
|
|
||||||
|
# 更新openai库到最新版本
|
||||||
|
if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
|
||||||
|
print("正在更新依赖库,请等待...")
|
||||||
|
if 'upgrade_dependencies' not in cfg:
|
||||||
|
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
|
||||||
|
else:
|
||||||
|
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
|
||||||
|
try:
|
||||||
|
ensure_dependencies()
|
||||||
|
except Exception as e:
|
||||||
|
print("更新openai库失败:{}, 请忽略或自行更新".format(e))
|
||||||
|
|
||||||
known_exception_caught = False
|
known_exception_caught = False
|
||||||
try:
|
try:
|
||||||
# 导入config.py
|
try:
|
||||||
assert os.path.exists('config.py')
|
|
||||||
|
|
||||||
config = importlib.import_module('config')
|
sh = reset_logging()
|
||||||
|
pkg.utils.context.context['logger_handler'] = sh
|
||||||
|
|
||||||
import pkg.utils.context
|
# 初始化文字转图片
|
||||||
pkg.utils.context.set_config(config)
|
from pkg.utils import text2img
|
||||||
|
text2img.initialize()
|
||||||
|
|
||||||
if pkg.utils.context.context['logger_handler'] is not None:
|
# 检查是否设置了管理员
|
||||||
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
|
if cfg['admin_qq'] == 0:
|
||||||
|
# logging.warning("未设置管理员QQ,管理员权限命令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限命令及运行告警将无法使用,请输入管理员QQ号: "))
|
||||||
|
# 写入到文件
|
||||||
|
|
||||||
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
|
# 读取文件
|
||||||
filename='qchatgpt.log', # log日志输出的文件位置和文件名
|
config_file_str = ""
|
||||||
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
|
with open("config.py", "r", encoding="utf-8") as f:
|
||||||
# 日志输出的格式
|
config_file_str = f.read()
|
||||||
# -8表示占位符,让输出左对齐,输出长度都为8位
|
# 替换
|
||||||
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
config_file_str = config_file_str.replace("admin_qq = 0", "admin_qq = " + str(cfg['admin_qq']))
|
||||||
)
|
# 写入
|
||||||
sh = logging.StreamHandler()
|
with open("config.py", "w", encoding="utf-8") as f:
|
||||||
sh.setLevel(config.logging_level)
|
f.write(config_file_str)
|
||||||
sh.setFormatter(colorlog.ColoredFormatter(
|
|
||||||
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
|
||||||
"%(message)s",
|
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
|
||||||
log_colors=log_colors_config
|
|
||||||
))
|
|
||||||
logging.getLogger().addHandler(sh)
|
|
||||||
|
|
||||||
# 检查是否设置了管理员
|
print("管理员QQ已设置,如需修改请修改config.py中的admin_qq字段")
|
||||||
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
|
time.sleep(4)
|
||||||
logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
|
break
|
||||||
|
except ValueError:
|
||||||
|
print("请输入数字")
|
||||||
|
|
||||||
|
# 初始化中央服务器 API 交互实例
|
||||||
|
from pkg.utils.center import apigroup
|
||||||
|
from pkg.utils.center import v2 as center_v2
|
||||||
|
|
||||||
import pkg.openai.manager
|
center_v2_api = center_v2.V2CenterAPI(
|
||||||
import pkg.database.manager
|
basic_info={
|
||||||
import pkg.openai.session
|
"host_id": pkg.audit.identifier.identifier['host_id'],
|
||||||
import pkg.qqbot.manager
|
"instance_id": pkg.audit.identifier.identifier['instance_id'],
|
||||||
|
"semantic_version": pkg.utils.updater.get_current_tag(),
|
||||||
|
"platform": sys.platform,
|
||||||
|
},
|
||||||
|
runtime_info={
|
||||||
|
"admin_id": "{}".format(cfg['admin_qq']),
|
||||||
|
"msg_source": cfg['msg_source_adapter'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pkg.utils.context.set_center_v2_api(center_v2_api)
|
||||||
|
|
||||||
pkg.utils.context.context['logger_handler'] = sh
|
import pkg.openai.manager
|
||||||
# 主启动流程
|
import pkg.database.manager
|
||||||
database = pkg.database.manager.DatabaseManager()
|
import pkg.openai.session
|
||||||
|
import pkg.qqbot.manager
|
||||||
|
import pkg.openai.dprompt
|
||||||
|
import pkg.qqbot.cmds.aamgr
|
||||||
|
|
||||||
|
try:
|
||||||
|
pkg.openai.dprompt.register_all()
|
||||||
|
pkg.qqbot.cmds.aamgr.register_all()
|
||||||
|
pkg.qqbot.cmds.aamgr.apply_privileges()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(e)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
database.initialize_database()
|
# 配置OpenAI proxy
|
||||||
|
import openai
|
||||||
|
openai.proxies = None # 先重置,因为重载后可能需要清除proxy
|
||||||
|
if "http_proxy" in cfg['openai_config'] and cfg['openai_config']["http_proxy"] is not None:
|
||||||
|
openai.proxies = {
|
||||||
|
"http": cfg['openai_config']["http_proxy"],
|
||||||
|
"https": cfg['openai_config']["http_proxy"]
|
||||||
|
}
|
||||||
|
|
||||||
openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key'])
|
# 配置openai api_base
|
||||||
|
if "reverse_proxy" in cfg['openai_config'] and cfg['openai_config']["reverse_proxy"] is not None:
|
||||||
|
logging.debug("设置反向代理: "+cfg['openai_config']['reverse_proxy'])
|
||||||
|
openai.base_url = cfg['openai_config']["reverse_proxy"]
|
||||||
|
|
||||||
# 加载所有未超时的session
|
# 主启动流程
|
||||||
pkg.openai.session.load_sessions()
|
database = pkg.database.manager.DatabaseManager()
|
||||||
|
|
||||||
# 初始化qq机器人
|
database.initialize_database()
|
||||||
qqbot = pkg.qqbot.manager.QQBotManager(mirai_http_api_config=config.mirai_http_api_config,
|
|
||||||
timeout=config.process_message_timeout, retry=config.retry_times,
|
|
||||||
first_time_init=first_time_init)
|
|
||||||
|
|
||||||
if first_time_init: # 不是热重载之后的启动,则不启动新的bot线程
|
openai_interact = pkg.openai.manager.OpenAIInteract(cfg['openai_config']['api_key'])
|
||||||
|
|
||||||
def run_bot_wrapper():
|
# 加载所有未超时的session
|
||||||
global known_exception_caught
|
pkg.openai.session.load_sessions()
|
||||||
try:
|
|
||||||
qqbot.bot.run()
|
# 初始化qq机器人
|
||||||
except TypeError as e:
|
qqbot = pkg.qqbot.manager.QQBotManager(first_time_init=first_time_init)
|
||||||
if str(e).__contains__("argument 'debug'"):
|
|
||||||
|
# 加载插件
|
||||||
|
import pkg.plugin.host
|
||||||
|
pkg.plugin.host.load_plugins()
|
||||||
|
|
||||||
|
pkg.plugin.host.initialize_plugins()
|
||||||
|
|
||||||
|
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
|
||||||
|
|
||||||
|
import mirai.exceptions
|
||||||
|
|
||||||
|
def run_bot_wrapper():
|
||||||
|
global known_exception_caught
|
||||||
|
try:
|
||||||
|
logging.debug("使用账号: {}".format(qqbot.bot_account_id))
|
||||||
|
qqbot.adapter.run_sync()
|
||||||
|
except TypeError as e:
|
||||||
|
if str(e).__contains__("argument 'debug'"):
|
||||||
|
logging.error(
|
||||||
|
"连接bot失败:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/82".format(e))
|
||||||
|
known_exception_caught = True
|
||||||
|
elif str(e).__contains__("As of 3.10, the *loop*"):
|
||||||
|
logging.error(
|
||||||
|
"Websockets版本过低:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/5".format(e))
|
||||||
|
known_exception_caught = True
|
||||||
|
|
||||||
|
except websockets.exceptions.InvalidStatus as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
"连接bot失败:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/82".format(e))
|
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||||
known_exception_caught = True
|
|
||||||
elif str(e).__contains__("As of 3.10, the *loop*"):
|
|
||||||
logging.error(
|
|
||||||
"Websockets版本过低:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/5".format(e))
|
|
||||||
known_exception_caught = True
|
|
||||||
|
|
||||||
except websockets.exceptions.InvalidStatus as e:
|
|
||||||
logging.error(
|
|
||||||
"mirai-api-http端口无法使用:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
|
||||||
e))
|
|
||||||
known_exception_caught = True
|
|
||||||
except mirai.exceptions.NetworkError as e:
|
|
||||||
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".format(e))
|
|
||||||
known_exception_caught = True
|
|
||||||
except Exception as e:
|
|
||||||
if str(e).__contains__("404"):
|
|
||||||
logging.error(
|
|
||||||
"mirai-api-http端口无法使用:{}, 请查看 https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
|
||||||
e))
|
e))
|
||||||
known_exception_caught = True
|
known_exception_caught = True
|
||||||
else:
|
except mirai.exceptions.NetworkError as e:
|
||||||
logging.error(
|
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".format(e))
|
||||||
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/issues 查找或提issue".format(e))
|
|
||||||
known_exception_caught = True
|
known_exception_caught = True
|
||||||
raise e
|
except Exception as e:
|
||||||
|
if str(e).__contains__("404"):
|
||||||
qq_bot_thread = threading.Thread(target=run_bot_wrapper, args=(), daemon=True)
|
logging.error(
|
||||||
qq_bot_thread.start()
|
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||||
|
e))
|
||||||
|
known_exception_caught = True
|
||||||
|
elif str(e).__contains__("signal only works in main thread"):
|
||||||
|
logging.error(
|
||||||
|
"hypercorn异常:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/86".format(
|
||||||
|
e))
|
||||||
|
known_exception_caught = True
|
||||||
|
elif str(e).__contains__("did not receive a valid HTTP"):
|
||||||
|
logging.error(
|
||||||
|
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||||
|
e))
|
||||||
|
else:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
logging.error(
|
||||||
|
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
|
||||||
|
known_exception_caught = True
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
time.sleep(12)
|
||||||
|
threading.Thread(
|
||||||
|
target=run_bot_wrapper
|
||||||
|
).start()
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
if isinstance(e, KeyboardInterrupt):
|
||||||
|
logging.info("程序被用户中止")
|
||||||
|
sys.exit(0)
|
||||||
|
elif isinstance(e, SyntaxError):
|
||||||
|
logging.error("配置文件存在语法错误,请检查配置文件:\n1. 是否存在中文符号\n2. 是否已按照文件中的说明填写正确")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
logging.error("初始化失败:{}".format(e))
|
||||||
|
sys.exit(1)
|
||||||
finally:
|
finally:
|
||||||
|
# 判断若是Windows,输出选择模式可能会暂停程序的警告
|
||||||
|
if os.name == 'nt':
|
||||||
|
time.sleep(2)
|
||||||
|
logging.info("您正在使用Windows系统,若命令行窗口处于“选择”模式,程序可能会被暂停,此时请右键点击窗口空白区域使其取消选择模式。")
|
||||||
|
|
||||||
time.sleep(12)
|
time.sleep(12)
|
||||||
|
|
||||||
if first_time_init:
|
if first_time_init:
|
||||||
if not known_exception_caught:
|
if not known_exception_caught:
|
||||||
logging.info('程序启动完成,如长时间未显示 ”成功登录到账号xxxxx“ ,并且不回复消息,请查看 '
|
if cfg['msg_source_adapter'] == "yirimirai":
|
||||||
'https://github.com/RockChinQ/QChatGPT/issues/37')
|
logging.info("QQ: {}, MAH: {}".format(cfg['mirai_http_api_config']['qq'], cfg['mirai_http_api_config']['host']+":"+str(cfg['mirai_http_api_config']['port'])))
|
||||||
|
logging.critical('程序启动完成,如长时间未显示 "成功登录到账号xxxxx" ,并且不回复消息,解决办法(请勿到群里问): '
|
||||||
|
'https://github.com/RockChinQ/QChatGPT/issues/37')
|
||||||
|
elif cfg['msg_source_adapter'] == 'nakuru':
|
||||||
|
logging.info("host: {}, port: {}, http_port: {}".format(cfg['nakuru_config']['host'], cfg['nakuru_config']['port'], cfg['nakuru_config']['http_port']))
|
||||||
|
logging.critical('程序启动完成,如长时间未显示 "Protocol: connected" ,并且不回复消息,请检查config.py中的nakuru_config是否正确')
|
||||||
else:
|
else:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
logging.info('热重载完成')
|
logging.info('热重载完成')
|
||||||
|
|
||||||
while True:
|
# 发送赞赏码
|
||||||
try:
|
if cfg['encourage_sponsor_at_start'] \
|
||||||
time.sleep(10)
|
and pkg.utils.context.get_openai_manager().audit_mgr.get_total_text_length() >= 2048:
|
||||||
if qqbot != pkg.utils.context.get_qqbot_manager(): # 已经reload了
|
|
||||||
logging.info("以前的main流程由于reload退出")
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
stop()
|
|
||||||
|
|
||||||
print("程序退出")
|
logging.info("发送赞赏码")
|
||||||
sys.exit(0)
|
from mirai import MessageChain, Plain, Image
|
||||||
|
import pkg.utils.constants
|
||||||
|
message_chain = MessageChain([
|
||||||
|
Plain("自2022年12月初以来,开发者已经花费了大量时间和精力来维护本项目,如果您觉得本项目对您有帮助,欢迎赞赏开发者,"
|
||||||
|
"以支持项目稳定运行😘"),
|
||||||
|
Image(base64=pkg.utils.constants.alipay_qr_b64),
|
||||||
|
Image(base64=pkg.utils.constants.wechat_qr_b64),
|
||||||
|
Plain("BTC: 3N4Azee63vbBB9boGv9Rjf4N5SocMe5eCq\nXMR: 89LS21EKQuDGkyQoe2nDupiuWXk4TVD6FALvSKv5owfmeJEPFpHeMsZLYtLiJ6GxLrhsRe5gMs6MyMSDn4GNQAse2Mae4KE\n\n"),
|
||||||
|
Plain("(本消息仅在启动时发送至管理员,如果您不想再看到此消息,请在config.py中将encourage_sponsor_at_start设置为False)")
|
||||||
|
])
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin_message_chain(message_chain)
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
import pkg.utils.updater
|
||||||
|
try:
|
||||||
|
if pkg.utils.updater.is_new_version_available():
|
||||||
|
logging.info("新版本可用,请发送 !update 进行自动更新\n更新日志:\n{}".format("\n".join(pkg.utils.updater.get_rls_notes())))
|
||||||
|
else:
|
||||||
|
# logging.info("当前已是最新版本")
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning("检查更新失败:{}".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pkg.utils.announcement as announcement
|
||||||
|
new_announcement = announcement.fetch_new()
|
||||||
|
if len(new_announcement) > 0:
|
||||||
|
for announcement in new_announcement:
|
||||||
|
logging.critical("[公告]<{}> {}".format(announcement['time'], announcement['content']))
|
||||||
|
|
||||||
|
# 发送统计数据
|
||||||
|
pkg.utils.context.get_center_v2_api().main.post_announcement_showed(
|
||||||
|
[announcement['id'] for announcement in new_announcement]
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning("获取公告失败:{}".format(e))
|
||||||
|
|
||||||
|
return qqbot
|
||||||
|
|
||||||
def stop():
|
def stop():
|
||||||
import pkg.utils.context
|
|
||||||
import pkg.qqbot.manager
|
import pkg.qqbot.manager
|
||||||
import pkg.openai.session
|
import pkg.openai.session
|
||||||
try:
|
try:
|
||||||
|
import pkg.plugin.host
|
||||||
|
pkg.plugin.host.unload_plugins()
|
||||||
|
|
||||||
qqbot_inst = pkg.utils.context.get_qqbot_manager()
|
qqbot_inst = pkg.utils.context.get_qqbot_manager()
|
||||||
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
|
assert isinstance(qqbot_inst, pkg.qqbot.manager.QQBotManager)
|
||||||
|
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.dump_fee()
|
|
||||||
for session in pkg.openai.session.sessions:
|
for session in pkg.openai.session.sessions:
|
||||||
logging.info('持久化session: %s', session)
|
logging.info('持久化session: %s', session)
|
||||||
pkg.openai.session.sessions[session].persistence()
|
pkg.openai.session.sessions[session].persistence()
|
||||||
@@ -176,29 +428,69 @@ def stop():
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def main():
|
||||||
# 检查是否有config.py,如果没有就把config-template.py复制一份,并退出程序
|
global use_override
|
||||||
if not os.path.exists('config.py'):
|
# 检查是否携带了 --override 或 -r 参数
|
||||||
shutil.copy('config-template.py', 'config.py')
|
if '--override' in sys.argv or '-r' in sys.argv:
|
||||||
print('请先在config.py中填写配置')
|
use_override = True
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
# 初始化logging
|
||||||
|
init_runtime_log_file()
|
||||||
|
pkg.utils.context.context['logger_handler'] = reset_logging()
|
||||||
|
|
||||||
|
# 配置线程池
|
||||||
|
from pkg.utils import ThreadCtl
|
||||||
|
thread_ctl = ThreadCtl(
|
||||||
|
sys_pool_num=8,
|
||||||
|
admin_pool_num=4,
|
||||||
|
user_pool_num=8
|
||||||
|
)
|
||||||
|
# 存进上下文
|
||||||
|
pkg.utils.context.set_thread_ctl(thread_ctl)
|
||||||
|
|
||||||
|
# 启动指令处理
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
|
if len(sys.argv) > 1 and sys.argv[1] == 'init_db':
|
||||||
init_db()
|
init_db()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
|
elif len(sys.argv) > 1 and sys.argv[1] == 'update':
|
||||||
try:
|
print("正在进行程序更新...")
|
||||||
from dulwich import porcelain
|
import pkg.utils.updater as updater
|
||||||
|
updater.update_all(cli=True)
|
||||||
repo = porcelain.open_repo('.')
|
|
||||||
porcelain.pull(repo)
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
print("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# import pkg.utils.configmgr
|
# 关闭urllib的http警告
|
||||||
#
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||||
# pkg.utils.configmgr.set_config_and_reload("quote_origin", False)
|
|
||||||
|
def run_wrapper():
|
||||||
|
asyncio.run(start_process(True))
|
||||||
|
|
||||||
|
pkg.utils.context.get_thread_ctl().submit_sys_task(
|
||||||
|
run_wrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
# 主线程循环
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
time.sleep(0xFF)
|
||||||
|
except:
|
||||||
|
stop()
|
||||||
|
pkg.utils.context.get_thread_ctl().shutdown()
|
||||||
|
|
||||||
|
launch_args = sys.argv.copy()
|
||||||
|
|
||||||
|
if "--cov-report" not in launch_args:
|
||||||
|
import platform
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
cmd = "taskkill /F /PID {}".format(os.getpid())
|
||||||
|
elif platform.system() in ['Linux', 'Darwin']:
|
||||||
|
cmd = "kill -9 {}".format(os.getpid())
|
||||||
|
os.system(cmd)
|
||||||
|
else:
|
||||||
|
print("正常退出以生成覆盖率报告")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
main(True)
|
|
||||||
|
|||||||
90
override-all.json
Normal file
90
override-all.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271",
|
||||||
|
"msg_source_adapter": "yirimirai",
|
||||||
|
"mirai_http_api_config": {
|
||||||
|
"adapter": "WebSocketAdapter",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 8080,
|
||||||
|
"verifyKey": "yirimirai",
|
||||||
|
"qq": 1234567890
|
||||||
|
},
|
||||||
|
"nakuru_config": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 6700,
|
||||||
|
"http_port": 5700,
|
||||||
|
"token": ""
|
||||||
|
},
|
||||||
|
"openai_config": {
|
||||||
|
"api_key": {
|
||||||
|
"default": "openai_api_key"
|
||||||
|
},
|
||||||
|
"http_proxy": null,
|
||||||
|
"reverse_proxy": null
|
||||||
|
},
|
||||||
|
"switch_strategy": "active",
|
||||||
|
"admin_qq": 0,
|
||||||
|
"default_prompt": {
|
||||||
|
"default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。"
|
||||||
|
},
|
||||||
|
"preset_mode": "normal",
|
||||||
|
"response_rules": {
|
||||||
|
"default": {
|
||||||
|
"at": true,
|
||||||
|
"prefix": [
|
||||||
|
"/ai",
|
||||||
|
"!ai",
|
||||||
|
"!ai",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"regexp": [],
|
||||||
|
"random_rate": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignore_rules": {
|
||||||
|
"prefix": [
|
||||||
|
"/"
|
||||||
|
],
|
||||||
|
"regexp": []
|
||||||
|
},
|
||||||
|
"income_msg_check": false,
|
||||||
|
"sensitive_word_filter": true,
|
||||||
|
"baidu_check": false,
|
||||||
|
"baidu_api_key": "",
|
||||||
|
"baidu_secret_key": "",
|
||||||
|
"inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规",
|
||||||
|
"encourage_sponsor_at_start": true,
|
||||||
|
"prompt_submit_length": 3072,
|
||||||
|
"auto_reset": true,
|
||||||
|
"completion_api_params": {
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
"temperature": 0.9
|
||||||
|
},
|
||||||
|
"image_api_params": {
|
||||||
|
"model": "dall-e-2",
|
||||||
|
"size": "256x256"
|
||||||
|
},
|
||||||
|
"trace_function_calls": false,
|
||||||
|
"quote_origin": false,
|
||||||
|
"at_sender": false,
|
||||||
|
"include_image_description": true,
|
||||||
|
"process_message_timeout": 120,
|
||||||
|
"show_prefix": false,
|
||||||
|
"force_delay_range": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"blob_message_threshold": 256,
|
||||||
|
"blob_message_strategy": "forward",
|
||||||
|
"wait_last_done": true,
|
||||||
|
"font_path": "",
|
||||||
|
"retry_times": 3,
|
||||||
|
"hide_exce_info_to_user": false,
|
||||||
|
"session_expire_time": 1200,
|
||||||
|
"rate_limitation": {
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
|
"rate_limit_strategy": "drop",
|
||||||
|
"upgrade_dependencies": false,
|
||||||
|
"report_usage": true,
|
||||||
|
"logging_level": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
审计相关操作
|
||||||
|
"""
|
||||||
@@ -1,42 +1,49 @@
|
|||||||
|
"""
|
||||||
|
使用量统计以及数据上报功能实现
|
||||||
|
"""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import pkg.utils.context
|
from ..utils import context
|
||||||
|
from ..utils import updater
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
|
|
||||||
class DataGatherer:
|
class DataGatherer:
|
||||||
"""数据收集器"""
|
"""数据收集器"""
|
||||||
|
|
||||||
usage = {}
|
usage = {}
|
||||||
"""以key值md5为key,{
|
"""各api-key的使用量
|
||||||
|
|
||||||
|
以key值md5为key,{
|
||||||
"text": {
|
"text": {
|
||||||
"text-davinci-003": 文字量:int,
|
"gpt-3.5-turbo": 文字量:int,
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"256x256": 图片数量:int,
|
"256x256": 图片数量:int,
|
||||||
}
|
}
|
||||||
}为值的字典"""
|
}为值的字典"""
|
||||||
|
|
||||||
|
version_str = "undetermined"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.load_from_db()
|
self.load_from_db()
|
||||||
|
|
||||||
def report_to_server(self, subservice_name: str, count: int):
|
|
||||||
try:
|
try:
|
||||||
config = pkg.utils.context.get_config()
|
self.version_str = updater.get_current_tag() # 从updater模块获取版本号
|
||||||
if hasattr(config, "report_usage") and not config.report_usage:
|
|
||||||
return
|
|
||||||
res = requests.get("http://rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, version, count))
|
|
||||||
if res.status_code != 200 or res.text != "ok":
|
|
||||||
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
|
|
||||||
except:
|
except:
|
||||||
return
|
pass
|
||||||
|
|
||||||
def report_text_model_usage(self, model, text):
|
def get_usage(self, key_md5):
|
||||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
return self.usage[key_md5] if key_md5 in self.usage else {}
|
||||||
|
|
||||||
|
def report_text_model_usage(self, model, total_tokens):
|
||||||
|
"""调用方报告文字模型请求文字使用量"""
|
||||||
|
|
||||||
|
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
|
||||||
|
|
||||||
if key_md5 not in self.usage:
|
if key_md5 not in self.usage:
|
||||||
self.usage[key_md5] = {}
|
self.usage[key_md5] = {}
|
||||||
@@ -47,14 +54,14 @@ class DataGatherer:
|
|||||||
if model not in self.usage[key_md5]["text"]:
|
if model not in self.usage[key_md5]["text"]:
|
||||||
self.usage[key_md5]["text"][model] = 0
|
self.usage[key_md5]["text"][model] = 0
|
||||||
|
|
||||||
length = int((len(text.encode('utf-8')) - len(text)) / 2 + len(text))
|
length = total_tokens
|
||||||
self.usage[key_md5]["text"][model] += length
|
self.usage[key_md5]["text"][model] += length
|
||||||
self.dump_to_db()
|
self.dump_to_db()
|
||||||
|
|
||||||
self.report_to_server("text", length)
|
|
||||||
|
|
||||||
def report_image_model_usage(self, size):
|
def report_image_model_usage(self, size):
|
||||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
"""调用方报告图片模型请求图片使用量"""
|
||||||
|
|
||||||
|
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||||
|
|
||||||
if key_md5 not in self.usage:
|
if key_md5 not in self.usage:
|
||||||
self.usage[key_md5] = {}
|
self.usage[key_md5] = {}
|
||||||
@@ -68,9 +75,8 @@ class DataGatherer:
|
|||||||
self.usage[key_md5]["image"][size] += 1
|
self.usage[key_md5]["image"][size] += 1
|
||||||
self.dump_to_db()
|
self.dump_to_db()
|
||||||
|
|
||||||
self.report_to_server("image", 1)
|
|
||||||
|
|
||||||
def get_text_length_of_key(self, key):
|
def get_text_length_of_key(self, key):
|
||||||
|
"""获取指定api-key (明文) 的文字总使用量(本地记录)"""
|
||||||
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||||
if key_md5 not in self.usage:
|
if key_md5 not in self.usage:
|
||||||
return 0
|
return 0
|
||||||
@@ -80,6 +86,8 @@ class DataGatherer:
|
|||||||
return sum(self.usage[key_md5]["text"].values())
|
return sum(self.usage[key_md5]["text"].values())
|
||||||
|
|
||||||
def get_image_count_of_key(self, key):
|
def get_image_count_of_key(self, key):
|
||||||
|
"""获取指定api-key (明文) 的图片总使用量(本地记录)"""
|
||||||
|
|
||||||
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||||
if key_md5 not in self.usage:
|
if key_md5 not in self.usage:
|
||||||
return 0
|
return 0
|
||||||
@@ -88,10 +96,19 @@ class DataGatherer:
|
|||||||
# 遍历其中所有模型,求和
|
# 遍历其中所有模型,求和
|
||||||
return sum(self.usage[key_md5]["image"].values())
|
return sum(self.usage[key_md5]["image"].values())
|
||||||
|
|
||||||
|
def get_total_text_length(self):
|
||||||
|
"""获取所有api-key的文字总使用量(本地记录)"""
|
||||||
|
total = 0
|
||||||
|
for key in self.usage:
|
||||||
|
if "text" not in self.usage[key]:
|
||||||
|
continue
|
||||||
|
total += sum(self.usage[key]["text"].values())
|
||||||
|
return total
|
||||||
|
|
||||||
def dump_to_db(self):
|
def dump_to_db(self):
|
||||||
pkg.utils.context.get_database_manager().dump_usage_json(self.usage)
|
context.get_database_manager().dump_usage_json(self.usage)
|
||||||
|
|
||||||
def load_from_db(self):
|
def load_from_db(self):
|
||||||
json_str = pkg.utils.context.get_database_manager().load_usage_json()
|
json_str = context.get_database_manager().load_usage_json()
|
||||||
if json_str is not None:
|
if json_str is not None:
|
||||||
self.usage = json.loads(json_str)
|
self.usage = json.loads(json_str)
|
||||||
|
|||||||
83
pkg/audit/identifier.py
Normal file
83
pkg/audit/identifier.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
identifier = {
|
||||||
|
'host_id': '',
|
||||||
|
'instance_id': '',
|
||||||
|
'host_create_ts': 0,
|
||||||
|
'instance_create_ts': 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
HOST_ID_FILE = os.path.expanduser('~/.qchatgpt/host_id.json')
|
||||||
|
INSTANCE_ID_FILE = 'res/instance_id.json'
|
||||||
|
|
||||||
|
def init():
|
||||||
|
global identifier
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.expanduser('~/.qchatgpt')):
|
||||||
|
os.mkdir(os.path.expanduser('~/.qchatgpt'))
|
||||||
|
|
||||||
|
if not os.path.exists(HOST_ID_FILE):
|
||||||
|
new_host_id = 'host_'+str(uuid.uuid4())
|
||||||
|
new_host_create_ts = int(time.time())
|
||||||
|
|
||||||
|
with open(HOST_ID_FILE, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'host_id': new_host_id,
|
||||||
|
'host_create_ts': new_host_create_ts
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
identifier['host_id'] = new_host_id
|
||||||
|
identifier['host_create_ts'] = new_host_create_ts
|
||||||
|
else:
|
||||||
|
loaded_host_id = ''
|
||||||
|
loaded_host_create_ts = 0
|
||||||
|
|
||||||
|
with open(HOST_ID_FILE, 'r') as f:
|
||||||
|
file_content = json.load(f)
|
||||||
|
loaded_host_id = file_content['host_id']
|
||||||
|
loaded_host_create_ts = file_content['host_create_ts']
|
||||||
|
|
||||||
|
identifier['host_id'] = loaded_host_id
|
||||||
|
identifier['host_create_ts'] = loaded_host_create_ts
|
||||||
|
|
||||||
|
# 检查实例 id
|
||||||
|
if os.path.exists(INSTANCE_ID_FILE):
|
||||||
|
instance_id = {}
|
||||||
|
with open(INSTANCE_ID_FILE, 'r') as f:
|
||||||
|
instance_id = json.load(f)
|
||||||
|
|
||||||
|
if instance_id['host_id'] != identifier['host_id']: # 如果实例 id 不是当前主机的,删除
|
||||||
|
os.remove(INSTANCE_ID_FILE)
|
||||||
|
|
||||||
|
if not os.path.exists(INSTANCE_ID_FILE):
|
||||||
|
new_instance_id = 'instance_'+str(uuid.uuid4())
|
||||||
|
new_instance_create_ts = int(time.time())
|
||||||
|
|
||||||
|
with open(INSTANCE_ID_FILE, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'host_id': identifier['host_id'],
|
||||||
|
'instance_id': new_instance_id,
|
||||||
|
'instance_create_ts': new_instance_create_ts
|
||||||
|
}, f)
|
||||||
|
|
||||||
|
identifier['instance_id'] = new_instance_id
|
||||||
|
identifier['instance_create_ts'] = new_instance_create_ts
|
||||||
|
else:
|
||||||
|
loaded_instance_id = ''
|
||||||
|
loaded_instance_create_ts = 0
|
||||||
|
|
||||||
|
with open(INSTANCE_ID_FILE, 'r') as f:
|
||||||
|
file_content = json.load(f)
|
||||||
|
loaded_instance_id = file_content['instance_id']
|
||||||
|
loaded_instance_create_ts = file_content['instance_create_ts']
|
||||||
|
|
||||||
|
identifier['instance_id'] = loaded_instance_id
|
||||||
|
identifier['instance_create_ts'] = loaded_instance_create_ts
|
||||||
|
|
||||||
|
def print_out():
|
||||||
|
global identifier
|
||||||
|
print(identifier)
|
||||||
0
pkg/config/__init__.py
Normal file
0
pkg/config/__init__.py
Normal file
62
pkg/config/impls/pymodule.py
Normal file
62
pkg/config/impls/pymodule.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .. import model as file_model
|
||||||
|
|
||||||
|
|
||||||
|
class PythonModuleConfigFile(file_model.ConfigFile):
|
||||||
|
"""Python模块配置文件"""
|
||||||
|
|
||||||
|
config_file_name: str = None
|
||||||
|
"""配置文件名"""
|
||||||
|
|
||||||
|
template_file_name: str = None
|
||||||
|
"""模板文件名"""
|
||||||
|
|
||||||
|
def __init__(self, config_file_name: str, template_file_name: str) -> None:
|
||||||
|
self.config_file_name = config_file_name
|
||||||
|
self.template_file_name = template_file_name
|
||||||
|
|
||||||
|
def exists(self) -> bool:
|
||||||
|
return os.path.exists(self.config_file_name)
|
||||||
|
|
||||||
|
async def create(self):
|
||||||
|
shutil.copyfile(self.template_file_name, self.config_file_name)
|
||||||
|
|
||||||
|
async def load(self) -> dict:
|
||||||
|
module_name = os.path.splitext(os.path.basename(self.config_file_name))[0]
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
cfg = {}
|
||||||
|
|
||||||
|
allowed_types = (int, float, str, bool, list, dict)
|
||||||
|
|
||||||
|
for key in dir(module):
|
||||||
|
if key.startswith('__'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(getattr(module, key), allowed_types):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cfg[key] = getattr(module, key)
|
||||||
|
|
||||||
|
# 从模板模块文件中进行补全
|
||||||
|
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
for key in dir(module):
|
||||||
|
if key.startswith('__'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not isinstance(getattr(module, key), allowed_types):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if key not in cfg:
|
||||||
|
cfg[key] = getattr(module, key)
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
async def save(self, data: dict):
|
||||||
|
logging.warning('Python模块配置文件不支持保存')
|
||||||
23
pkg/config/manager.py
Normal file
23
pkg/config/manager.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from . import model as file_model
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""配置文件管理器"""
|
||||||
|
|
||||||
|
file: file_model.ConfigFile = None
|
||||||
|
"""配置文件实例"""
|
||||||
|
|
||||||
|
data: dict = None
|
||||||
|
"""配置数据"""
|
||||||
|
|
||||||
|
def __init__(self, cfg_file: file_model.ConfigFile) -> None:
|
||||||
|
self.file = cfg_file
|
||||||
|
self.data = {}
|
||||||
|
context.set_config_manager(self)
|
||||||
|
|
||||||
|
async def load_config(self):
|
||||||
|
self.data = await self.file.load()
|
||||||
|
|
||||||
|
async def dump_config(self):
|
||||||
|
await self.file.save(self.data)
|
||||||
27
pkg/config/model.py
Normal file
27
pkg/config/model.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFile(metaclass=abc.ABCMeta):
|
||||||
|
"""配置文件抽象类"""
|
||||||
|
|
||||||
|
config_file_name: str = None
|
||||||
|
"""配置文件名"""
|
||||||
|
|
||||||
|
template_file_name: str = None
|
||||||
|
"""模板文件名"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def exists(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def create(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def load(self) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def save(self, data: dict):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
数据库操作封装
|
||||||
|
"""
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
|
"""
|
||||||
|
数据库管理模块
|
||||||
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from sqlite3 import Cursor
|
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
import pkg.utils.context
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
# 数据库管理
|
|
||||||
# 为其他模块提供数据库操作接口
|
|
||||||
class DatabaseManager:
|
class DatabaseManager:
|
||||||
|
"""封装数据库底层操作,并提供方法给上层使用"""
|
||||||
|
|
||||||
conn = None
|
conn = None
|
||||||
cursor = None
|
cursor = None
|
||||||
|
|
||||||
@@ -19,25 +21,29 @@ class DatabaseManager:
|
|||||||
|
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
pkg.utils.context.set_database_manager(self)
|
context.set_database_manager(self)
|
||||||
|
|
||||||
# 连接到数据库文件
|
# 连接到数据库文件
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
|
"""连接到数据库"""
|
||||||
self.conn = sqlite3.connect('database.db', check_same_thread=False)
|
self.conn = sqlite3.connect('database.db', check_same_thread=False)
|
||||||
self.cursor = self.conn.cursor()
|
self.cursor = self.conn.cursor()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
||||||
def execute(self, *args, **kwargs) -> Cursor:
|
def __execute__(self, *args, **kwargs) -> sqlite3.Cursor:
|
||||||
# logging.debug('SQL: {}'.format(sql))
|
# logging.debug('SQL: {}'.format(sql))
|
||||||
|
logging.debug('SQL: {}'.format(args))
|
||||||
c = self.cursor.execute(*args, **kwargs)
|
c = self.cursor.execute(*args, **kwargs)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
return c
|
return c
|
||||||
|
|
||||||
# 初始化数据库的函数
|
# 初始化数据库的函数
|
||||||
def initialize_database(self):
|
def initialize_database(self):
|
||||||
self.execute("""
|
"""创建数据表"""
|
||||||
|
|
||||||
|
self.__execute__("""
|
||||||
create table if not exists `sessions` (
|
create table if not exists `sessions` (
|
||||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
`name` varchar(255) not null,
|
`name` varchar(255) not null,
|
||||||
@@ -46,20 +52,31 @@ class DatabaseManager:
|
|||||||
`create_timestamp` bigint not null,
|
`create_timestamp` bigint not null,
|
||||||
`last_interact_timestamp` bigint not null,
|
`last_interact_timestamp` bigint not null,
|
||||||
`status` varchar(255) not null default 'on_going',
|
`status` varchar(255) not null default 'on_going',
|
||||||
`prompt` text not null
|
`default_prompt` text not null default '',
|
||||||
|
`prompt` text not null,
|
||||||
|
`token_counts` text not null default '[]'
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# self.execute("""
|
# 检查sessions表是否存在`default_prompt`字段, 检查是否存在`token_counts`字段
|
||||||
# create table if not exists `api_key_usage`(
|
self.__execute__("PRAGMA table_info('sessions')")
|
||||||
# `id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
columns = self.cursor.fetchall()
|
||||||
# `key_md5` varchar(255) not null,
|
has_default_prompt = False
|
||||||
# `timestamp` bigint not null,
|
has_token_counts = False
|
||||||
# `usage` bigint not null
|
for field in columns:
|
||||||
# )
|
if field[1] == 'default_prompt':
|
||||||
# """)
|
has_default_prompt = True
|
||||||
|
if field[1] == 'token_counts':
|
||||||
|
has_token_counts = True
|
||||||
|
if has_default_prompt and has_token_counts:
|
||||||
|
break
|
||||||
|
if not has_default_prompt:
|
||||||
|
self.__execute__("alter table `sessions` add column `default_prompt` text not null default ''")
|
||||||
|
if not has_token_counts:
|
||||||
|
self.__execute__("alter table `sessions` add column `token_counts` text not null default '[]'")
|
||||||
|
|
||||||
self.execute("""
|
|
||||||
|
self.__execute__("""
|
||||||
create table if not exists `account_fee`(
|
create table if not exists `account_fee`(
|
||||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
`key_md5` varchar(255) not null,
|
`key_md5` varchar(255) not null,
|
||||||
@@ -68,68 +85,70 @@ class DatabaseManager:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
create table if not exists `account_usage`(
|
create table if not exists `account_usage`(
|
||||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
`json` text not null
|
`json` text not null
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
print('Database initialized.')
|
# print('Database initialized.')
|
||||||
|
|
||||||
# session持久化
|
# session持久化
|
||||||
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
|
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
|
||||||
last_interact_timestamp: int, prompt: str):
|
last_interact_timestamp: int, prompt: str, default_prompt: str = '', token_counts: str = ''):
|
||||||
|
"""持久化指定session"""
|
||||||
|
|
||||||
# 检查是否已经有了此name和create_timestamp的session
|
# 检查是否已经有了此name和create_timestamp的session
|
||||||
# 如果有,就更新prompt和last_interact_timestamp
|
# 如果有,就更新prompt和last_interact_timestamp
|
||||||
# 如果没有,就插入一条新的记录
|
# 如果没有,就插入一条新的记录
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select count(*) from `sessions` where `type` = '{}' and `number` = {} and `create_timestamp` = {}
|
select count(*) from `sessions` where `type` = '{}' and `number` = {} and `create_timestamp` = {}
|
||||||
""".format(subject_type, subject_number, create_timestamp))
|
""".format(subject_type, subject_number, create_timestamp))
|
||||||
count = self.cursor.fetchone()[0]
|
count = self.cursor.fetchone()[0]
|
||||||
if count == 0:
|
if count == 0:
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`)
|
insert into `sessions` (`name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `default_prompt`, `token_counts`)
|
||||||
values (?, ?, ?, ?, ?, ?)
|
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.execute(sql,
|
self.__execute__(sql,
|
||||||
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
|
("{}_{}".format(subject_type, subject_number), subject_type, subject_number, create_timestamp,
|
||||||
last_interact_timestamp, prompt))
|
last_interact_timestamp, prompt, default_prompt, token_counts))
|
||||||
else:
|
else:
|
||||||
sql = """
|
sql = """
|
||||||
update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?
|
update `sessions` set `last_interact_timestamp` = ?, `prompt` = ?, `token_counts` = ?
|
||||||
where `type` = ? and `number` = ? and `create_timestamp` = ?
|
where `type` = ? and `number` = ? and `create_timestamp` = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.execute(sql, (last_interact_timestamp, prompt, subject_type,
|
self.__execute__(sql, (last_interact_timestamp, prompt, token_counts, subject_type,
|
||||||
subject_number, create_timestamp))
|
subject_number, create_timestamp))
|
||||||
|
|
||||||
# 显式关闭一个session
|
# 显式关闭一个session
|
||||||
def explicit_close_session(self, session_name: str, create_timestamp: int):
|
def explicit_close_session(self, session_name: str, create_timestamp: int):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
update `sessions` set `status` = 'explicitly_closed' where `name` = '{}' and `create_timestamp` = {}
|
update `sessions` set `status` = 'explicitly_closed' where `name` = '{}' and `create_timestamp` = {}
|
||||||
""".format(session_name, create_timestamp))
|
""".format(session_name, create_timestamp))
|
||||||
|
|
||||||
def set_session_ongoing(self, session_name: str, create_timestamp: int):
|
def set_session_ongoing(self, session_name: str, create_timestamp: int):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
update `sessions` set `status` = 'on_going' where `name` = '{}' and `create_timestamp` = {}
|
update `sessions` set `status` = 'on_going' where `name` = '{}' and `create_timestamp` = {}
|
||||||
""".format(session_name, create_timestamp))
|
""".format(session_name, create_timestamp))
|
||||||
|
|
||||||
# 设置session为过期
|
# 设置session为过期
|
||||||
def set_session_expired(self, session_name: str, create_timestamp: int):
|
def set_session_expired(self, session_name: str, create_timestamp: int):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
update `sessions` set `status` = 'expired' where `name` = '{}' and `create_timestamp` = {}
|
update `sessions` set `status` = 'expired' where `name` = '{}' and `create_timestamp` = {}
|
||||||
""".format(session_name, create_timestamp))
|
""".format(session_name, create_timestamp))
|
||||||
|
|
||||||
# 从数据库加载还没过期的session数据
|
# 从数据库加载还没过期的session数据
|
||||||
def load_valid_sessions(self) -> dict:
|
def load_valid_sessions(self) -> dict:
|
||||||
# 从数据库中加载所有还没过期的session
|
# 从数据库中加载所有还没过期的session
|
||||||
config = pkg.utils.context.get_config()
|
config = context.get_config_manager().data
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
|
||||||
from `sessions` where `last_interact_timestamp` > {}
|
from `sessions` where `last_interact_timestamp` > {}
|
||||||
""".format(int(time.time()) - config.session_expire_time))
|
""".format(int(time.time()) - config['session_expire_time']))
|
||||||
results = self.cursor.fetchall()
|
results = self.cursor.fetchall()
|
||||||
sessions = {}
|
sessions = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
@@ -140,6 +159,8 @@ class DatabaseManager:
|
|||||||
last_interact_timestamp = result[4]
|
last_interact_timestamp = result[4]
|
||||||
prompt = result[5]
|
prompt = result[5]
|
||||||
status = result[6]
|
status = result[6]
|
||||||
|
default_prompt = result[7]
|
||||||
|
token_counts = result[8]
|
||||||
|
|
||||||
# 当且仅当最后一个该对象的会话是on_going状态时,才会被加载
|
# 当且仅当最后一个该对象的会话是on_going状态时,才会被加载
|
||||||
if status == 'on_going':
|
if status == 'on_going':
|
||||||
@@ -148,7 +169,9 @@ class DatabaseManager:
|
|||||||
'subject_number': subject_number,
|
'subject_number': subject_number,
|
||||||
'create_timestamp': create_timestamp,
|
'create_timestamp': create_timestamp,
|
||||||
'last_interact_timestamp': last_interact_timestamp,
|
'last_interact_timestamp': last_interact_timestamp,
|
||||||
'prompt': prompt
|
'prompt': prompt,
|
||||||
|
'default_prompt': default_prompt,
|
||||||
|
'token_counts': token_counts
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
if session_name in sessions:
|
if session_name in sessions:
|
||||||
@@ -159,8 +182,8 @@ class DatabaseManager:
|
|||||||
# 获取此session_name前一个session的数据
|
# 获取此session_name前一个session的数据
|
||||||
def last_session(self, session_name: str, cursor_timestamp: int):
|
def last_session(self, session_name: str, cursor_timestamp: int):
|
||||||
|
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
|
||||||
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
|
from `sessions` where `name` = '{}' and `last_interact_timestamp` < {} order by `last_interact_timestamp` desc
|
||||||
limit 1
|
limit 1
|
||||||
""".format(session_name, cursor_timestamp))
|
""".format(session_name, cursor_timestamp))
|
||||||
@@ -176,20 +199,24 @@ class DatabaseManager:
|
|||||||
last_interact_timestamp = result[4]
|
last_interact_timestamp = result[4]
|
||||||
prompt = result[5]
|
prompt = result[5]
|
||||||
status = result[6]
|
status = result[6]
|
||||||
|
default_prompt = result[7]
|
||||||
|
token_counts = result[8]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
'subject_number': subject_number,
|
'subject_number': subject_number,
|
||||||
'create_timestamp': create_timestamp,
|
'create_timestamp': create_timestamp,
|
||||||
'last_interact_timestamp': last_interact_timestamp,
|
'last_interact_timestamp': last_interact_timestamp,
|
||||||
'prompt': prompt
|
'prompt': prompt,
|
||||||
|
'default_prompt': default_prompt,
|
||||||
|
'token_counts': token_counts
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取此session_name后一个session的数据
|
# 获取此session_name后一个session的数据
|
||||||
def next_session(self, session_name: str, cursor_timestamp: int):
|
def next_session(self, session_name: str, cursor_timestamp: int):
|
||||||
|
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
|
||||||
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
|
from `sessions` where `name` = '{}' and `last_interact_timestamp` > {} order by `last_interact_timestamp` asc
|
||||||
limit 1
|
limit 1
|
||||||
""".format(session_name, cursor_timestamp))
|
""".format(session_name, cursor_timestamp))
|
||||||
@@ -205,19 +232,23 @@ class DatabaseManager:
|
|||||||
last_interact_timestamp = result[4]
|
last_interact_timestamp = result[4]
|
||||||
prompt = result[5]
|
prompt = result[5]
|
||||||
status = result[6]
|
status = result[6]
|
||||||
|
default_prompt = result[7]
|
||||||
|
token_counts = result[8]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
'subject_number': subject_number,
|
'subject_number': subject_number,
|
||||||
'create_timestamp': create_timestamp,
|
'create_timestamp': create_timestamp,
|
||||||
'last_interact_timestamp': last_interact_timestamp,
|
'last_interact_timestamp': last_interact_timestamp,
|
||||||
'prompt': prompt
|
'prompt': prompt,
|
||||||
|
'default_prompt': default_prompt,
|
||||||
|
'token_counts': token_counts
|
||||||
}
|
}
|
||||||
|
|
||||||
# 列出与某个对象的所有对话session
|
# 列出与某个对象的所有对话session
|
||||||
def list_history(self, session_name: str, capacity: int, page: int, replace: str = ""):
|
def list_history(self, session_name: str, capacity: int, page: int):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`
|
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
|
||||||
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
|
from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit {} offset {}
|
||||||
""".format(session_name, capacity, capacity * page))
|
""".format(session_name, capacity, capacity * page))
|
||||||
results = self.cursor.fetchall()
|
results = self.cursor.fetchall()
|
||||||
@@ -230,17 +261,42 @@ class DatabaseManager:
|
|||||||
last_interact_timestamp = result[4]
|
last_interact_timestamp = result[4]
|
||||||
prompt = result[5]
|
prompt = result[5]
|
||||||
status = result[6]
|
status = result[6]
|
||||||
|
default_prompt = result[7]
|
||||||
|
token_counts = result[8]
|
||||||
|
|
||||||
sessions.append({
|
sessions.append({
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
'subject_number': subject_number,
|
'subject_number': subject_number,
|
||||||
'create_timestamp': create_timestamp,
|
'create_timestamp': create_timestamp,
|
||||||
'last_interact_timestamp': last_interact_timestamp,
|
'last_interact_timestamp': last_interact_timestamp,
|
||||||
'prompt': prompt if replace == "" else prompt.replace(replace, "")
|
'prompt': prompt,
|
||||||
|
'default_prompt': default_prompt,
|
||||||
|
'token_counts': token_counts
|
||||||
})
|
})
|
||||||
|
|
||||||
return sessions
|
return sessions
|
||||||
|
|
||||||
|
def delete_history(self, session_name: str, index: int) -> bool:
|
||||||
|
# 删除倒序第index个session
|
||||||
|
# 查找其id再删除
|
||||||
|
self.__execute__("""
|
||||||
|
delete from `sessions` where `id` in (select `id` from `sessions` where `name` = '{}' order by `last_interact_timestamp` desc limit 1 offset {})
|
||||||
|
""".format(session_name, index))
|
||||||
|
|
||||||
|
return self.cursor.rowcount == 1
|
||||||
|
|
||||||
|
def delete_all_history(self, session_name: str) -> bool:
|
||||||
|
self.__execute__("""
|
||||||
|
delete from `sessions` where `name` = '{}'
|
||||||
|
""".format(session_name))
|
||||||
|
return self.cursor.rowcount > 0
|
||||||
|
|
||||||
|
def delete_all_session_history(self) -> bool:
|
||||||
|
self.__execute__("""
|
||||||
|
delete from `sessions`
|
||||||
|
""")
|
||||||
|
return self.cursor.rowcount > 0
|
||||||
|
|
||||||
# 将apikey的使用量存进数据库
|
# 将apikey的使用量存进数据库
|
||||||
def dump_api_key_usage(self, api_keys: dict, usage: dict):
|
def dump_api_key_usage(self, api_keys: dict, usage: dict):
|
||||||
logging.debug('dumping api key usage...')
|
logging.debug('dumping api key usage...')
|
||||||
@@ -255,22 +311,22 @@ class DatabaseManager:
|
|||||||
usage_count = usage[key_md5]
|
usage_count = usage[key_md5]
|
||||||
# 将使用量存进数据库
|
# 将使用量存进数据库
|
||||||
# 先检查是否已存在
|
# 先检查是否已存在
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select count(*) from `api_key_usage` where `key_md5` = '{}'""".format(key_md5))
|
select count(*) from `api_key_usage` where `key_md5` = '{}'""".format(key_md5))
|
||||||
result = self.cursor.fetchone()
|
result = self.cursor.fetchone()
|
||||||
if result[0] == 0:
|
if result[0] == 0:
|
||||||
# 不存在则插入
|
# 不存在则插入
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
insert into `api_key_usage` (`key_md5`, `usage`,`timestamp`) values ('{}', {}, {})
|
insert into `api_key_usage` (`key_md5`, `usage`,`timestamp`) values ('{}', {}, {})
|
||||||
""".format(key_md5, usage_count, int(time.time())))
|
""".format(key_md5, usage_count, int(time.time())))
|
||||||
else:
|
else:
|
||||||
# 存在则更新,timestamp设置为当前
|
# 存在则更新,timestamp设置为当前
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
update `api_key_usage` set `usage` = {}, `timestamp` = {} where `key_md5` = '{}'
|
update `api_key_usage` set `usage` = {}, `timestamp` = {} where `key_md5` = '{}'
|
||||||
""".format(usage_count, int(time.time()), key_md5))
|
""".format(usage_count, int(time.time()), key_md5))
|
||||||
|
|
||||||
def load_api_key_usage(self):
|
def load_api_key_usage(self):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `key_md5`, `usage` from `api_key_usage`
|
select `key_md5`, `usage` from `api_key_usage`
|
||||||
""")
|
""")
|
||||||
results = self.cursor.fetchall()
|
results = self.cursor.fetchall()
|
||||||
@@ -281,63 +337,25 @@ class DatabaseManager:
|
|||||||
usage[key_md5] = usage_count
|
usage[key_md5] = usage_count
|
||||||
return usage
|
return usage
|
||||||
|
|
||||||
def dump_api_key_fee(self, api_keys: dict, fee: dict):
|
|
||||||
logging.debug("dumping api key fee...")
|
|
||||||
logging.debug(api_keys)
|
|
||||||
logging.debug(fee)
|
|
||||||
for api_key in api_keys:
|
|
||||||
# 计算key的md5值
|
|
||||||
key_md5 = hashlib.md5(api_keys[api_key].encode('utf-8')).hexdigest()
|
|
||||||
# 获取使用量
|
|
||||||
fee_count = 0
|
|
||||||
if key_md5 in fee:
|
|
||||||
fee_count = fee[key_md5]
|
|
||||||
# 将使用量存进数据库
|
|
||||||
# 先检查是否已存在
|
|
||||||
self.execute("""
|
|
||||||
select count(*) from `account_fee` where `key_md5` = '{}'""".format(key_md5))
|
|
||||||
result = self.cursor.fetchone()
|
|
||||||
if result[0] == 0:
|
|
||||||
# 不存在则插入
|
|
||||||
self.execute("""
|
|
||||||
insert into `account_fee` (`key_md5`, `fee`,`timestamp`) values ('{}', {}, {})
|
|
||||||
""".format(key_md5, fee_count, int(time.time())))
|
|
||||||
else:
|
|
||||||
# 存在则更新,timestamp设置为当前
|
|
||||||
self.execute("""
|
|
||||||
update `account_fee` set `fee` = {}, `timestamp` = {} where `key_md5` = '{}'
|
|
||||||
""".format(fee_count, int(time.time()), key_md5))
|
|
||||||
|
|
||||||
def load_api_key_fee(self):
|
|
||||||
self.execute("""
|
|
||||||
select `key_md5`, `fee` from `account_fee`
|
|
||||||
""")
|
|
||||||
results = self.cursor.fetchall()
|
|
||||||
fee = {}
|
|
||||||
for result in results:
|
|
||||||
key_md5 = result[0]
|
|
||||||
fee_count = result[1]
|
|
||||||
fee[key_md5] = fee_count
|
|
||||||
return fee
|
|
||||||
|
|
||||||
def dump_usage_json(self, usage: dict):
|
def dump_usage_json(self, usage: dict):
|
||||||
|
|
||||||
json_str = json.dumps(usage)
|
json_str = json.dumps(usage)
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select count(*) from `account_usage`""")
|
select count(*) from `account_usage`""")
|
||||||
result = self.cursor.fetchone()
|
result = self.cursor.fetchone()
|
||||||
if result[0] == 0:
|
if result[0] == 0:
|
||||||
# 不存在则插入
|
# 不存在则插入
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
insert into `account_usage` (`json`) values ('{}')
|
insert into `account_usage` (`json`) values ('{}')
|
||||||
""".format(json_str))
|
""".format(json_str))
|
||||||
else:
|
else:
|
||||||
# 存在则更新
|
# 存在则更新
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
update `account_usage` set `json` = '{}' where `id` = 1
|
update `account_usage` set `json` = '{}' where `id` = 1
|
||||||
""".format(json_str))
|
""".format(json_str))
|
||||||
|
|
||||||
def load_usage_json(self):
|
def load_usage_json(self):
|
||||||
self.execute("""
|
self.__execute__("""
|
||||||
select `json` from `account_usage` order by id desc limit 1
|
select `json` from `account_usage` order by id desc limit 1
|
||||||
""")
|
""")
|
||||||
result = self.cursor.fetchone()
|
result = self.cursor.fetchone()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""OpenAI 接口处理及会话管理相关"""
|
||||||
|
|||||||
0
pkg/openai/api/__init__.py
Normal file
0
pkg/openai/api/__init__.py
Normal file
232
pkg/openai/api/chat_completion.py
Normal file
232
pkg/openai/api/chat_completion.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import openai
|
||||||
|
from openai.types.chat import chat_completion_message
|
||||||
|
|
||||||
|
from .model import RequestBase
|
||||||
|
from .. import funcmgr
|
||||||
|
from ...plugin import host
|
||||||
|
from ...utils import context
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionRequest(RequestBase):
|
||||||
|
"""调用ChatCompletion接口的请求类。
|
||||||
|
|
||||||
|
此类保证每一次返回的角色为assistant的信息的finish_reason一定为stop。
|
||||||
|
若有函数调用响应,本类的返回瀑布是:函数调用请求->函数调用结果->...->assistant的信息->stop。
|
||||||
|
"""
|
||||||
|
|
||||||
|
model: str
|
||||||
|
messages: list[dict[str, str]]
|
||||||
|
kwargs: dict
|
||||||
|
|
||||||
|
stopped: bool = False
|
||||||
|
|
||||||
|
pending_func_call: chat_completion_message.FunctionCall = None
|
||||||
|
|
||||||
|
pending_msg: str
|
||||||
|
|
||||||
|
def flush_pending_msg(self):
|
||||||
|
self.append_message(
|
||||||
|
role="assistant",
|
||||||
|
content=self.pending_msg
|
||||||
|
)
|
||||||
|
self.pending_msg = ""
|
||||||
|
|
||||||
|
def append_message(self, role: str, content: str, name: str=None, function_call: dict=None):
|
||||||
|
msg = {
|
||||||
|
"role": role,
|
||||||
|
"content": content
|
||||||
|
}
|
||||||
|
|
||||||
|
if name is not None:
|
||||||
|
msg['name'] = name
|
||||||
|
|
||||||
|
if function_call is not None:
|
||||||
|
msg['function_call'] = function_call
|
||||||
|
|
||||||
|
self.messages.append(msg)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: openai.Client,
|
||||||
|
model: str,
|
||||||
|
messages: list[dict[str, str]],
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
self.client = client
|
||||||
|
self.model = model
|
||||||
|
self.messages = messages.copy()
|
||||||
|
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
self.req_func = self.client.chat.completions.create
|
||||||
|
|
||||||
|
self.pending_func_call = None
|
||||||
|
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
|
self.pending_msg = ""
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> dict:
|
||||||
|
if self.stopped:
|
||||||
|
raise StopIteration()
|
||||||
|
|
||||||
|
if self.pending_func_call is None: # 没有待处理的函数调用请求
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": self.messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
funcs = funcmgr.get_func_schema_list()
|
||||||
|
|
||||||
|
if len(funcs) > 0:
|
||||||
|
args['functions'] = funcs
|
||||||
|
|
||||||
|
# 拼接kwargs
|
||||||
|
args = {**args, **self.kwargs}
|
||||||
|
|
||||||
|
from openai.types.chat import chat_completion
|
||||||
|
|
||||||
|
resp: chat_completion.ChatCompletion = self._req(**args)
|
||||||
|
|
||||||
|
choice0 = resp.choices[0]
|
||||||
|
|
||||||
|
# 如果不是函数调用,且finish_reason为stop,则停止迭代
|
||||||
|
if choice0.finish_reason == 'stop': # and choice0["finish_reason"] == "stop"
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
if hasattr(choice0.message, 'function_call') and choice0.message.function_call is not None:
|
||||||
|
self.pending_func_call = choice0.message.function_call
|
||||||
|
|
||||||
|
self.append_message(
|
||||||
|
role="assistant",
|
||||||
|
content=choice0.message.content,
|
||||||
|
function_call=choice0.message.function_call
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": resp.id,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": choice0.index,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"type": "function_call",
|
||||||
|
"content": choice0.message.content,
|
||||||
|
"function_call": {
|
||||||
|
"name": choice0.message.function_call.name,
|
||||||
|
"arguments": choice0.message.function_call.arguments
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finish_reason": "function_call"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": resp.usage.prompt_tokens,
|
||||||
|
"completion_tokens": resp.usage.completion_tokens,
|
||||||
|
"total_tokens": resp.usage.total_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
|
||||||
|
# self.pending_msg += choice0['message']['content']
|
||||||
|
# 普通回复一定处于最后方,故不用再追加进内部messages
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": resp.id,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": choice0.index,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"type": "text",
|
||||||
|
"content": choice0.message.content
|
||||||
|
},
|
||||||
|
"finish_reason": choice0.finish_reason
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": resp.usage.prompt_tokens,
|
||||||
|
"completion_tokens": resp.usage.completion_tokens,
|
||||||
|
"total_tokens": resp.usage.total_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else: # 处理函数调用请求
|
||||||
|
|
||||||
|
cp_pending_func_call = self.pending_func_call.copy()
|
||||||
|
|
||||||
|
self.pending_func_call = None
|
||||||
|
|
||||||
|
func_name = cp_pending_func_call.name
|
||||||
|
arguments = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
try:
|
||||||
|
arguments = json.loads(cp_pending_func_call.arguments)
|
||||||
|
# 若不是json格式的异常处理
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
# 获取函数的参数列表
|
||||||
|
func_schema = funcmgr.get_func_schema(func_name)
|
||||||
|
|
||||||
|
arguments = {
|
||||||
|
func_schema['parameters']['required'][0]: cp_pending_func_call.arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info("执行函数调用: name={}, arguments={}".format(func_name, arguments))
|
||||||
|
|
||||||
|
# 执行函数调用
|
||||||
|
ret = ""
|
||||||
|
try:
|
||||||
|
ret = funcmgr.execute_function(func_name, arguments)
|
||||||
|
|
||||||
|
logging.info("函数执行完成。")
|
||||||
|
except Exception as e:
|
||||||
|
ret = "error: execute function failed: {}".format(str(e))
|
||||||
|
logging.error("函数执行失败: {}".format(str(e)))
|
||||||
|
|
||||||
|
# 上报数据
|
||||||
|
plugin_info = host.get_plugin_info_for_audit(func_name.split('-')[0])
|
||||||
|
audit_func_name = func_name.split('-')[1]
|
||||||
|
audit_func_desc = funcmgr.get_func_schema(func_name)['description']
|
||||||
|
context.get_center_v2_api().usage.post_function_record(
|
||||||
|
plugin=plugin_info,
|
||||||
|
function_name=audit_func_name,
|
||||||
|
function_description=audit_func_desc,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.append_message(
|
||||||
|
role="function",
|
||||||
|
content=json.dumps(ret, ensure_ascii=False),
|
||||||
|
name=func_name
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": -1,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": -1,
|
||||||
|
"message": {
|
||||||
|
"role": "function",
|
||||||
|
"type": "function_return",
|
||||||
|
"function_name": func_name,
|
||||||
|
"content": json.dumps(ret, ensure_ascii=False)
|
||||||
|
},
|
||||||
|
"finish_reason": "function_return"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except funcmgr.ContentFunctionNotFoundError:
|
||||||
|
raise Exception("没有找到函数: {}".format(func_name))
|
||||||
100
pkg/openai/api/completion.py
Normal file
100
pkg/openai/api/completion.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import openai
|
||||||
|
from openai.types import completion, completion_choice
|
||||||
|
|
||||||
|
from . import model
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionRequest(model.RequestBase):
|
||||||
|
"""调用Completion接口的请求类。
|
||||||
|
|
||||||
|
调用方可以一直next completion直到finish_reason为stop。
|
||||||
|
"""
|
||||||
|
|
||||||
|
model: str
|
||||||
|
prompt: str
|
||||||
|
kwargs: dict
|
||||||
|
|
||||||
|
stopped: bool = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client: openai.Client,
|
||||||
|
model: str,
|
||||||
|
messages: list[dict[str, str]],
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
self.client = client
|
||||||
|
self.model = model
|
||||||
|
self.prompt = ""
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
self.prompt += message["role"] + ": " + message["content"] + "\n"
|
||||||
|
|
||||||
|
self.prompt += "assistant: "
|
||||||
|
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
self.req_func = self.client.completions.create
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> dict:
|
||||||
|
"""调用Completion接口,返回生成的文本
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "id",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"type": "text",
|
||||||
|
"content": "message"
|
||||||
|
},
|
||||||
|
"finish_reason": "reason"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 10,
|
||||||
|
"completion_tokens": 20,
|
||||||
|
"total_tokens": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.stopped:
|
||||||
|
raise StopIteration()
|
||||||
|
|
||||||
|
resp: completion.Completion = self._req(
|
||||||
|
model=self.model,
|
||||||
|
prompt=self.prompt,
|
||||||
|
**self.kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if resp.choices[0].finish_reason == "stop":
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
choice0: completion_choice.CompletionChoice = resp.choices[0]
|
||||||
|
|
||||||
|
self.prompt += choice0.text
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": resp.id,
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": choice0.index,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"type": "text",
|
||||||
|
"content": choice0.text
|
||||||
|
},
|
||||||
|
"finish_reason": choice0.finish_reason
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": resp.usage.prompt_tokens,
|
||||||
|
"completion_tokens": resp.usage.completion_tokens,
|
||||||
|
"total_tokens": resp.usage.total_tokens
|
||||||
|
}
|
||||||
|
}
|
||||||
40
pkg/openai/api/model.py
Normal file
40
pkg/openai/api/model.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 定义不同接口请求的模型
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import openai
|
||||||
|
|
||||||
|
from ...utils import context
|
||||||
|
|
||||||
|
|
||||||
|
class RequestBase:
|
||||||
|
|
||||||
|
client: openai.Client
|
||||||
|
|
||||||
|
req_func: callable
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _next_key(self):
|
||||||
|
switched, name = context.get_openai_manager().key_mgr.auto_switch()
|
||||||
|
logging.debug("切换api-key: switched={}, name={}".format(switched, name))
|
||||||
|
self.client.api_key = context.get_openai_manager().key_mgr.get_using_key()
|
||||||
|
|
||||||
|
def _req(self, **kwargs):
|
||||||
|
"""处理代理问题"""
|
||||||
|
logging.debug("请求接口参数: %s", str(kwargs))
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
ret = self.req_func(**kwargs)
|
||||||
|
logging.debug("接口请求返回:%s", str(ret))
|
||||||
|
|
||||||
|
if config['switch_strategy'] == 'active':
|
||||||
|
self._next_key()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
raise self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
raise NotImplementedError
|
||||||
134
pkg/openai/dprompt.py
Normal file
134
pkg/openai/dprompt.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# 多情景预设值管理
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
# __current__ = "default"
|
||||||
|
# """当前默认使用的情景预设的名称
|
||||||
|
|
||||||
|
# 由管理员使用`!default <名称>`命令切换
|
||||||
|
# """
|
||||||
|
|
||||||
|
# __prompts_from_files__ = {}
|
||||||
|
# """从文件中读取的情景预设值"""
|
||||||
|
|
||||||
|
# __scenario_from_files__ = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioMode:
|
||||||
|
"""情景预设模式抽象类"""
|
||||||
|
|
||||||
|
using_prompt_name = "default"
|
||||||
|
"""新session创建时使用的prompt名称"""
|
||||||
|
|
||||||
|
prompts: dict[str, list] = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logging.debug("prompts: {}".format(self.prompts))
|
||||||
|
|
||||||
|
def list(self) -> dict[str, list]:
|
||||||
|
"""获取所有情景预设的名称及内容"""
|
||||||
|
return self.prompts
|
||||||
|
|
||||||
|
def get_prompt(self, name: str) -> tuple[list, str]:
|
||||||
|
"""获取指定情景预设的名称及内容"""
|
||||||
|
for key in self.prompts:
|
||||||
|
if key.startswith(name):
|
||||||
|
return self.prompts[key], key
|
||||||
|
raise Exception("没有找到情景预设: {}".format(name))
|
||||||
|
|
||||||
|
def set_using_name(self, name: str) -> str:
|
||||||
|
"""设置默认情景预设"""
|
||||||
|
for key in self.prompts:
|
||||||
|
if key.startswith(name):
|
||||||
|
self.using_prompt_name = key
|
||||||
|
return key
|
||||||
|
raise Exception("没有找到情景预设: {}".format(name))
|
||||||
|
|
||||||
|
def get_full_name(self, name: str) -> str:
|
||||||
|
"""获取完整的情景预设名称"""
|
||||||
|
for key in self.prompts:
|
||||||
|
if key.startswith(name):
|
||||||
|
return key
|
||||||
|
raise Exception("没有找到情景预设: {}".format(name))
|
||||||
|
|
||||||
|
def get_using_name(self) -> str:
|
||||||
|
"""获取默认情景预设"""
|
||||||
|
return self.using_prompt_name
|
||||||
|
|
||||||
|
|
||||||
|
class NormalScenarioMode(ScenarioMode):
|
||||||
|
"""普通情景预设模式"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
# 加载config中的default_prompt值
|
||||||
|
if type(config['default_prompt']) == str:
|
||||||
|
self.using_prompt_name = "default"
|
||||||
|
self.prompts = {"default": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": config['default_prompt']
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
|
||||||
|
elif type(config['default_prompt']) == dict:
|
||||||
|
for key in config['default_prompt']:
|
||||||
|
self.prompts[key] = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": config['default_prompt'][key]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 从prompts/目录下的文件中载入
|
||||||
|
# 遍历文件
|
||||||
|
for file in os.listdir("prompts"):
|
||||||
|
with open(os.path.join("prompts", file), encoding="utf-8") as f:
|
||||||
|
self.prompts[file] = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": f.read()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FullScenarioMode(ScenarioMode):
|
||||||
|
"""完整情景预设模式"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""从json读取所有"""
|
||||||
|
# 遍历scenario/目录下的所有文件,以文件名为键,文件内容中的prompt为值
|
||||||
|
for file in os.listdir("scenario"):
|
||||||
|
if file == "default-template.json":
|
||||||
|
continue
|
||||||
|
with open(os.path.join("scenario", file), encoding="utf-8") as f:
|
||||||
|
self.prompts[file] = json.load(f)["prompt"]
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
scenario_mode_mapping = {}
|
||||||
|
"""情景预设模式名称与对象的映射"""
|
||||||
|
|
||||||
|
|
||||||
|
def register_all():
|
||||||
|
"""注册所有情景预设模式,不使用装饰器,因为装饰器的方式不支持热重载"""
|
||||||
|
global scenario_mode_mapping
|
||||||
|
scenario_mode_mapping = {
|
||||||
|
"normal": NormalScenarioMode(),
|
||||||
|
"full_scenario": FullScenarioMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mode_inst() -> ScenarioMode:
|
||||||
|
"""获取指定名称的情景预设模式对象"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if config['preset_mode'] == "default":
|
||||||
|
config['preset_mode'] = "normal"
|
||||||
|
|
||||||
|
return scenario_mode_mapping[config['preset_mode']]
|
||||||
46
pkg/openai/funcmgr.py
Normal file
46
pkg/openai/funcmgr.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 封装了function calling的一些支持函数
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..plugin import host
|
||||||
|
|
||||||
|
|
||||||
|
class ContentFunctionNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_func_schema_list() -> list:
|
||||||
|
"""从plugin包中的函数结构中获取并处理成受GPT支持的格式"""
|
||||||
|
if not host.__enable_content_functions__:
|
||||||
|
return []
|
||||||
|
|
||||||
|
schemas = []
|
||||||
|
|
||||||
|
for func in host.__callable_functions__:
|
||||||
|
if func['enabled']:
|
||||||
|
fun_cp = func.copy()
|
||||||
|
|
||||||
|
del fun_cp['enabled']
|
||||||
|
|
||||||
|
schemas.append(fun_cp)
|
||||||
|
|
||||||
|
return schemas
|
||||||
|
|
||||||
|
def get_func(name: str) -> callable:
|
||||||
|
if name not in host.__function_inst_map__:
|
||||||
|
raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
|
||||||
|
|
||||||
|
return host.__function_inst_map__[name]
|
||||||
|
|
||||||
|
def get_func_schema(name: str) -> dict:
|
||||||
|
for func in host.__callable_functions__:
|
||||||
|
if func['name'] == name:
|
||||||
|
return func
|
||||||
|
raise ContentFunctionNotFoundError("没有找到内容函数: {}".format(name))
|
||||||
|
|
||||||
|
def execute_function(name: str, kwargs: dict) -> any:
|
||||||
|
"""执行函数调用"""
|
||||||
|
|
||||||
|
logging.debug("executing function: name='{}', kwargs={}".format(name, kwargs))
|
||||||
|
|
||||||
|
func = get_func(name)
|
||||||
|
return func(**kwargs)
|
||||||
@@ -2,27 +2,28 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pkg.database.manager
|
from ..plugin import host as plugin_host
|
||||||
import pkg.qqbot.manager
|
from ..plugin import models as plugin_models
|
||||||
import pkg.utils.context
|
|
||||||
|
|
||||||
|
|
||||||
class KeysManager:
|
class KeysManager:
|
||||||
api_key = {}
|
api_key = {}
|
||||||
|
"""所有api-key"""
|
||||||
# api-key的使用量
|
|
||||||
# 其中键为api-key的md5值,值为使用量
|
|
||||||
fee = {}
|
|
||||||
|
|
||||||
api_key_fee_threshold = 18.0
|
|
||||||
|
|
||||||
using_key = ""
|
using_key = ""
|
||||||
|
"""当前使用的api-key"""
|
||||||
|
|
||||||
alerted = []
|
alerted = []
|
||||||
|
"""已提示过超额的key
|
||||||
|
|
||||||
|
记录在此以避免重复提示
|
||||||
|
"""
|
||||||
|
|
||||||
# 在此list中的都是经超额报错标记过的api-key
|
|
||||||
# 记录的是key值,仅在运行时有效
|
|
||||||
exceeded = []
|
exceeded = []
|
||||||
|
"""已超额的key
|
||||||
|
|
||||||
|
供自动切换功能识别
|
||||||
|
"""
|
||||||
|
|
||||||
def get_using_key(self):
|
def get_using_key(self):
|
||||||
return self.using_key
|
return self.using_key
|
||||||
@@ -31,61 +32,68 @@ class KeysManager:
|
|||||||
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
def __init__(self, api_key):
|
def __init__(self, api_key):
|
||||||
# if hasattr(config, 'api_key_usage_threshold'):
|
|
||||||
# self.api_key_usage_threshold = config.api_key_usage_threshold
|
assert type(api_key) == dict
|
||||||
|
self.api_key = api_key
|
||||||
config = pkg.utils.context.get_config()
|
|
||||||
if hasattr(config, 'api_key_fee_threshold'):
|
|
||||||
self.api_key_fee_threshold = config.api_key_fee_threshold
|
|
||||||
self.load_fee()
|
|
||||||
|
|
||||||
if type(api_key) is dict:
|
|
||||||
self.api_key = api_key
|
|
||||||
elif type(api_key) is str:
|
|
||||||
self.api_key = {
|
|
||||||
"default": api_key
|
|
||||||
}
|
|
||||||
elif type(api_key) is list:
|
|
||||||
for i in range(len(api_key)):
|
|
||||||
self.api_key[str(i)] = api_key[i]
|
|
||||||
|
|
||||||
self.auto_switch()
|
|
||||||
# 从usage中删除未加载的api-key的记录
|
# 从usage中删除未加载的api-key的记录
|
||||||
# 不删了,也许会运行时添加曾经有记录的api-key
|
# 不删了,也许会运行时添加曾经有记录的api-key
|
||||||
|
|
||||||
if 'exceeded_keys' in pkg.utils.context.context and pkg.utils.context.context['exceeded_keys'] is not None:
|
self.auto_switch()
|
||||||
self.exceeded = pkg.utils.context.context['exceeded_keys']
|
|
||||||
|
def auto_switch(self) -> tuple[bool, str]:
|
||||||
|
"""尝试切换api-key
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否切换成功, 切换后的api-key的别名
|
||||||
|
"""
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
|
||||||
# 根据tested自动切换到可用的api-key
|
|
||||||
# 返回是否切换成功, 切换后的api-key的别名
|
|
||||||
def auto_switch(self) -> (bool, str):
|
|
||||||
self.dump_fee()
|
|
||||||
for key_name in self.api_key:
|
for key_name in self.api_key:
|
||||||
|
if self.api_key[key_name] == self.using_key:
|
||||||
|
break
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
# 从当前key开始向后轮询
|
||||||
|
start_index = index
|
||||||
|
index += 1
|
||||||
|
if index >= len(self.api_key):
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while index != start_index:
|
||||||
|
|
||||||
|
key_name = list(self.api_key.keys())[index]
|
||||||
|
|
||||||
if self.api_key[key_name] not in self.exceeded:
|
if self.api_key[key_name] not in self.exceeded:
|
||||||
self.using_key = self.api_key[key_name]
|
self.using_key = self.api_key[key_name]
|
||||||
logging.info("使用api-key:" + key_name)
|
|
||||||
|
logging.debug("使用api-key:" + key_name)
|
||||||
|
|
||||||
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
"key_name": key_name,
|
||||||
|
"key_list": self.api_key.keys()
|
||||||
|
}
|
||||||
|
_ = plugin_host.emit(plugin_models.KeySwitched, **args)
|
||||||
|
|
||||||
return True, key_name
|
return True, key_name
|
||||||
# if self.get_fee(self.api_key[key_name]) < self.api_key_fee_threshold:
|
|
||||||
# self.using_key = self.api_key[key_name]
|
|
||||||
# logging.info("使用api-key:" + key_name)
|
|
||||||
# return True, key_name
|
|
||||||
|
|
||||||
self.using_key = list(self.api_key.values())[0]
|
index += 1
|
||||||
logging.info("使用api-key:" + list(self.api_key.keys())[0])
|
if index >= len(self.api_key):
|
||||||
|
index = 0
|
||||||
|
|
||||||
return False, ""
|
self.using_key = list(self.api_key.values())[start_index]
|
||||||
|
logging.debug("使用api-key:" + list(self.api_key.keys())[start_index])
|
||||||
|
|
||||||
|
return False, list(self.api_key.keys())[start_index]
|
||||||
|
|
||||||
def add(self, key_name, key):
|
def add(self, key_name, key):
|
||||||
self.api_key[key_name] = key
|
self.api_key[key_name] = key
|
||||||
|
|
||||||
# 设置当前使用的api-key使用量超限
|
|
||||||
# 这是在尝试调用api时发生超限异常时调用的
|
|
||||||
def set_current_exceeded(self):
|
def set_current_exceeded(self):
|
||||||
# md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
"""设置当前使用的api-key使用量超限"""
|
||||||
# self.usage[md5] = self.api_key_usage_threshold
|
|
||||||
# self.fee[md5] = self.api_key_fee_threshold
|
|
||||||
self.exceeded.append(self.using_key)
|
self.exceeded.append(self.using_key)
|
||||||
self.dump_fee()
|
|
||||||
|
|
||||||
def get_key_name(self, api_key):
|
def get_key_name(self, api_key):
|
||||||
"""根据api-key获取其别名"""
|
"""根据api-key获取其别名"""
|
||||||
@@ -93,45 +101,3 @@ class KeysManager:
|
|||||||
if self.api_key[key_name] == api_key:
|
if self.api_key[key_name] == api_key:
|
||||||
return key_name
|
return key_name
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_fee(self, api_key):
|
|
||||||
md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest()
|
|
||||||
if md5 not in self.fee:
|
|
||||||
self.fee[md5] = 0
|
|
||||||
return self.fee[md5]
|
|
||||||
|
|
||||||
def report_fee(self, fee: float) -> bool:
|
|
||||||
logging.debug("report fee:" + str(fee))
|
|
||||||
|
|
||||||
md5 = hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
|
||||||
if md5 not in self.fee:
|
|
||||||
self.fee[md5] = 0
|
|
||||||
|
|
||||||
self.fee[md5] += fee
|
|
||||||
|
|
||||||
config = pkg.utils.context.get_config()
|
|
||||||
if self.fee[md5] >= self.api_key_fee_threshold and \
|
|
||||||
hasattr(config, 'auto_switch_api_key') and config.auto_switch_api_key:
|
|
||||||
switch_result, key_name = self.auto_switch()
|
|
||||||
|
|
||||||
# 检查是否切换到新的
|
|
||||||
if switch_result:
|
|
||||||
if key_name not in self.alerted:
|
|
||||||
# 通知管理员
|
|
||||||
pkg.utils.context.get_qqbot_manager().notify_admin("api-key已切换到:" + key_name)
|
|
||||||
self.alerted.append(key_name)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
if key_name not in self.alerted:
|
|
||||||
# 通知管理员
|
|
||||||
pkg.utils.context.get_qqbot_manager().notify_admin("api-key已用完,无未使用的api-key可供切换")
|
|
||||||
self.alerted.append(key_name)
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def dump_fee(self):
|
|
||||||
pkg.utils.context.get_database_manager().dump_api_key_fee(api_keys=self.api_key, fee=self.fee)
|
|
||||||
|
|
||||||
def load_fee(self):
|
|
||||||
self.fee = pkg.utils.context.get_database_manager().load_api_key_fee()
|
|
||||||
logging.info("load fee:" + str(self.fee))
|
|
||||||
|
|||||||
@@ -1,61 +1,84 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
|
from openai.types import images_response
|
||||||
|
|
||||||
import pkg.openai.keymgr
|
from ..openai import keymgr
|
||||||
import pkg.openai.pricing as pricing
|
from ..utils import context
|
||||||
import pkg.utils.context
|
from ..audit import gatherer
|
||||||
import pkg.audit.gatherer
|
from ..openai import modelmgr
|
||||||
|
from ..openai.api import model as api_model
|
||||||
|
|
||||||
|
|
||||||
# 为其他模块提供与OpenAI交互的接口
|
|
||||||
class OpenAIInteract:
|
class OpenAIInteract:
|
||||||
api_params = {}
|
"""OpenAI 接口封装
|
||||||
|
|
||||||
key_mgr: pkg.openai.keymgr.KeysManager = None
|
将文字接口和图片接口封装供调用方使用
|
||||||
|
"""
|
||||||
|
|
||||||
audit_mgr: pkg.audit.gatherer.DataGatherer = None
|
key_mgr: keymgr.KeysManager = None
|
||||||
|
|
||||||
|
audit_mgr: gatherer.DataGatherer = None
|
||||||
|
|
||||||
default_image_api_params = {
|
default_image_api_params = {
|
||||||
"size": "256x256",
|
"size": "256x256",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client: openai.Client = None
|
||||||
|
|
||||||
def __init__(self, api_key: str):
|
def __init__(self, api_key: str):
|
||||||
# self.api_key = api_key
|
|
||||||
|
|
||||||
self.key_mgr = pkg.openai.keymgr.KeysManager(api_key)
|
self.key_mgr = keymgr.KeysManager(api_key)
|
||||||
self.audit_mgr = pkg.audit.gatherer.DataGatherer()
|
self.audit_mgr = gatherer.DataGatherer()
|
||||||
|
|
||||||
openai.api_key = self.key_mgr.get_using_key()
|
# logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length())
|
||||||
|
|
||||||
pkg.utils.context.set_openai_manager(self)
|
self.client = openai.Client(
|
||||||
|
api_key=self.key_mgr.get_using_key(),
|
||||||
# 请求OpenAI Completion
|
base_url=openai.base_url
|
||||||
def request_completion(self, prompt, stop):
|
|
||||||
config = pkg.utils.context.get_config()
|
|
||||||
response = openai.Completion.create(
|
|
||||||
prompt=prompt,
|
|
||||||
stop=stop,
|
|
||||||
timeout=config.process_message_timeout,
|
|
||||||
**config.completion_api_params
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
|
context.set_openai_manager(self)
|
||||||
prompt + response['choices'][0]['text'])
|
|
||||||
|
|
||||||
switched = self.key_mgr.report_fee(pricing.language_base_price(config.completion_api_params['model'],
|
def request_completion(self, messages: list):
|
||||||
prompt + response['choices'][0]['text']))
|
"""请求补全接口回复=
|
||||||
if switched:
|
"""
|
||||||
openai.api_key = self.key_mgr.get_using_key()
|
# 选择接口请求类
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
return response
|
request: api_model.RequestBase
|
||||||
|
|
||||||
def request_image(self, prompt):
|
model: str = config['completion_api_params']['model']
|
||||||
|
|
||||||
config = pkg.utils.context.get_config()
|
cp_parmas = config['completion_api_params'].copy()
|
||||||
params = config.image_api_params if hasattr(config, "image_api_params") else self.default_image_api_params
|
del cp_parmas['model']
|
||||||
|
|
||||||
response = openai.Image.create(
|
request = modelmgr.select_request_cls(self.client, model, messages, cp_parmas)
|
||||||
|
|
||||||
|
# 请求接口
|
||||||
|
for resp in request:
|
||||||
|
|
||||||
|
if resp['usage']['total_tokens'] > 0:
|
||||||
|
self.audit_mgr.report_text_model_usage(
|
||||||
|
model,
|
||||||
|
resp['usage']['total_tokens']
|
||||||
|
)
|
||||||
|
|
||||||
|
yield resp
|
||||||
|
|
||||||
|
def request_image(self, prompt) -> images_response.ImagesResponse:
|
||||||
|
"""请求图片接口回复
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
prompt (str): 提示语
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 响应
|
||||||
|
"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
params = config['image_api_params']
|
||||||
|
|
||||||
|
response = self.client.images.generate(
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
n=1,
|
n=1,
|
||||||
**params
|
**params
|
||||||
@@ -63,10 +86,5 @@ class OpenAIInteract:
|
|||||||
|
|
||||||
self.audit_mgr.report_image_model_usage(params['size'])
|
self.audit_mgr.report_image_model_usage(params['size'])
|
||||||
|
|
||||||
switched = self.key_mgr.report_fee(pricing.image_price(params['size']))
|
|
||||||
|
|
||||||
if switched:
|
|
||||||
openai.api_key = self.key_mgr.get_using_key()
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,48 @@
|
|||||||
# 提供与模型交互的抽象接口
|
"""OpenAI 接口底层封装
|
||||||
|
|
||||||
|
目前使用的对话接口有:
|
||||||
|
ChatCompletion - gpt-3.5-turbo 等模型
|
||||||
|
Completion - text-davinci-003 等模型
|
||||||
|
此模块封装此两个接口的请求实现,为上层提供统一的调用方式
|
||||||
|
"""
|
||||||
|
import tiktoken
|
||||||
|
import openai
|
||||||
|
|
||||||
|
from ..openai.api import model as api_model
|
||||||
|
from ..openai.api import completion as api_completion
|
||||||
|
from ..openai.api import chat_completion as api_chat_completion
|
||||||
|
|
||||||
COMPLETION_MODELS = {
|
COMPLETION_MODELS = {
|
||||||
'text-davinci-003'
|
"gpt-3.5-turbo-instruct",
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAT_COMPLETION_MODELS = {
|
||||||
|
# GPT 4 系列
|
||||||
|
"gpt-4-1106-preview",
|
||||||
|
"gpt-4-vision-preview",
|
||||||
|
"gpt-4",
|
||||||
|
"gpt-4-32k",
|
||||||
|
"gpt-4-0613",
|
||||||
|
"gpt-4-32k-0613",
|
||||||
|
"gpt-4-0314", # legacy
|
||||||
|
"gpt-4-32k-0314", # legacy
|
||||||
|
# GPT 3.5 系列
|
||||||
|
"gpt-3.5-turbo-1106",
|
||||||
|
"gpt-3.5-turbo",
|
||||||
|
"gpt-3.5-turbo-16k",
|
||||||
|
"gpt-3.5-turbo-0613", # legacy
|
||||||
|
"gpt-3.5-turbo-16k-0613", # legacy
|
||||||
|
"gpt-3.5-turbo-0301", # legacy
|
||||||
|
# One-API 接入
|
||||||
|
"SparkDesk",
|
||||||
|
"chatglm_pro",
|
||||||
|
"chatglm_std",
|
||||||
|
"chatglm_lite",
|
||||||
|
"qwen-v1",
|
||||||
|
"qwen-plus-v1",
|
||||||
|
"ERNIE-Bot",
|
||||||
|
"ERNIE-Bot-turbo",
|
||||||
|
"gemini-pro",
|
||||||
}
|
}
|
||||||
|
|
||||||
EDIT_MODELS = {
|
EDIT_MODELS = {
|
||||||
@@ -13,22 +54,86 @@ IMAGE_MODELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ModelManager
|
def select_request_cls(client: openai.Client, model_name: str, messages: list, args: dict) -> api_model.RequestBase:
|
||||||
# 由session包含
|
if model_name in CHAT_COMPLETION_MODELS:
|
||||||
class ModelMgr(object):
|
return api_chat_completion.ChatCompletionRequest(client, model_name, messages, **args)
|
||||||
|
elif model_name in COMPLETION_MODELS:
|
||||||
|
return api_completion.CompletionRequest(client, model_name, messages, **args)
|
||||||
|
raise ValueError("不支持模型[{}],请检查配置文件".format(model_name))
|
||||||
|
|
||||||
using_completion_model = ""
|
|
||||||
using_edit_model = ""
|
|
||||||
using_image_model = ""
|
|
||||||
|
|
||||||
def __init__(self):
|
def count_chat_completion_tokens(messages: list, model: str) -> int:
|
||||||
pass
|
"""Return the number of tokens used by a list of messages."""
|
||||||
|
try:
|
||||||
|
encoding = tiktoken.encoding_for_model(model)
|
||||||
|
except KeyError:
|
||||||
|
print("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
encoding = tiktoken.get_encoding("cl100k_base")
|
||||||
|
if model in {
|
||||||
|
"gpt-3.5-turbo-0613",
|
||||||
|
"gpt-3.5-turbo-16k-0613",
|
||||||
|
"gpt-4-0314",
|
||||||
|
"gpt-4-32k-0314",
|
||||||
|
"gpt-4-0613",
|
||||||
|
"gpt-4-32k-0613",
|
||||||
|
"SparkDesk",
|
||||||
|
"chatglm_pro",
|
||||||
|
"chatglm_std",
|
||||||
|
"chatglm_lite",
|
||||||
|
"qwen-v1",
|
||||||
|
"qwen-plus-v1",
|
||||||
|
"ERNIE-Bot",
|
||||||
|
"ERNIE-Bot-turbo",
|
||||||
|
"gemini-pro",
|
||||||
|
}:
|
||||||
|
tokens_per_message = 3
|
||||||
|
tokens_per_name = 1
|
||||||
|
elif model == "gpt-3.5-turbo-0301":
|
||||||
|
tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||||
|
tokens_per_name = -1 # if there's a name, the role is omitted
|
||||||
|
elif "gpt-3.5-turbo" in model:
|
||||||
|
# print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.")
|
||||||
|
return count_chat_completion_tokens(messages, model="gpt-3.5-turbo-0613")
|
||||||
|
elif "gpt-4" in model:
|
||||||
|
# print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.")
|
||||||
|
return count_chat_completion_tokens(messages, model="gpt-4-0613")
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"""count_chat_completion_tokens() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens."""
|
||||||
|
)
|
||||||
|
num_tokens = 0
|
||||||
|
for message in messages:
|
||||||
|
num_tokens += tokens_per_message
|
||||||
|
for key, value in message.items():
|
||||||
|
num_tokens += len(encoding.encode(value))
|
||||||
|
if key == "name":
|
||||||
|
num_tokens += tokens_per_name
|
||||||
|
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
|
||||||
|
return num_tokens
|
||||||
|
|
||||||
def get_using_completion_model(self):
|
|
||||||
return self.using_completion_model
|
|
||||||
|
|
||||||
def get_using_edit_model(self):
|
def count_completion_tokens(messages: list, model: str) -> int:
|
||||||
return self.using_edit_model
|
|
||||||
|
|
||||||
def get_using_image_model(self):
|
try:
|
||||||
return self.using_image_model
|
encoding = tiktoken.encoding_for_model(model)
|
||||||
|
except KeyError:
|
||||||
|
print("Warning: model not found. Using cl100k_base encoding.")
|
||||||
|
encoding = tiktoken.get_encoding("cl100k_base")
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
text += message['role'] + message['content'] + "\n"
|
||||||
|
|
||||||
|
text += "assistant: "
|
||||||
|
|
||||||
|
return len(encoding.encode(text))
|
||||||
|
|
||||||
|
|
||||||
|
def count_tokens(messages: list, model: str):
|
||||||
|
|
||||||
|
if model in CHAT_COMPLETION_MODELS:
|
||||||
|
return count_chat_completion_tokens(messages, model)
|
||||||
|
elif model in COMPLETION_MODELS:
|
||||||
|
return count_completion_tokens(messages, model)
|
||||||
|
raise ValueError("不支持模型[{}],请检查配置文件".format(model))
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
pricing = {
|
|
||||||
"base": { # 文字模型单位是1000字符
|
|
||||||
"text-davinci-003": 0.02,
|
|
||||||
},
|
|
||||||
"image": {
|
|
||||||
"256x256": 0.016,
|
|
||||||
"512x512": 0.018,
|
|
||||||
"1024x1024": 0.02,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def language_base_price(model, text):
|
|
||||||
salt_rate = 0.93
|
|
||||||
length = ((len(text.encode('utf-8')) - len(text)) / 2 + len(text)) * salt_rate
|
|
||||||
logging.debug("text length: %d" % length)
|
|
||||||
|
|
||||||
return pricing["base"][model] * length / 1000
|
|
||||||
|
|
||||||
|
|
||||||
def image_price(size):
|
|
||||||
logging.debug("image size: %s" % size)
|
|
||||||
return pricing["image"][size]
|
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
|
"""主线使用的会话管理模块
|
||||||
|
|
||||||
|
每个人、每个群单独一个session,session内部保留了对话的上下文,
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
import pkg.openai.manager
|
from ..openai import manager as openai_manager
|
||||||
import pkg.database.manager
|
from ..openai import modelmgr as openai_modelmgr
|
||||||
import pkg.utils.context
|
from ..database import manager as database_manager
|
||||||
|
from ..utils import context as context
|
||||||
|
|
||||||
|
from ..plugin import host as plugin_host
|
||||||
|
from ..plugin import models as plugin_models
|
||||||
|
|
||||||
# 运行时保存的所有session
|
# 运行时保存的所有session
|
||||||
sessions = {}
|
sessions = {}
|
||||||
@@ -17,26 +27,33 @@ class SessionOfflineStatus:
|
|||||||
|
|
||||||
# 从数据加载session
|
# 从数据加载session
|
||||||
def load_sessions():
|
def load_sessions():
|
||||||
|
"""从数据库加载sessions"""
|
||||||
|
|
||||||
global sessions
|
global sessions
|
||||||
|
|
||||||
db_inst = pkg.utils.context.get_database_manager()
|
db_inst = context.get_database_manager()
|
||||||
|
|
||||||
session_data = db_inst.load_valid_sessions()
|
session_data = db_inst.load_valid_sessions()
|
||||||
|
|
||||||
for session_name in session_data:
|
for session_name in session_data:
|
||||||
logging.info('加载session: {}'.format(session_name))
|
logging.debug('加载session: {}'.format(session_name))
|
||||||
|
|
||||||
temp_session = Session(session_name)
|
temp_session = Session(session_name)
|
||||||
temp_session.name = session_name
|
temp_session.name = session_name
|
||||||
temp_session.create_timestamp = session_data[session_name]['create_timestamp']
|
temp_session.create_timestamp = session_data[session_name]['create_timestamp']
|
||||||
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
|
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
|
||||||
temp_session.prompt = session_data[session_name]['prompt']
|
|
||||||
|
temp_session.prompt = json.loads(session_data[session_name]['prompt'])
|
||||||
|
temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
|
||||||
|
|
||||||
|
temp_session.default_prompt = json.loads(session_data[session_name]['default_prompt']) if \
|
||||||
|
session_data[session_name]['default_prompt'] else []
|
||||||
|
|
||||||
sessions[session_name] = temp_session
|
sessions[session_name] = temp_session
|
||||||
|
|
||||||
|
|
||||||
# 获取指定名称的session,如果不存在则创建一个新的
|
# 获取指定名称的session,如果不存在则创建一个新的
|
||||||
def get_session(session_name: str):
|
def get_session(session_name: str) -> 'Session':
|
||||||
global sessions
|
global sessions
|
||||||
if session_name not in sessions:
|
if session_name not in sessions:
|
||||||
sessions[session_name] = Session(session_name)
|
sessions[session_name] = Session(session_name)
|
||||||
@@ -51,38 +68,23 @@ def dump_session(session_name: str):
|
|||||||
del sessions[session_name]
|
del sessions[session_name]
|
||||||
|
|
||||||
|
|
||||||
# def blocked_func(lock: threading.Lock):
|
|
||||||
#
|
|
||||||
# def decorator(func):
|
|
||||||
# def wrapper(*args, **kwargs):
|
|
||||||
# print('lock acquire,{}'.format(lock))
|
|
||||||
# lock.acquire()
|
|
||||||
# try:
|
|
||||||
# return func(*args, **kwargs)
|
|
||||||
# finally:
|
|
||||||
# lock.release()
|
|
||||||
#
|
|
||||||
# return wrapper
|
|
||||||
#
|
|
||||||
# return decorator
|
|
||||||
|
|
||||||
|
|
||||||
# 通用的OpenAI API交互session
|
# 通用的OpenAI API交互session
|
||||||
# session内部保留了对话的上下文,
|
# session内部保留了对话的上下文,
|
||||||
# 收到用户消息后,将上下文提交给OpenAI API生成回复
|
# 收到用户消息后,将上下文提交给OpenAI API生成回复
|
||||||
class Session:
|
class Session:
|
||||||
name = ''
|
name = ''
|
||||||
|
|
||||||
prompt = ""
|
prompt = []
|
||||||
|
"""使用list来保存会话中的回合"""
|
||||||
|
|
||||||
import config
|
default_prompt = []
|
||||||
|
"""本session的默认prompt"""
|
||||||
user_name = config.user_name if hasattr(config, 'user_name') and config.user_name != '' else 'You'
|
|
||||||
bot_name = config.bot_name if hasattr(config, 'bot_name') and config.bot_name != '' else 'Bot'
|
|
||||||
|
|
||||||
create_timestamp = 0
|
create_timestamp = 0
|
||||||
|
"""会话创建时间"""
|
||||||
|
|
||||||
last_interact_timestamp = 0
|
last_interact_timestamp = 0
|
||||||
|
"""上次交互(产生回复)时间"""
|
||||||
|
|
||||||
just_switched_to_exist_session = False
|
just_switched_to_exist_session = False
|
||||||
|
|
||||||
@@ -102,22 +104,27 @@ class Session:
|
|||||||
logging.debug('{},lock release successfully,{}'.format(self.name, self.response_lock))
|
logging.debug('{},lock release successfully,{}'.format(self.name, self.response_lock))
|
||||||
|
|
||||||
# 从配置文件获取会话预设信息
|
# 从配置文件获取会话预设信息
|
||||||
def get_default_prompt(self):
|
def get_default_prompt(self, use_default: str = None):
|
||||||
config = pkg.utils.context.get_config()
|
import pkg.openai.dprompt as dprompt
|
||||||
user_name = config.user_name if hasattr(config, 'user_name') and config.user_name != '' else 'You'
|
|
||||||
bot_name = config.bot_name if hasattr(config, 'bot_name') and config.bot_name != '' else 'Bot'
|
if use_default is None:
|
||||||
return user_name + ":{}\n".format(config.default_prompt if hasattr(config, 'default_prompt') \
|
use_default = dprompt.mode_inst().get_using_name()
|
||||||
and config.default_prompt != "" else '') + \
|
|
||||||
bot_name + ":好的\n"
|
current_default_prompt, _ = dprompt.mode_inst().get_prompt(use_default)
|
||||||
|
return current_default_prompt
|
||||||
|
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.create_timestamp = int(time.time())
|
self.create_timestamp = int(time.time())
|
||||||
self.last_interact_timestamp = int(time.time())
|
self.last_interact_timestamp = int(time.time())
|
||||||
|
self.prompt = []
|
||||||
|
self.token_counts = []
|
||||||
self.schedule()
|
self.schedule()
|
||||||
|
|
||||||
self.response_lock = threading.Lock()
|
self.response_lock = threading.Lock()
|
||||||
self.prompt = self.get_default_prompt()
|
|
||||||
|
self.default_prompt = self.get_default_prompt()
|
||||||
|
logging.debug("prompt is: {}".format(self.default_prompt))
|
||||||
|
|
||||||
# 设定检查session最后一次对话是否超过过期时间的计时器
|
# 设定检查session最后一次对话是否超过过期时间的计时器
|
||||||
def schedule(self):
|
def schedule(self):
|
||||||
@@ -133,9 +140,20 @@ class Session:
|
|||||||
if self.create_timestamp != create_timestamp or self not in sessions.values():
|
if self.create_timestamp != create_timestamp or self not in sessions.values():
|
||||||
return
|
return
|
||||||
|
|
||||||
config = pkg.utils.context.get_config()
|
config = context.get_config_manager().data
|
||||||
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time:
|
if int(time.time()) - self.last_interact_timestamp > config['session_expire_time']:
|
||||||
logging.info('session {} 已过期'.format(self.name))
|
logging.info('session {} 已过期'.format(self.name))
|
||||||
|
|
||||||
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
'session_name': self.name,
|
||||||
|
'session': self,
|
||||||
|
'session_expire_time': config['session_expire_time']
|
||||||
|
}
|
||||||
|
event = plugin_host.emit(plugin_models.SessionExpired, **args)
|
||||||
|
if event.is_prevented_default():
|
||||||
|
return
|
||||||
|
|
||||||
self.reset(expired=True, schedule_new=False)
|
self.reset(expired=True, schedule_new=False)
|
||||||
|
|
||||||
# 删除此session
|
# 删除此session
|
||||||
@@ -144,72 +162,252 @@ class Session:
|
|||||||
|
|
||||||
# 请求回复
|
# 请求回复
|
||||||
# 这个函数是阻塞的
|
# 这个函数是阻塞的
|
||||||
def append(self, text: str) -> str:
|
def query(self, text: str=None) -> tuple[str, str, list[str]]:
|
||||||
|
"""向session中添加一条消息,返回接口回复
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): 用户消息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[str, str]: (接口回复, finish_reason, 已调用的函数列表)
|
||||||
|
"""
|
||||||
|
|
||||||
self.last_interact_timestamp = int(time.time())
|
self.last_interact_timestamp = int(time.time())
|
||||||
|
|
||||||
# max_rounds = config.prompt_submit_round_amount if hasattr(config, 'prompt_submit_round_amount') else 7
|
# 触发插件事件
|
||||||
config = pkg.utils.context.get_config()
|
if not self.prompt:
|
||||||
max_rounds = 1000 # 不再限制回合数
|
args = {
|
||||||
max_length = config.prompt_submit_length if hasattr(config, "prompt_submit_length") else 1024
|
'session_name': self.name,
|
||||||
|
'session': self,
|
||||||
|
'default_prompt': self.default_prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
event = plugin_host.emit(plugin_models.SessionFirstMessageReceived, **args)
|
||||||
|
if event.is_prevented_default():
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
max_length = config['prompt_submit_length']
|
||||||
|
|
||||||
|
local_default_prompt = self.default_prompt.copy()
|
||||||
|
local_prompt = self.prompt.copy()
|
||||||
|
|
||||||
|
# 触发PromptPreProcessing事件
|
||||||
|
args = {
|
||||||
|
'session_name': self.name,
|
||||||
|
'default_prompt': self.default_prompt,
|
||||||
|
'prompt': self.prompt,
|
||||||
|
'text_message': text,
|
||||||
|
}
|
||||||
|
|
||||||
|
event = plugin_host.emit(plugin_models.PromptPreProcessing, **args)
|
||||||
|
|
||||||
|
if event.get_return_value('default_prompt') is not None:
|
||||||
|
local_default_prompt = event.get_return_value('default_prompt')
|
||||||
|
|
||||||
|
if event.get_return_value('prompt') is not None:
|
||||||
|
local_prompt = event.get_return_value('prompt')
|
||||||
|
|
||||||
|
if event.get_return_value('text_message') is not None:
|
||||||
|
text = event.get_return_value('text_message')
|
||||||
|
|
||||||
|
# 裁剪messages到合适长度
|
||||||
|
prompts, _ = self.cut_out(text, max_length, local_default_prompt, local_prompt)
|
||||||
|
|
||||||
|
res_text = ""
|
||||||
|
|
||||||
|
pending_msgs = []
|
||||||
|
|
||||||
|
total_tokens = 0
|
||||||
|
|
||||||
|
finish_reason: str = ""
|
||||||
|
|
||||||
|
funcs = []
|
||||||
|
|
||||||
|
trace_func_calls = config['trace_function_calls']
|
||||||
|
botmgr = context.get_qqbot_manager()
|
||||||
|
|
||||||
|
session_name_spt: list[str] = self.name.split("_")
|
||||||
|
|
||||||
|
pending_res_text = ""
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# TODO 对不起,我知道这样非常非常屎山,但我之后会重构的
|
||||||
|
for resp in context.get_openai_manager().request_completion(prompts):
|
||||||
|
|
||||||
|
if pending_res_text != "":
|
||||||
|
botmgr.adapter.send_message(
|
||||||
|
session_name_spt[0],
|
||||||
|
session_name_spt[1],
|
||||||
|
pending_res_text
|
||||||
|
)
|
||||||
|
pending_res_text = ""
|
||||||
|
|
||||||
|
finish_reason = resp['choices'][0]['finish_reason']
|
||||||
|
|
||||||
|
if resp['choices'][0]['message']['role'] == "assistant" and resp['choices'][0]['message']['content'] != None: # 包含纯文本响应
|
||||||
|
|
||||||
|
if not trace_func_calls:
|
||||||
|
res_text += resp['choices'][0]['message']['content']
|
||||||
|
else:
|
||||||
|
res_text = resp['choices'][0]['message']['content']
|
||||||
|
pending_res_text = resp['choices'][0]['message']['content']
|
||||||
|
|
||||||
|
total_tokens += resp['usage']['total_tokens']
|
||||||
|
|
||||||
|
msg = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": resp['choices'][0]['message']['content']
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'function_call' in resp['choices'][0]['message']:
|
||||||
|
msg['function_call'] = json.dumps(resp['choices'][0]['message']['function_call'])
|
||||||
|
|
||||||
|
pending_msgs.append(msg)
|
||||||
|
|
||||||
|
if resp['choices'][0]['message']['type'] == 'function_call':
|
||||||
|
# self.prompt.append(
|
||||||
|
# {
|
||||||
|
# "role": "assistant",
|
||||||
|
# "content": "function call: "+json.dumps(resp['choices'][0]['message']['function_call'])
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
if trace_func_calls:
|
||||||
|
botmgr.adapter.send_message(
|
||||||
|
session_name_spt[0],
|
||||||
|
session_name_spt[1],
|
||||||
|
"调用函数 "+resp['choices'][0]['message']['function_call']['name'] + "..."
|
||||||
|
)
|
||||||
|
|
||||||
|
total_tokens += resp['usage']['total_tokens']
|
||||||
|
elif resp['choices'][0]['message']['type'] == 'function_return':
|
||||||
|
# self.prompt.append(
|
||||||
|
# {
|
||||||
|
# "role": "function",
|
||||||
|
# "name": resp['choices'][0]['message']['function_name'],
|
||||||
|
# "content": json.dumps(resp['choices'][0]['message']['content'])
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
|
||||||
|
# total_tokens += resp['usage']['total_tokens']
|
||||||
|
funcs.append(
|
||||||
|
resp['choices'][0]['message']['function_name']
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
# 向API请求补全
|
# 向API请求补全
|
||||||
response = pkg.utils.context.get_openai_manager().request_completion(
|
# message, total_token = pkg.utils.context.get_openai_manager().request_completion(
|
||||||
self.cut_out(self.prompt + self.user_name + ':' +
|
# prompts,
|
||||||
text + '\n' + self.bot_name + ':',
|
# )
|
||||||
max_rounds, max_length),
|
|
||||||
self.user_name + ':')
|
|
||||||
|
|
||||||
self.prompt += self.user_name + ':' + text + '\n' + self.bot_name + ':'
|
# 成功获取,处理回复
|
||||||
# print(response)
|
# res_test = message
|
||||||
# 处理回复
|
res_ans = res_text.strip()
|
||||||
res_test = response["choices"][0]["text"]
|
|
||||||
res_ans = res_test
|
|
||||||
|
|
||||||
# 去除开头可能的提示
|
# 将此次对话的双方内容加入到prompt中
|
||||||
res_ans_spt = res_test.split("\n\n")
|
# self.prompt.append({'role': 'user', 'content': text})
|
||||||
if len(res_ans_spt) > 1:
|
# self.prompt.append({'role': 'assistant', 'content': res_ans})
|
||||||
del (res_ans_spt[0])
|
if text:
|
||||||
res_ans = '\n\n'.join(res_ans_spt)
|
self.prompt.append({'role': 'user', 'content': text})
|
||||||
|
# 添加pending_msgs
|
||||||
|
self.prompt += pending_msgs
|
||||||
|
|
||||||
self.prompt += "{}".format(res_ans) + '\n'
|
# 向token_counts中添加本回合的token数量
|
||||||
|
# self.token_counts.append(total_tokens-total_token_before_query)
|
||||||
|
# logging.debug("本回合使用token: {}, session counts: {}".format(total_tokens-total_token_before_query, self.token_counts))
|
||||||
|
|
||||||
if self.just_switched_to_exist_session:
|
if self.just_switched_to_exist_session:
|
||||||
self.just_switched_to_exist_session = False
|
self.just_switched_to_exist_session = False
|
||||||
self.set_ongoing()
|
self.set_ongoing()
|
||||||
|
|
||||||
return res_ans
|
# 上报使用量数据
|
||||||
|
session_type = session_name_spt[0]
|
||||||
|
session_id = session_name_spt[1]
|
||||||
|
|
||||||
# 从尾部截取prompt里不多于max_rounds个回合,长度不大于max_tokens的字符串
|
ability_provider = "QChatGPT.Text"
|
||||||
# 保证都是完整的对话
|
usage = total_tokens
|
||||||
def cut_out(self, prompt: str, max_rounds: int, max_tokens: int) -> str:
|
model_name = context.get_config_manager().data['completion_api_params']['model']
|
||||||
# 分隔出每个回合
|
response_seconds = int(time.time() - start_time)
|
||||||
rounds_spt_by_user_name = prompt.split(self.user_name + ':')
|
retry_times = -1 # 暂不记录
|
||||||
|
|
||||||
result = ''
|
context.get_center_v2_api().usage.post_query_record(
|
||||||
|
session_type=session_type,
|
||||||
|
session_id=session_id,
|
||||||
|
query_ability_provider=ability_provider,
|
||||||
|
usage=usage,
|
||||||
|
model_name=model_name,
|
||||||
|
response_seconds=response_seconds,
|
||||||
|
retry_times=retry_times
|
||||||
|
)
|
||||||
|
|
||||||
checked_rounds = 0
|
return res_ans if res_ans[0] != '\n' else res_ans[1:], finish_reason, funcs
|
||||||
# 从后往前遍历,加到result前面,检查result是否符合要求
|
|
||||||
for i in range(len(rounds_spt_by_user_name) - 1, 0, -1):
|
|
||||||
result_temp = self.user_name + ':' + rounds_spt_by_user_name[i] + result
|
|
||||||
checked_rounds += 1
|
|
||||||
|
|
||||||
if checked_rounds > max_rounds:
|
# 删除上一回合并返回上一回合的问题
|
||||||
|
def undo(self) -> str:
|
||||||
|
self.last_interact_timestamp = int(time.time())
|
||||||
|
|
||||||
|
# 删除最后两个消息
|
||||||
|
if len(self.prompt) < 2:
|
||||||
|
raise Exception('之前无对话,无法撤销')
|
||||||
|
|
||||||
|
question = self.prompt[-2]['content']
|
||||||
|
self.prompt = self.prompt[:-2]
|
||||||
|
self.token_counts = self.token_counts[:-1]
|
||||||
|
|
||||||
|
# 返回上一回合的问题
|
||||||
|
return question
|
||||||
|
|
||||||
|
# 构建对话体
|
||||||
|
def cut_out(self, msg: str, max_tokens: int, default_prompt: list, prompt: list) -> tuple[list, list]:
|
||||||
|
"""将现有prompt进行切割处理,使得新的prompt长度不超过max_tokens
|
||||||
|
|
||||||
|
:return: (新的prompt, 新的token_counts)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 最终由三个部分组成
|
||||||
|
# - default_prompt 情景预设固定值
|
||||||
|
# - changable_prompts 可变部分, 此会话中的历史对话回合
|
||||||
|
# - current_question 当前问题
|
||||||
|
|
||||||
|
# 包装目前的对话回合内容
|
||||||
|
changable_prompts = []
|
||||||
|
|
||||||
|
use_model = context.get_config_manager().data['completion_api_params']['model']
|
||||||
|
|
||||||
|
ptr = len(prompt) - 1
|
||||||
|
|
||||||
|
# 直接从后向前扫描拼接,不管是否是整回合
|
||||||
|
while ptr >= 0:
|
||||||
|
if openai_modelmgr.count_tokens(prompt[ptr:ptr+1]+changable_prompts, use_model) > max_tokens:
|
||||||
break
|
break
|
||||||
|
|
||||||
if int((len(result_temp.encode('utf-8')) - len(result_temp)) / 2 + len(result_temp)) > max_tokens:
|
changable_prompts.insert(0, prompt[ptr])
|
||||||
break
|
|
||||||
|
|
||||||
result = result_temp
|
ptr -= 1
|
||||||
|
|
||||||
logging.debug('cut_out: {}'.format(result))
|
# 将default_prompt和changable_prompts合并
|
||||||
return result
|
result_prompt = default_prompt + changable_prompts
|
||||||
|
|
||||||
|
# 添加当前问题
|
||||||
|
if msg:
|
||||||
|
result_prompt.append(
|
||||||
|
{
|
||||||
|
'role': 'user',
|
||||||
|
'content': msg
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.debug("cut_out: {}".format(json.dumps(result_prompt, ensure_ascii=False, indent=4)))
|
||||||
|
|
||||||
|
return result_prompt, openai_modelmgr.count_tokens(changable_prompts, use_model)
|
||||||
|
|
||||||
# 持久化session
|
# 持久化session
|
||||||
def persistence(self):
|
def persistence(self):
|
||||||
if self.prompt == self.get_default_prompt():
|
if self.prompt == self.get_default_prompt():
|
||||||
return
|
return
|
||||||
|
|
||||||
db_inst = pkg.utils.context.get_database_manager()
|
db_inst = context.get_database_manager()
|
||||||
|
|
||||||
name_spt = self.name.split('_')
|
name_spt = self.name.split('_')
|
||||||
|
|
||||||
@@ -217,18 +415,31 @@ class Session:
|
|||||||
subject_number = int(name_spt[1])
|
subject_number = int(name_spt[1])
|
||||||
|
|
||||||
db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp,
|
db_inst.persistence_session(subject_type, subject_number, self.create_timestamp, self.last_interact_timestamp,
|
||||||
self.prompt)
|
json.dumps(self.prompt), json.dumps(self.default_prompt), json.dumps(self.token_counts))
|
||||||
|
|
||||||
# 重置session
|
# 重置session
|
||||||
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True):
|
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None, persist: bool = False):
|
||||||
if self.prompt != self.get_default_prompt():
|
if self.prompt:
|
||||||
self.persistence()
|
self.persistence()
|
||||||
if explicit:
|
if explicit:
|
||||||
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
'session_name': self.name,
|
||||||
|
'session': self
|
||||||
|
}
|
||||||
|
|
||||||
|
# 此事件不支持阻止默认行为
|
||||||
|
_ = plugin_host.emit(plugin_models.SessionExplicitReset, **args)
|
||||||
|
|
||||||
|
context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
||||||
|
|
||||||
if expired:
|
if expired:
|
||||||
pkg.utils.context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
|
context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
|
||||||
self.prompt = self.get_default_prompt()
|
|
||||||
|
if not persist: # 不要求保持default prompt
|
||||||
|
self.default_prompt = self.get_default_prompt(use_prompt)
|
||||||
|
self.prompt = []
|
||||||
|
self.token_counts = []
|
||||||
self.create_timestamp = int(time.time())
|
self.create_timestamp = int(time.time())
|
||||||
self.last_interact_timestamp = int(time.time())
|
self.last_interact_timestamp = int(time.time())
|
||||||
self.just_switched_to_exist_session = False
|
self.just_switched_to_exist_session = False
|
||||||
@@ -240,11 +451,11 @@ class Session:
|
|||||||
|
|
||||||
# 将本session的数据库状态设置为on_going
|
# 将本session的数据库状态设置为on_going
|
||||||
def set_ongoing(self):
|
def set_ongoing(self):
|
||||||
pkg.utils.context.get_database_manager().set_session_ongoing(self.name, self.create_timestamp)
|
context.get_database_manager().set_session_ongoing(self.name, self.create_timestamp)
|
||||||
|
|
||||||
# 切换到上一个session
|
# 切换到上一个session
|
||||||
def last_session(self):
|
def last_session(self):
|
||||||
last_one = pkg.utils.context.get_database_manager().last_session(self.name, self.last_interact_timestamp)
|
last_one = context.get_database_manager().last_session(self.name, self.last_interact_timestamp)
|
||||||
if last_one is None:
|
if last_one is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@@ -252,14 +463,18 @@ class Session:
|
|||||||
|
|
||||||
self.create_timestamp = last_one['create_timestamp']
|
self.create_timestamp = last_one['create_timestamp']
|
||||||
self.last_interact_timestamp = last_one['last_interact_timestamp']
|
self.last_interact_timestamp = last_one['last_interact_timestamp']
|
||||||
self.prompt = last_one['prompt']
|
|
||||||
|
self.prompt = json.loads(last_one['prompt'])
|
||||||
|
self.token_counts = json.loads(last_one['token_counts'])
|
||||||
|
|
||||||
|
self.default_prompt = json.loads(last_one['default_prompt']) if last_one['default_prompt'] else []
|
||||||
|
|
||||||
self.just_switched_to_exist_session = True
|
self.just_switched_to_exist_session = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# 切换到下一个session
|
# 切换到下一个session
|
||||||
def next_session(self):
|
def next_session(self):
|
||||||
next_one = pkg.utils.context.get_database_manager().next_session(self.name, self.last_interact_timestamp)
|
next_one = context.get_database_manager().next_session(self.name, self.last_interact_timestamp)
|
||||||
if next_one is None:
|
if next_one is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
@@ -267,14 +482,23 @@ class Session:
|
|||||||
|
|
||||||
self.create_timestamp = next_one['create_timestamp']
|
self.create_timestamp = next_one['create_timestamp']
|
||||||
self.last_interact_timestamp = next_one['last_interact_timestamp']
|
self.last_interact_timestamp = next_one['last_interact_timestamp']
|
||||||
self.prompt = next_one['prompt']
|
|
||||||
|
self.prompt = json.loads(next_one['prompt'])
|
||||||
|
self.token_counts = json.loads(next_one['token_counts'])
|
||||||
|
|
||||||
|
self.default_prompt = json.loads(next_one['default_prompt']) if next_one['default_prompt'] else []
|
||||||
|
|
||||||
self.just_switched_to_exist_session = True
|
self.just_switched_to_exist_session = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def list_history(self, capacity: int = 10, page: int = 0):
|
def list_history(self, capacity: int = 10, page: int = 0):
|
||||||
return pkg.utils.context.get_database_manager().list_history(self.name, capacity, page,
|
return context.get_database_manager().list_history(self.name, capacity, page)
|
||||||
self.get_default_prompt())
|
|
||||||
|
def delete_history(self, index: int) -> bool:
|
||||||
|
return context.get_database_manager().delete_history(self.name, index)
|
||||||
|
|
||||||
|
def delete_all_history(self) -> bool:
|
||||||
|
return context.get_database_manager().delete_all_history(self.name)
|
||||||
|
|
||||||
def draw_image(self, prompt: str):
|
def draw_image(self, prompt: str):
|
||||||
return pkg.utils.context.get_openai_manager().request_image(prompt)
|
return context.get_openai_manager().request_image(prompt)
|
||||||
|
|||||||
4
pkg/plugin/__init__.py
Normal file
4
pkg/plugin/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""插件支持包
|
||||||
|
|
||||||
|
包含插件基类、插件宿主以及部分API接口
|
||||||
|
"""
|
||||||
578
pkg/plugin/host.py
Normal file
578
pkg/plugin/host.py
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
# 插件管理模块
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..utils import updater as updater
|
||||||
|
from ..utils import network as network
|
||||||
|
from ..utils import context as context
|
||||||
|
from ..plugin import switch as switch
|
||||||
|
from ..plugin import settings as settings
|
||||||
|
from ..qqbot import adapter as msadapter
|
||||||
|
from ..plugin import metadata as metadata
|
||||||
|
|
||||||
|
from mirai import Mirai
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from CallingGPT.session.session import Session
|
||||||
|
|
||||||
|
__plugins__ = {}
|
||||||
|
"""插件列表
|
||||||
|
|
||||||
|
示例:
|
||||||
|
{
|
||||||
|
"example": {
|
||||||
|
"path": "plugins/example/main.py",
|
||||||
|
"enabled: True,
|
||||||
|
"name": "example",
|
||||||
|
"description": "example",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "RockChinQ",
|
||||||
|
"class": <class 'plugins.example.ExamplePlugin'>,
|
||||||
|
"hooks": {
|
||||||
|
"person_message": [
|
||||||
|
<function ExamplePlugin.person_message at 0x0000020E1D1B8D38>
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"instance": None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__plugins_order__ = []
|
||||||
|
"""插件顺序"""
|
||||||
|
|
||||||
|
__enable_content_functions__ = True
|
||||||
|
"""是否启用内容函数"""
|
||||||
|
|
||||||
|
__callable_functions__ = []
|
||||||
|
"""供GPT调用的函数结构"""
|
||||||
|
|
||||||
|
__function_inst_map__: dict[str, callable] = {}
|
||||||
|
"""函数名:实例 映射"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_plugin_order():
|
||||||
|
"""根据__plugin__生成插件初始顺序,无视是否启用"""
|
||||||
|
global __plugins_order__
|
||||||
|
__plugins_order__ = []
|
||||||
|
for plugin_name in __plugins__:
|
||||||
|
__plugins_order__.append(plugin_name)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_plugins():
|
||||||
|
"""按照顺序迭代插件"""
|
||||||
|
for plugin_name in __plugins_order__:
|
||||||
|
if plugin_name not in __plugins__:
|
||||||
|
continue
|
||||||
|
yield __plugins__[plugin_name]
|
||||||
|
|
||||||
|
|
||||||
|
def iter_plugins_name():
|
||||||
|
"""迭代插件名"""
|
||||||
|
for plugin_name in __plugins_order__:
|
||||||
|
yield plugin_name
|
||||||
|
|
||||||
|
|
||||||
|
__current_module_path__ = ""
|
||||||
|
|
||||||
|
|
||||||
|
def walk_plugin_path(module, prefix="", path_prefix=""):
|
||||||
|
global __current_module_path__
|
||||||
|
"""遍历插件路径"""
|
||||||
|
for item in pkgutil.iter_modules(module.__path__):
|
||||||
|
if item.ispkg:
|
||||||
|
logging.debug("扫描插件包: plugins/{}".format(path_prefix + item.name))
|
||||||
|
walk_plugin_path(
|
||||||
|
__import__(module.__name__ + "." + item.name, fromlist=[""]),
|
||||||
|
prefix + item.name + ".",
|
||||||
|
path_prefix + item.name + "/",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logging.debug(
|
||||||
|
"扫描插件模块: plugins/{}".format(path_prefix + item.name + ".py")
|
||||||
|
)
|
||||||
|
__current_module_path__ = "plugins/" + path_prefix + item.name + ".py"
|
||||||
|
|
||||||
|
importlib.import_module(module.__name__ + "." + item.name)
|
||||||
|
logging.debug(
|
||||||
|
"加载模块: plugins/{} 成功".format(path_prefix + item.name + ".py")
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logging.error(
|
||||||
|
"加载模块: plugins/{} 失败: {}".format(
|
||||||
|
path_prefix + item.name + ".py", sys.exc_info()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def load_plugins():
|
||||||
|
"""加载插件"""
|
||||||
|
logging.debug("加载插件")
|
||||||
|
PluginHost()
|
||||||
|
walk_plugin_path(__import__("plugins"))
|
||||||
|
|
||||||
|
logging.debug(__plugins__)
|
||||||
|
|
||||||
|
# 加载开关数据
|
||||||
|
switch.load_switch()
|
||||||
|
|
||||||
|
# 生成初始顺序
|
||||||
|
generate_plugin_order()
|
||||||
|
# 加载插件顺序
|
||||||
|
settings.load_settings()
|
||||||
|
|
||||||
|
logging.debug("registered plugins: {}".format(__plugins__))
|
||||||
|
|
||||||
|
# 输出已注册的内容函数列表
|
||||||
|
logging.debug("registered content functions: {}".format(__callable_functions__))
|
||||||
|
logging.debug("function instance map: {}".format(__function_inst_map__))
|
||||||
|
|
||||||
|
# 迁移插件源地址记录
|
||||||
|
metadata.do_plugin_git_repo_migrate()
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_plugins():
|
||||||
|
"""初始化插件"""
|
||||||
|
logging.debug("初始化插件")
|
||||||
|
import pkg.plugin.models as models
|
||||||
|
|
||||||
|
successfully_initialized_plugins = []
|
||||||
|
|
||||||
|
for plugin in iter_plugins():
|
||||||
|
# if not plugin['enabled']:
|
||||||
|
# continue
|
||||||
|
try:
|
||||||
|
models.__current_registering_plugin__ = plugin["name"]
|
||||||
|
plugin["instance"] = plugin["class"](plugin_host=context.get_plugin_host())
|
||||||
|
# logging.info("插件 {} 已初始化".format(plugin['name']))
|
||||||
|
successfully_initialized_plugins.append(plugin["name"])
|
||||||
|
except:
|
||||||
|
logging.error("插件{}初始化时发生错误: {}".format(plugin["name"], sys.exc_info()))
|
||||||
|
logging.debug(traceback.format_exc())
|
||||||
|
|
||||||
|
logging.info("以下插件已初始化: {}".format(", ".join(successfully_initialized_plugins)))
|
||||||
|
|
||||||
|
|
||||||
|
def unload_plugins():
|
||||||
|
"""卸载插件"""
|
||||||
|
# 不再显式卸载插件,因为当程序结束时,插件的析构函数会被系统执行
|
||||||
|
# for plugin in __plugins__.values():
|
||||||
|
# if plugin['enabled'] and plugin['instance'] is not None:
|
||||||
|
# if not hasattr(plugin['instance'], '__del__'):
|
||||||
|
# logging.warning("插件{}没有定义析构函数".format(plugin['name']))
|
||||||
|
# else:
|
||||||
|
# try:
|
||||||
|
# plugin['instance'].__del__()
|
||||||
|
# logging.info("卸载插件: {}".format(plugin['name']))
|
||||||
|
# plugin['instance'] = None
|
||||||
|
# except:
|
||||||
|
# logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_plugin_repo_label(repo_url: str) -> list[str]:
|
||||||
|
"""获取username, repo"""
|
||||||
|
|
||||||
|
# 提取 username/repo , 正则表达式
|
||||||
|
repo = re.findall(
|
||||||
|
r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
|
||||||
|
repo_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(repo) > 0: # github
|
||||||
|
return repo[0].split("/")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def download_plugin_source_code(repo_url: str, target_path: str) -> str:
|
||||||
|
"""下载插件源码"""
|
||||||
|
# 检查源类型
|
||||||
|
|
||||||
|
# 提取 username/repo , 正则表达式
|
||||||
|
repo = get_github_plugin_repo_label(repo_url)
|
||||||
|
|
||||||
|
target_path += repo[1]
|
||||||
|
|
||||||
|
if repo is not None: # github
|
||||||
|
logging.info("从 GitHub 下载插件源码...")
|
||||||
|
|
||||||
|
zipball_url = f"https://api.github.com/repos/{'/'.join(repo)}/zipball/HEAD"
|
||||||
|
|
||||||
|
zip_resp = requests.get(
|
||||||
|
url=zipball_url, proxies=network.wrapper_proxies(), stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if zip_resp.status_code != 200:
|
||||||
|
raise Exception("下载源码失败: {}".format(zip_resp.text))
|
||||||
|
|
||||||
|
if os.path.exists("temp/" + target_path):
|
||||||
|
shutil.rmtree("temp/" + target_path)
|
||||||
|
|
||||||
|
if os.path.exists(target_path):
|
||||||
|
shutil.rmtree(target_path)
|
||||||
|
|
||||||
|
os.makedirs("temp/" + target_path)
|
||||||
|
|
||||||
|
with open("temp/" + target_path + "/source.zip", "wb") as f:
|
||||||
|
for chunk in zip_resp.iter_content(chunk_size=1024):
|
||||||
|
if chunk:
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
logging.info("下载完成, 解压...")
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
with zipfile.ZipFile("temp/" + target_path + "/source.zip", "r") as zip_ref:
|
||||||
|
zip_ref.extractall("temp/" + target_path)
|
||||||
|
os.remove("temp/" + target_path + "/source.zip")
|
||||||
|
|
||||||
|
# 目标是 username-repo-hash , 用正则表达式提取完整的文件夹名,复制到 plugins/repo
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# 获取解压后的文件夹名
|
||||||
|
unzip_dir = glob.glob("temp/" + target_path + "/*")[0]
|
||||||
|
|
||||||
|
# 复制到 plugins/repo
|
||||||
|
shutil.copytree(unzip_dir, target_path + "/")
|
||||||
|
|
||||||
|
# 删除解压后的文件夹
|
||||||
|
shutil.rmtree(unzip_dir)
|
||||||
|
|
||||||
|
logging.info("解压完成")
|
||||||
|
else:
|
||||||
|
raise Exception("暂不支持的源类型,请使用 GitHub 仓库发行插件。")
|
||||||
|
|
||||||
|
return repo[1]
|
||||||
|
|
||||||
|
|
||||||
|
def check_requirements(path: str):
|
||||||
|
# 检查此目录是否包含requirements.txt
|
||||||
|
if os.path.exists(path + "/requirements.txt"):
|
||||||
|
logging.info("检测到requirements.txt,正在安装依赖")
|
||||||
|
import pkg.utils.pkgmgr
|
||||||
|
|
||||||
|
pkg.utils.pkgmgr.install_requirements(path + "/requirements.txt")
|
||||||
|
|
||||||
|
import pkg.utils.log as log
|
||||||
|
|
||||||
|
log.reset_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def install_plugin(repo_url: str):
|
||||||
|
"""安装插件,从git储存库获取并解决依赖"""
|
||||||
|
|
||||||
|
repo_label = download_plugin_source_code(repo_url, "plugins/")
|
||||||
|
|
||||||
|
check_requirements("plugins/" + repo_label)
|
||||||
|
|
||||||
|
metadata.set_plugin_metadata(repo_label, repo_url, int(time.time()), "HEAD")
|
||||||
|
|
||||||
|
# 上报安装记录
|
||||||
|
context.get_center_v2_api().plugin.post_install_record(
|
||||||
|
plugin={
|
||||||
|
"name": "unknown",
|
||||||
|
"remote": repo_url,
|
||||||
|
"author": "unknown",
|
||||||
|
"version": "HEAD",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_plugin(plugin_name: str) -> str:
|
||||||
|
"""卸载插件"""
|
||||||
|
if plugin_name not in __plugins__:
|
||||||
|
raise Exception("插件不存在")
|
||||||
|
|
||||||
|
plugin_info = get_plugin_info_for_audit(plugin_name)
|
||||||
|
|
||||||
|
# 获取文件夹路径
|
||||||
|
plugin_path = __plugins__[plugin_name]["path"].replace("\\", "/")
|
||||||
|
|
||||||
|
# 剪切路径为plugins/插件名
|
||||||
|
plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
|
||||||
|
|
||||||
|
# 删除文件夹
|
||||||
|
shutil.rmtree("plugins/" + plugin_path)
|
||||||
|
|
||||||
|
# 上报卸载记录
|
||||||
|
context.get_center_v2_api().plugin.post_remove_record(
|
||||||
|
plugin=plugin_info
|
||||||
|
)
|
||||||
|
|
||||||
|
return "plugins/" + plugin_path
|
||||||
|
|
||||||
|
|
||||||
|
def update_plugin(plugin_name: str):
|
||||||
|
"""更新插件"""
|
||||||
|
# 检查是否有远程地址记录
|
||||||
|
plugin_path_name = get_plugin_path_name_by_plugin_name(plugin_name)
|
||||||
|
|
||||||
|
meta = metadata.get_plugin_metadata(plugin_path_name)
|
||||||
|
|
||||||
|
if meta == {}:
|
||||||
|
raise Exception("没有此插件元数据信息,无法更新")
|
||||||
|
|
||||||
|
old_plugin_info = get_plugin_info_for_audit(plugin_name)
|
||||||
|
|
||||||
|
context.get_center_v2_api().plugin.post_update_record(
|
||||||
|
plugin=old_plugin_info,
|
||||||
|
old_version=old_plugin_info['version'],
|
||||||
|
new_version='HEAD',
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_url = meta["source"]
|
||||||
|
if (
|
||||||
|
remote_url == "https://github.com/RockChinQ/QChatGPT"
|
||||||
|
or remote_url == "https://gitee.com/RockChin/QChatGPT"
|
||||||
|
or remote_url == ""
|
||||||
|
or remote_url is None
|
||||||
|
or remote_url == "http://github.com/RockChinQ/QChatGPT"
|
||||||
|
or remote_url == "http://gitee.com/RockChin/QChatGPT"
|
||||||
|
):
|
||||||
|
raise Exception("插件没有远程地址记录,无法更新")
|
||||||
|
|
||||||
|
# 重新安装插件
|
||||||
|
logging.info("正在重新安装插件以进行更新...")
|
||||||
|
|
||||||
|
install_plugin(remote_url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_name_by_path_name(plugin_path_name: str) -> str:
|
||||||
|
for k, v in __plugins__.items():
|
||||||
|
if v["path"] == "plugins/" + plugin_path_name + "/main.py":
|
||||||
|
return k
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_path_name_by_plugin_name(plugin_name: str) -> str:
|
||||||
|
if plugin_name not in __plugins__:
|
||||||
|
return None
|
||||||
|
|
||||||
|
plugin_main_module_path = __plugins__[plugin_name]["path"]
|
||||||
|
|
||||||
|
plugin_main_module_path = plugin_main_module_path.replace("\\", "/")
|
||||||
|
|
||||||
|
spt = plugin_main_module_path.split("/")
|
||||||
|
|
||||||
|
return spt[1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_info_for_audit(plugin_name: str) -> dict:
|
||||||
|
"""获取插件信息"""
|
||||||
|
if plugin_name not in __plugins__:
|
||||||
|
return {}
|
||||||
|
plugin = __plugins__[plugin_name]
|
||||||
|
|
||||||
|
name = plugin["name"]
|
||||||
|
meta = metadata.get_plugin_metadata(get_plugin_path_name_by_plugin_name(name))
|
||||||
|
remote = meta["source"] if meta != {} else ""
|
||||||
|
author = plugin["author"]
|
||||||
|
version = plugin["version"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"remote": remote,
|
||||||
|
"author": author,
|
||||||
|
"version": version,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EventContext:
|
||||||
|
"""事件上下文"""
|
||||||
|
|
||||||
|
eid = 0
|
||||||
|
"""事件编号"""
|
||||||
|
|
||||||
|
name = ""
|
||||||
|
|
||||||
|
__prevent_default__ = False
|
||||||
|
"""是否阻止默认行为"""
|
||||||
|
|
||||||
|
__prevent_postorder__ = False
|
||||||
|
"""是否阻止后续插件的执行"""
|
||||||
|
|
||||||
|
__return_value__ = {}
|
||||||
|
""" 返回值
|
||||||
|
示例:
|
||||||
|
{
|
||||||
|
"example": [
|
||||||
|
'value1',
|
||||||
|
'value2',
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
'key1': 'value1',
|
||||||
|
},
|
||||||
|
['value1', 'value2']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_return(self, key: str, ret):
|
||||||
|
"""添加返回值"""
|
||||||
|
if key not in self.__return_value__:
|
||||||
|
self.__return_value__[key] = []
|
||||||
|
self.__return_value__[key].append(ret)
|
||||||
|
|
||||||
|
def get_return(self, key: str) -> list:
|
||||||
|
"""获取key的所有返回值"""
|
||||||
|
if key in self.__return_value__:
|
||||||
|
return self.__return_value__[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_return_value(self, key: str):
|
||||||
|
"""获取key的首个返回值"""
|
||||||
|
if key in self.__return_value__:
|
||||||
|
return self.__return_value__[key][0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def prevent_default(self):
|
||||||
|
"""阻止默认行为"""
|
||||||
|
self.__prevent_default__ = True
|
||||||
|
|
||||||
|
def prevent_postorder(self):
|
||||||
|
"""阻止后续插件执行"""
|
||||||
|
self.__prevent_postorder__ = True
|
||||||
|
|
||||||
|
def is_prevented_default(self):
|
||||||
|
"""是否阻止默认行为"""
|
||||||
|
return self.__prevent_default__
|
||||||
|
|
||||||
|
def is_prevented_postorder(self):
|
||||||
|
"""是否阻止后序插件执行"""
|
||||||
|
return self.__prevent_postorder__
|
||||||
|
|
||||||
|
def __init__(self, name: str):
|
||||||
|
self.name = name
|
||||||
|
self.eid = EventContext.eid
|
||||||
|
self.__prevent_default__ = False
|
||||||
|
self.__prevent_postorder__ = False
|
||||||
|
self.__return_value__ = {}
|
||||||
|
EventContext.eid += 1
|
||||||
|
|
||||||
|
|
||||||
|
def emit(event_name: str, **kwargs) -> EventContext:
|
||||||
|
"""触发事件"""
|
||||||
|
import pkg.utils.context as context
|
||||||
|
|
||||||
|
if context.get_plugin_host() is None:
|
||||||
|
return None
|
||||||
|
return context.get_plugin_host().emit(event_name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PluginHost:
|
||||||
|
"""插件宿主"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化插件宿主"""
|
||||||
|
context.set_plugin_host(self)
|
||||||
|
self.calling_gpt_session = Session([])
|
||||||
|
|
||||||
|
def get_runtime_context(self) -> context:
|
||||||
|
"""获取运行时上下文(pkg.utils.context模块的对象)
|
||||||
|
|
||||||
|
此上下文用于和主程序其他模块交互(数据库、QQ机器人、OpenAI接口等)
|
||||||
|
详见pkg.utils.context模块
|
||||||
|
其中的context变量保存了其他重要模块的类对象,可以使用这些对象进行交互
|
||||||
|
"""
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_bot(self) -> Mirai:
|
||||||
|
"""获取机器人对象"""
|
||||||
|
return context.get_qqbot_manager().bot
|
||||||
|
|
||||||
|
def get_bot_adapter(self) -> msadapter.MessageSourceAdapter:
|
||||||
|
"""获取消息源适配器"""
|
||||||
|
return context.get_qqbot_manager().adapter
|
||||||
|
|
||||||
|
def send_person_message(self, person, message):
|
||||||
|
"""发送私聊消息"""
|
||||||
|
self.get_bot_adapter().send_message("person", person, message)
|
||||||
|
|
||||||
|
def send_group_message(self, group, message):
|
||||||
|
"""发送群消息"""
|
||||||
|
self.get_bot_adapter().send_message("group", group, message)
|
||||||
|
|
||||||
|
def notify_admin(self, message):
|
||||||
|
"""通知管理员"""
|
||||||
|
context.get_qqbot_manager().notify_admin(message)
|
||||||
|
|
||||||
|
def emit(self, event_name: str, **kwargs) -> EventContext:
|
||||||
|
"""触发事件"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
event_context = EventContext(event_name)
|
||||||
|
logging.debug("触发事件: {} ({})".format(event_name, event_context.eid))
|
||||||
|
|
||||||
|
emitted_plugins = []
|
||||||
|
for plugin in iter_plugins():
|
||||||
|
if not plugin["enabled"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# if plugin['instance'] is None:
|
||||||
|
# # 从关闭状态切到开启状态之后,重新加载插件
|
||||||
|
# try:
|
||||||
|
# plugin['instance'] = plugin["class"](plugin_host=self)
|
||||||
|
# logging.info("插件 {} 已初始化".format(plugin['name']))
|
||||||
|
# except:
|
||||||
|
# logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||||
|
# continue
|
||||||
|
|
||||||
|
if "hooks" not in plugin or event_name not in plugin["hooks"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
emitted_plugins.append(plugin['name'])
|
||||||
|
|
||||||
|
hooks = []
|
||||||
|
if event_name in plugin["hooks"]:
|
||||||
|
hooks = plugin["hooks"][event_name]
|
||||||
|
for hook in hooks:
|
||||||
|
try:
|
||||||
|
already_prevented_default = event_context.is_prevented_default()
|
||||||
|
|
||||||
|
kwargs["host"] = context.get_plugin_host()
|
||||||
|
kwargs["event"] = event_context
|
||||||
|
|
||||||
|
hook(plugin["instance"], **kwargs)
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_context.is_prevented_default()
|
||||||
|
and not already_prevented_default
|
||||||
|
):
|
||||||
|
logging.debug(
|
||||||
|
"插件 {} 已要求阻止事件 {} 的默认行为".format(plugin["name"], event_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("插件{}响应事件{}时发生错误".format(plugin["name"], event_name))
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# print("done:{}".format(plugin['name']))
|
||||||
|
if event_context.is_prevented_postorder():
|
||||||
|
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"]))
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.debug(
|
||||||
|
"事件 {} ({}) 处理完毕,返回值: {}".format(
|
||||||
|
event_name, event_context.eid, event_context.__return_value__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(emitted_plugins) > 0:
|
||||||
|
plugins_info = [get_plugin_info_for_audit(p) for p in emitted_plugins]
|
||||||
|
|
||||||
|
context.get_center_v2_api().usage.post_event_record(
|
||||||
|
plugins=plugins_info,
|
||||||
|
event_name=event_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return event_context
|
||||||
87
pkg/plugin/metadata.py
Normal file
87
pkg/plugin/metadata.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import dulwich.errors as dulwich_err
|
||||||
|
|
||||||
|
from ..utils import updater
|
||||||
|
|
||||||
|
|
||||||
|
def read_metadata_file() -> dict:
|
||||||
|
# 读取 plugins/metadata.json 文件
|
||||||
|
if not os.path.exists('plugins/metadata.json'):
|
||||||
|
return {}
|
||||||
|
with open('plugins/metadata.json', 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def write_metadata_file(metadata: dict):
|
||||||
|
if not os.path.exists('plugins'):
|
||||||
|
os.mkdir('plugins')
|
||||||
|
|
||||||
|
with open('plugins/metadata.json', 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def do_plugin_git_repo_migrate():
|
||||||
|
# 仅在 plugins/metadata.json 不存在时执行
|
||||||
|
if os.path.exists('plugins/metadata.json'):
|
||||||
|
return
|
||||||
|
|
||||||
|
metadata = read_metadata_file()
|
||||||
|
|
||||||
|
# 遍历 plugins 下所有目录,获取目录的git远程地址
|
||||||
|
for plugin_name in os.listdir('plugins'):
|
||||||
|
plugin_path = os.path.join('plugins', plugin_name)
|
||||||
|
if not os.path.isdir(plugin_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
remote_url = None
|
||||||
|
try:
|
||||||
|
remote_url = updater.get_remote_url(plugin_path)
|
||||||
|
except dulwich_err.NotGitRepository:
|
||||||
|
continue
|
||||||
|
if remote_url == "https://github.com/RockChinQ/QChatGPT" or remote_url == "https://gitee.com/RockChin/QChatGPT" \
|
||||||
|
or remote_url == "" or remote_url is None or remote_url == "http://github.com/RockChinQ/QChatGPT" or remote_url == "http://gitee.com/RockChin/QChatGPT":
|
||||||
|
continue
|
||||||
|
|
||||||
|
from . import host
|
||||||
|
|
||||||
|
if plugin_name not in metadata:
|
||||||
|
metadata[plugin_name] = {
|
||||||
|
'source': remote_url,
|
||||||
|
'install_timestamp': int(time.time()),
|
||||||
|
'ref': 'HEAD',
|
||||||
|
}
|
||||||
|
|
||||||
|
write_metadata_file(metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def set_plugin_metadata(
|
||||||
|
plugin_name: str,
|
||||||
|
source: str,
|
||||||
|
install_timestamp: int,
|
||||||
|
ref: str,
|
||||||
|
):
|
||||||
|
metadata = read_metadata_file()
|
||||||
|
metadata[plugin_name] = {
|
||||||
|
'source': source,
|
||||||
|
'install_timestamp': install_timestamp,
|
||||||
|
'ref': ref,
|
||||||
|
}
|
||||||
|
write_metadata_file(metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_plugin_metadata(plugin_name: str):
|
||||||
|
metadata = read_metadata_file()
|
||||||
|
if plugin_name in metadata:
|
||||||
|
del metadata[plugin_name]
|
||||||
|
write_metadata_file(metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_metadata(plugin_name: str) -> dict:
|
||||||
|
metadata = read_metadata_file()
|
||||||
|
if plugin_name in metadata:
|
||||||
|
return metadata[plugin_name]
|
||||||
|
return {}
|
||||||
299
pkg/plugin/models.py
Normal file
299
pkg/plugin/models.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from ..plugin import host
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
PersonMessageReceived = "person_message_received"
|
||||||
|
"""收到私聊消息时,在判断是否应该响应前触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
message_chain: mirai.models.message.MessageChain 消息链
|
||||||
|
"""
|
||||||
|
|
||||||
|
GroupMessageReceived = "group_message_received"
|
||||||
|
"""收到群聊消息时,在判断是否应该响应前触发(所有群消息)
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
message_chain: mirai.models.message.MessageChain 消息链
|
||||||
|
"""
|
||||||
|
|
||||||
|
PersonNormalMessageReceived = "person_normal_message_received"
|
||||||
|
"""判断为应该处理的私聊普通消息时触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
text_message: str 消息文本
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
alter: str 修改后的消息文本
|
||||||
|
reply: list 回复消息组件列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
PersonCommandSent = "person_command_sent"
|
||||||
|
"""判断为应该处理的私聊命令时触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
command: str 命令
|
||||||
|
params: list[str] 参数列表
|
||||||
|
text_message: str 完整命令文本
|
||||||
|
is_admin: bool 是否为管理员
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
alter: str 修改后的完整命令文本
|
||||||
|
reply: list 回复消息组件列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
GroupNormalMessageReceived = "group_normal_message_received"
|
||||||
|
"""判断为应该处理的群聊普通消息时触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
text_message: str 消息文本
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
alter: str 修改后的消息文本
|
||||||
|
reply: list 回复消息组件列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
GroupCommandSent = "group_command_sent"
|
||||||
|
"""判断为应该处理的群聊命令时触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
command: str 命令
|
||||||
|
params: list[str] 参数列表
|
||||||
|
text_message: str 完整命令文本
|
||||||
|
is_admin: bool 是否为管理员
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
alter: str 修改后的完整命令文本
|
||||||
|
reply: list 回复消息组件列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
NormalMessageResponded = "normal_message_responded"
|
||||||
|
"""获取到对普通消息的文字响应时触发
|
||||||
|
kwargs:
|
||||||
|
launcher_type: str 发起对象类型(group/person)
|
||||||
|
launcher_id: int 发起对象ID(群号/QQ号)
|
||||||
|
sender_id: int 发送者ID(QQ号)
|
||||||
|
session: pkg.openai.session.Session 会话对象
|
||||||
|
prefix: str 回复文字消息的前缀
|
||||||
|
response_text: str 响应文本
|
||||||
|
finish_reason: str 响应结束原因
|
||||||
|
funcs_called: list[str] 此次响应中调用的函数列表
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
prefix: str 修改后的回复文字消息的前缀
|
||||||
|
reply: list 替换回复消息组件列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
SessionFirstMessageReceived = "session_first_message_received"
|
||||||
|
"""会话被第一次交互时触发
|
||||||
|
kwargs:
|
||||||
|
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||||
|
session: pkg.openai.session.Session 会话对象
|
||||||
|
default_prompt: str 预设值
|
||||||
|
"""
|
||||||
|
|
||||||
|
SessionExplicitReset = "session_reset"
|
||||||
|
"""会话被用户手动重置时触发,此事件不支持阻止默认行为
|
||||||
|
kwargs:
|
||||||
|
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||||
|
session: pkg.openai.session.Session 会话对象
|
||||||
|
"""
|
||||||
|
|
||||||
|
SessionExpired = "session_expired"
|
||||||
|
"""会话过期时触发
|
||||||
|
kwargs:
|
||||||
|
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||||
|
session: pkg.openai.session.Session 会话对象
|
||||||
|
session_expire_time: int 已设置的会话过期时间(秒)
|
||||||
|
"""
|
||||||
|
|
||||||
|
KeyExceeded = "key_exceeded"
|
||||||
|
"""api-key超额时触发
|
||||||
|
kwargs:
|
||||||
|
key_name: str 超额的api-key名称
|
||||||
|
usage: dict 超额的api-key使用情况
|
||||||
|
exceeded_keys: list[str] 超额的api-key列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
KeySwitched = "key_switched"
|
||||||
|
"""api-key超额切换成功时触发,此事件不支持阻止默认行为
|
||||||
|
kwargs:
|
||||||
|
key_name: str 切换成功的api-key名称
|
||||||
|
key_list: list[str] api-key列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
PromptPreProcessing = "prompt_pre_processing"
|
||||||
|
"""每回合调用接口前对prompt进行预处理时触发,此事件不支持阻止默认行为
|
||||||
|
kwargs:
|
||||||
|
session_name: str 会话名称(<launcher_type>_<launcher_id>)
|
||||||
|
default_prompt: list 此session使用的情景预设内容
|
||||||
|
prompt: list 此session现有的prompt内容
|
||||||
|
text_message: str 用户发送的消息文本
|
||||||
|
|
||||||
|
returns (optional):
|
||||||
|
default_prompt: list 修改后的情景预设内容
|
||||||
|
prompt: list 修改后的prompt内容
|
||||||
|
text_message: str 修改后的消息文本
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def on(*args, **kwargs):
|
||||||
|
"""注册事件监听器
|
||||||
|
"""
|
||||||
|
return Plugin.on(*args, **kwargs)
|
||||||
|
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
"""注册内容函数,声明此函数为一个内容函数,在对话中将发送此函数给GPT以供其调用
|
||||||
|
此函数可以具有任意的参数,但必须按照[此文档](https://github.com/RockChinQ/CallingGPT/wiki/1.-Function-Format#function-format)
|
||||||
|
所述的格式编写函数的docstring。
|
||||||
|
此功能仅支持在使用gpt-3.5或gpt-4系列模型时使用。
|
||||||
|
"""
|
||||||
|
return Plugin.func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__current_registering_plugin__ = ""
|
||||||
|
|
||||||
|
|
||||||
|
def require_ver(ge: str, le: str="v999.9.9") -> bool:
|
||||||
|
"""插件版本要求装饰器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ge (str): 最低版本要求
|
||||||
|
le (str, optional): 最高版本要求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否满足要求, False时为无法获取版本号,True时为满足要求,报错为不满足要求
|
||||||
|
"""
|
||||||
|
qchatgpt_version = ""
|
||||||
|
|
||||||
|
from pkg.utils.updater import get_current_tag, compare_version_str
|
||||||
|
|
||||||
|
try:
|
||||||
|
qchatgpt_version = get_current_tag() # 从updater模块获取版本号
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if compare_version_str(qchatgpt_version, ge) < 0 or \
|
||||||
|
(compare_version_str(qchatgpt_version, le) > 0):
|
||||||
|
raise Exception("QChatGPT 版本不满足要求,某些功能(可能是由插件提供的)无法正常使用。(要求版本:{}-{},但当前版本:{})".format(ge, le, qchatgpt_version))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin:
|
||||||
|
"""插件基类"""
|
||||||
|
|
||||||
|
host: host.PluginHost
|
||||||
|
"""插件宿主,提供插件的一些基础功能"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def on(cls, event):
|
||||||
|
"""事件处理器装饰器
|
||||||
|
|
||||||
|
:param
|
||||||
|
event: 事件类型
|
||||||
|
:return:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
global __current_registering_plugin__
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
plugin_hooks = host.__plugins__[__current_registering_plugin__]["hooks"]
|
||||||
|
|
||||||
|
if event not in plugin_hooks:
|
||||||
|
plugin_hooks[event] = []
|
||||||
|
plugin_hooks[event].append(func)
|
||||||
|
|
||||||
|
# print("registering hook: p='{}', e='{}', f={}".format(__current_registering_plugin__, event, func))
|
||||||
|
|
||||||
|
host.__plugins__[__current_registering_plugin__]["hooks"] = plugin_hooks
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def func(cls, name: str=None):
|
||||||
|
"""内容函数装饰器
|
||||||
|
"""
|
||||||
|
global __current_registering_plugin__
|
||||||
|
from CallingGPT.entities.namespace import get_func_schema
|
||||||
|
|
||||||
|
def wrapper(func):
|
||||||
|
|
||||||
|
function_schema = get_func_schema(func)
|
||||||
|
function_schema['name'] = __current_registering_plugin__ + '-' + (func.__name__ if name is None else name)
|
||||||
|
|
||||||
|
function_schema['enabled'] = True
|
||||||
|
|
||||||
|
host.__function_inst_map__[function_schema['name']] = function_schema['function']
|
||||||
|
|
||||||
|
del function_schema['function']
|
||||||
|
|
||||||
|
# logging.debug("registering content function: p='{}', f='{}', s={}".format(__current_registering_plugin__, func, function_schema))
|
||||||
|
|
||||||
|
host.__callable_functions__.append(
|
||||||
|
function_schema
|
||||||
|
)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def register(name: str, description: str, version: str, author: str):
|
||||||
|
"""注册插件, 此函数作为装饰器使用
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): 插件名称
|
||||||
|
description (str): 插件描述
|
||||||
|
version (str): 插件版本
|
||||||
|
author (str): 插件作者
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
global __current_registering_plugin__
|
||||||
|
|
||||||
|
__current_registering_plugin__ = name
|
||||||
|
# print("registering plugin: n='{}', d='{}', v={}, a='{}'".format(name, description, version, author))
|
||||||
|
host.__plugins__[name] = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"version": version,
|
||||||
|
"author": author,
|
||||||
|
"hooks": {},
|
||||||
|
"path": host.__current_module_path__,
|
||||||
|
"enabled": True,
|
||||||
|
"instance": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def wrapper(cls: Plugin):
|
||||||
|
cls.name = name
|
||||||
|
cls.description = description
|
||||||
|
cls.version = version
|
||||||
|
cls.author = author
|
||||||
|
cls.host = context.get_plugin_host()
|
||||||
|
cls.enabled = True
|
||||||
|
cls.path = host.__current_module_path__
|
||||||
|
|
||||||
|
# 存到插件列表
|
||||||
|
host.__plugins__[name]["class"] = cls
|
||||||
|
|
||||||
|
logging.info("插件注册完成: n='{}', d='{}', v={}, a='{}' ({})".format(name, description, version, author, cls))
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return wrapper
|
||||||
103
pkg/plugin/settings.py
Normal file
103
pkg/plugin/settings.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..plugin import host
|
||||||
|
|
||||||
|
def wrapper_dict_from_runtime_context() -> dict:
|
||||||
|
"""从变量中包装settings.json的数据字典"""
|
||||||
|
settings = {
|
||||||
|
"order": [],
|
||||||
|
"functions": {
|
||||||
|
"enabled": host.__enable_content_functions__
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for plugin_name in host.__plugins_order__:
|
||||||
|
settings["order"].append(plugin_name)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
|
||||||
|
def apply_settings(settings: dict):
|
||||||
|
"""将settings.json数据应用到变量中"""
|
||||||
|
if "order" in settings:
|
||||||
|
host.__plugins_order__ = settings["order"]
|
||||||
|
|
||||||
|
if "functions" in settings:
|
||||||
|
if "enabled" in settings["functions"]:
|
||||||
|
host.__enable_content_functions__ = settings["functions"]["enabled"]
|
||||||
|
# logging.debug("set content function enabled: {}".format(host.__enable_content_functions__))
|
||||||
|
|
||||||
|
|
||||||
|
def dump_settings():
|
||||||
|
"""保存settings.json数据"""
|
||||||
|
logging.debug("保存plugins/settings.json数据")
|
||||||
|
|
||||||
|
settings = wrapper_dict_from_runtime_context()
|
||||||
|
|
||||||
|
with open("plugins/settings.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(settings, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings():
|
||||||
|
"""加载settings.json数据"""
|
||||||
|
logging.debug("加载plugins/settings.json数据")
|
||||||
|
|
||||||
|
# 读取plugins/settings.json
|
||||||
|
settings = {
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not os.path.exists("plugins/settings.json"):
|
||||||
|
# 不存在则创建
|
||||||
|
with open("plugins/settings.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(wrapper_dict_from_runtime_context(), f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
with open("plugins/settings.json", "r", encoding="utf-8") as f:
|
||||||
|
settings = json.load(f)
|
||||||
|
|
||||||
|
if settings is None:
|
||||||
|
settings = {
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查每个设置项
|
||||||
|
if "order" not in settings:
|
||||||
|
settings["order"] = []
|
||||||
|
|
||||||
|
settings_modified = False
|
||||||
|
|
||||||
|
settings_copy = settings.copy()
|
||||||
|
|
||||||
|
# 检查settings中多余的插件项
|
||||||
|
|
||||||
|
# order
|
||||||
|
for plugin_name in settings_copy["order"]:
|
||||||
|
if plugin_name not in host.__plugins_order__:
|
||||||
|
settings["order"].remove(plugin_name)
|
||||||
|
settings_modified = True
|
||||||
|
|
||||||
|
# 检查settings中缺少的插件项
|
||||||
|
|
||||||
|
# order
|
||||||
|
for plugin_name in host.__plugins_order__:
|
||||||
|
if plugin_name not in settings_copy["order"]:
|
||||||
|
settings["order"].append(plugin_name)
|
||||||
|
settings_modified = True
|
||||||
|
|
||||||
|
if "functions" not in settings:
|
||||||
|
settings["functions"] = {
|
||||||
|
"enabled": host.__enable_content_functions__
|
||||||
|
}
|
||||||
|
settings_modified = True
|
||||||
|
elif "enabled" not in settings["functions"]:
|
||||||
|
settings["functions"]["enabled"] = host.__enable_content_functions__
|
||||||
|
settings_modified = True
|
||||||
|
|
||||||
|
logging.info("已全局{}内容函数。".format("启用" if settings["functions"]["enabled"] else "禁用"))
|
||||||
|
|
||||||
|
apply_settings(settings)
|
||||||
|
|
||||||
|
if settings_modified:
|
||||||
|
dump_settings()
|
||||||
94
pkg/plugin/switch.py
Normal file
94
pkg/plugin/switch.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 控制插件的开关
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..plugin import host
|
||||||
|
|
||||||
|
|
||||||
|
def wrapper_dict_from_plugin_list() -> dict:
|
||||||
|
"""将插件列表转换为开关json"""
|
||||||
|
switch = {}
|
||||||
|
|
||||||
|
for plugin_name in host.__plugins__:
|
||||||
|
plugin = host.__plugins__[plugin_name]
|
||||||
|
|
||||||
|
switch[plugin_name] = {
|
||||||
|
"path": plugin["path"],
|
||||||
|
"enabled": plugin["enabled"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch
|
||||||
|
|
||||||
|
|
||||||
|
def apply_switch(switch: dict):
|
||||||
|
"""将开关数据应用到插件列表中"""
|
||||||
|
# print("将开关数据应用到插件列表中")
|
||||||
|
# print(switch)
|
||||||
|
for plugin_name in switch:
|
||||||
|
host.__plugins__[plugin_name]["enabled"] = switch[plugin_name]["enabled"]
|
||||||
|
|
||||||
|
# 查找此插件的所有内容函数
|
||||||
|
for func in host.__callable_functions__:
|
||||||
|
if func['name'].startswith(plugin_name + '-'):
|
||||||
|
func['enabled'] = switch[plugin_name]["enabled"]
|
||||||
|
|
||||||
|
|
||||||
|
def dump_switch():
|
||||||
|
"""保存开关数据"""
|
||||||
|
logging.debug("保存开关数据")
|
||||||
|
# 将开关数据写入plugins/switch.json
|
||||||
|
|
||||||
|
switch = wrapper_dict_from_plugin_list()
|
||||||
|
|
||||||
|
with open("plugins/switch.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(switch, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def load_switch():
|
||||||
|
"""加载开关数据"""
|
||||||
|
logging.debug("加载开关数据")
|
||||||
|
# 读取plugins/switch.json
|
||||||
|
|
||||||
|
switch = {}
|
||||||
|
|
||||||
|
# 检查文件是否存在
|
||||||
|
if not os.path.exists("plugins/switch.json"):
|
||||||
|
# 不存在则创建
|
||||||
|
with open("plugins/switch.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(switch, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
with open("plugins/switch.json", "r", encoding="utf-8") as f:
|
||||||
|
switch = json.load(f)
|
||||||
|
|
||||||
|
if switch is None:
|
||||||
|
switch = {}
|
||||||
|
|
||||||
|
switch_modified = False
|
||||||
|
|
||||||
|
switch_copy = switch.copy()
|
||||||
|
# 检查switch中多余的和path不相符的
|
||||||
|
for plugin_name in switch_copy:
|
||||||
|
if plugin_name not in host.__plugins__:
|
||||||
|
del switch[plugin_name]
|
||||||
|
switch_modified = True
|
||||||
|
elif switch[plugin_name]["path"] != host.__plugins__[plugin_name]["path"]:
|
||||||
|
# 删除此不相符的
|
||||||
|
del switch[plugin_name]
|
||||||
|
switch_modified = True
|
||||||
|
|
||||||
|
# 检查plugin中多余的
|
||||||
|
for plugin_name in host.__plugins__:
|
||||||
|
if plugin_name not in switch:
|
||||||
|
switch[plugin_name] = {
|
||||||
|
"path": host.__plugins__[plugin_name]["path"],
|
||||||
|
"enabled": host.__plugins__[plugin_name]["enabled"],
|
||||||
|
}
|
||||||
|
switch_modified = True
|
||||||
|
|
||||||
|
# 应用开关数据
|
||||||
|
apply_switch(switch)
|
||||||
|
|
||||||
|
# 如果switch有修改,保存
|
||||||
|
if switch_modified:
|
||||||
|
dump_switch()
|
||||||
137
pkg/qqbot/adapter.py
Normal file
137
pkg/qqbot/adapter.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# MessageSource的适配器
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import mirai
|
||||||
|
|
||||||
|
|
||||||
|
class MessageSourceAdapter:
|
||||||
|
bot_account_id: int
|
||||||
|
def __init__(self, config: dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_message(
|
||||||
|
self,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
message: mirai.MessageChain
|
||||||
|
):
|
||||||
|
"""发送消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_type (str): 目标类型,`person`或`group`
|
||||||
|
target_id (str): 目标ID
|
||||||
|
message (mirai.MessageChain): YiriMirai库的消息链
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def reply_message(
|
||||||
|
self,
|
||||||
|
message_source: mirai.MessageEvent,
|
||||||
|
message: mirai.MessageChain,
|
||||||
|
quote_origin: bool = False
|
||||||
|
):
|
||||||
|
"""回复消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_source (mirai.MessageEvent): YiriMirai消息源事件
|
||||||
|
message (mirai.MessageChain): YiriMirai库的消息链
|
||||||
|
quote_origin (bool, optional): 是否引用原消息. Defaults to False.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def is_muted(self, group_id: int) -> bool:
|
||||||
|
"""获取账号是否在指定群被禁言"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def register_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
"""注册事件监听器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
|
||||||
|
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def unregister_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
"""注销事件监听器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
|
||||||
|
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def run_sync(self):
|
||||||
|
"""以阻塞的方式运行适配器"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def kill(self) -> bool:
|
||||||
|
"""关闭适配器
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MessageConverter:
|
||||||
|
"""消息链转换器基类"""
|
||||||
|
@staticmethod
|
||||||
|
def yiri2target(message_chain: mirai.MessageChain):
|
||||||
|
"""将YiriMirai消息链转换为目标消息链
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_chain (mirai.MessageChain): YiriMirai消息链
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
typing.Any: 目标消息链
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target2yiri(message_chain: typing.Any) -> mirai.MessageChain:
|
||||||
|
"""将目标消息链转换为YiriMirai消息链
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_chain (typing.Any): 目标消息链
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
mirai.MessageChain: YiriMirai消息链
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class EventConverter:
|
||||||
|
"""事件转换器基类"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def yiri2target(event: typing.Type[mirai.Event]):
|
||||||
|
"""将YiriMirai事件转换为目标事件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (typing.Type[mirai.Event]): YiriMirai事件
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
typing.Any: 目标事件
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target2yiri(event: typing.Any) -> mirai.Event:
|
||||||
|
"""将目标事件的调用参数转换为YiriMirai的事件参数对象
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (typing.Any): 目标事件
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
typing.Type[mirai.Event]: YiriMirai事件
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
50
pkg/qqbot/banlist.py
Normal file
50
pkg/qqbot/banlist.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
|
def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool:
|
||||||
|
if not context.get_qqbot_manager().enable_banlist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if launcher_type == 'group':
|
||||||
|
# 检查是否显式声明发起人QQ要被person忽略
|
||||||
|
if sender_id in context.get_qqbot_manager().ban_person:
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
for group_rule in context.get_qqbot_manager().ban_group:
|
||||||
|
if type(group_rule) == int:
|
||||||
|
if group_rule == launcher_id: # 此群群号被禁用
|
||||||
|
result = True
|
||||||
|
elif type(group_rule) == str:
|
||||||
|
if group_rule.startswith('!'):
|
||||||
|
# 截取!后面的字符串作为表达式,判断是否匹配
|
||||||
|
reg_str = group_rule[1:]
|
||||||
|
import re
|
||||||
|
if re.match(reg_str, str(launcher_id)): # 被豁免,最高级别
|
||||||
|
result = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 判断是否匹配regexp
|
||||||
|
import re
|
||||||
|
if re.match(group_rule, str(launcher_id)): # 此群群号被禁用
|
||||||
|
result = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ban_person, 与群规则相同
|
||||||
|
for person_rule in context.get_qqbot_manager().ban_person:
|
||||||
|
if type(person_rule) == int:
|
||||||
|
if person_rule == launcher_id:
|
||||||
|
result = True
|
||||||
|
elif type(person_rule) == str:
|
||||||
|
if person_rule.startswith('!'):
|
||||||
|
reg_str = person_rule[1:]
|
||||||
|
import re
|
||||||
|
if re.match(reg_str, str(launcher_id)):
|
||||||
|
result = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
import re
|
||||||
|
if re.match(person_rule, str(launcher_id)):
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
100
pkg/qqbot/blob.py
Normal file
100
pkg/qqbot/blob.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# 长消息处理相关
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from mirai.models.message import MessageComponent, MessageChain, Image
|
||||||
|
from mirai.models.message import ForwardMessageNode
|
||||||
|
from mirai.models.base import MiraiBaseModel
|
||||||
|
|
||||||
|
from ..utils import text2img
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardMessageDiaplay(MiraiBaseModel):
|
||||||
|
title: str = "群聊的聊天记录"
|
||||||
|
brief: str = "[聊天记录]"
|
||||||
|
source: str = "聊天记录"
|
||||||
|
preview: typing.List[str] = []
|
||||||
|
summary: str = "查看x条转发消息"
|
||||||
|
|
||||||
|
|
||||||
|
class Forward(MessageComponent):
|
||||||
|
"""合并转发。"""
|
||||||
|
type: str = "Forward"
|
||||||
|
"""消息组件类型。"""
|
||||||
|
display: ForwardMessageDiaplay
|
||||||
|
"""显示信息"""
|
||||||
|
node_list: typing.List[ForwardMessageNode]
|
||||||
|
"""转发消息节点列表。"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if len(args) == 1:
|
||||||
|
self.node_list = args[0]
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '[聊天记录]'
|
||||||
|
|
||||||
|
|
||||||
|
def text_to_image(text: str) -> MessageComponent:
|
||||||
|
"""将文本转换成图片"""
|
||||||
|
# 检查temp文件夹是否存在
|
||||||
|
if not os.path.exists('temp'):
|
||||||
|
os.mkdir('temp')
|
||||||
|
img_path = text2img.text_to_image(text_str=text, save_as='temp/{}.png'.format(int(time.time())))
|
||||||
|
|
||||||
|
compressed_path, size = text2img.compress_image(img_path, outfile="temp/{}_compressed.png".format(int(time.time())))
|
||||||
|
# 读取图片,转换成base64
|
||||||
|
with open(compressed_path, 'rb') as f:
|
||||||
|
img = f.read()
|
||||||
|
|
||||||
|
b64 = base64.b64encode(img)
|
||||||
|
|
||||||
|
# 删除图片
|
||||||
|
os.remove(img_path)
|
||||||
|
|
||||||
|
# 判断compressed_path是否存在
|
||||||
|
if os.path.exists(compressed_path):
|
||||||
|
os.remove(compressed_path)
|
||||||
|
# 返回图片
|
||||||
|
return Image(base64=b64.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def check_text(text: str) -> list:
|
||||||
|
"""检查文本是否为长消息,并转换成该使用的消息链组件"""
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if len(text) > config['blob_message_threshold']:
|
||||||
|
|
||||||
|
# logging.info("长消息: {}".format(text))
|
||||||
|
if config['blob_message_strategy'] == 'image':
|
||||||
|
# 转换成图片
|
||||||
|
return [text_to_image(text)]
|
||||||
|
elif config['blob_message_strategy'] == 'forward':
|
||||||
|
|
||||||
|
# 包装转发消息
|
||||||
|
display = ForwardMessageDiaplay(
|
||||||
|
title='群聊的聊天记录',
|
||||||
|
brief='[聊天记录]',
|
||||||
|
source='聊天记录',
|
||||||
|
preview=["bot: "+text],
|
||||||
|
summary="查看1条转发消息"
|
||||||
|
)
|
||||||
|
|
||||||
|
node = ForwardMessageNode(
|
||||||
|
sender_id=config['mirai_http_api_config']['qq'],
|
||||||
|
sender_name='bot',
|
||||||
|
message_chain=MessageChain([text])
|
||||||
|
)
|
||||||
|
|
||||||
|
forward = Forward(
|
||||||
|
display=display,
|
||||||
|
node_list=[node]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [forward]
|
||||||
|
else:
|
||||||
|
return [text]
|
||||||
0
pkg/qqbot/cmds/__init__.py
Normal file
0
pkg/qqbot/cmds/__init__.py
Normal file
333
pkg/qqbot/cmds/aamgr.py
Normal file
333
pkg/qqbot/cmds/aamgr.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import logging
|
||||||
|
import copy
|
||||||
|
import pkgutil
|
||||||
|
import traceback
|
||||||
|
import json
|
||||||
|
|
||||||
|
import tips as tips_custom
|
||||||
|
|
||||||
|
|
||||||
|
__command_list__ = {}
|
||||||
|
"""命令树
|
||||||
|
|
||||||
|
结构:
|
||||||
|
{
|
||||||
|
'cmd1': {
|
||||||
|
'description': 'cmd1 description',
|
||||||
|
'usage': 'cmd1 usage',
|
||||||
|
'aliases': ['cmd1 alias1', 'cmd1 alias2'],
|
||||||
|
'privilege': 0,
|
||||||
|
'parent': None,
|
||||||
|
'cls': <class 'pkg.qqbot.cmds.cmd1.CommandCmd1'>,
|
||||||
|
'sub': [
|
||||||
|
'cmd1-1'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'cmd1.cmd1-1: {
|
||||||
|
'description': 'cmd1-1 description',
|
||||||
|
'usage': 'cmd1-1 usage',
|
||||||
|
'aliases': ['cmd1-1 alias1', 'cmd1-1 alias2'],
|
||||||
|
'privilege': 0,
|
||||||
|
'parent': 'cmd1',
|
||||||
|
'cls': <class 'pkg.qqbot.cmds.cmd1.CommandCmd1_1'>,
|
||||||
|
'sub': []
|
||||||
|
},
|
||||||
|
'cmd2': {
|
||||||
|
'description': 'cmd2 description',
|
||||||
|
'usage': 'cmd2 usage',
|
||||||
|
'aliases': ['cmd2 alias1', 'cmd2 alias2'],
|
||||||
|
'privilege': 0,
|
||||||
|
'parent': None,
|
||||||
|
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2'>,
|
||||||
|
'sub': [
|
||||||
|
'cmd2-1'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'cmd2.cmd2-1': {
|
||||||
|
'description': 'cmd2-1 description',
|
||||||
|
'usage': 'cmd2-1 usage',
|
||||||
|
'aliases': ['cmd2-1 alias1', 'cmd2-1 alias2'],
|
||||||
|
'privilege': 0,
|
||||||
|
'parent': 'cmd2',
|
||||||
|
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2_1'>,
|
||||||
|
'sub': [
|
||||||
|
'cmd2-1-1'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'cmd2.cmd2-1.cmd2-1-1': {
|
||||||
|
'description': 'cmd2-1-1 description',
|
||||||
|
'usage': 'cmd2-1-1 usage',
|
||||||
|
'aliases': ['cmd2-1-1 alias1', 'cmd2-1-1 alias2'],
|
||||||
|
'privilege': 0,
|
||||||
|
'parent': 'cmd2.cmd2-1',
|
||||||
|
'cls': <class 'pkg.qqbot.cmds.cmd2.CommandCmd2_1_1'>,
|
||||||
|
'sub': []
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tree_index__: dict[str, list] = {}
|
||||||
|
"""命令树索引
|
||||||
|
|
||||||
|
结构:
|
||||||
|
{
|
||||||
|
'pkg.qqbot.cmds.cmd1.CommandCmd1': 'cmd1', # 顶级命令
|
||||||
|
'pkg.qqbot.cmds.cmd1.CommandCmd1_1': 'cmd1.cmd1-1', # 类名: 节点路径
|
||||||
|
'pkg.qqbot.cmds.cmd2.CommandCmd2': 'cmd2',
|
||||||
|
'pkg.qqbot.cmds.cmd2.CommandCmd2_1': 'cmd2.cmd2-1',
|
||||||
|
'pkg.qqbot.cmds.cmd2.CommandCmd2_1_1': 'cmd2.cmd2-1.cmd2-1-1',
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
"""命令执行上下文"""
|
||||||
|
command: str
|
||||||
|
"""顶级命令文本"""
|
||||||
|
|
||||||
|
crt_command: str
|
||||||
|
"""当前子命令文本"""
|
||||||
|
|
||||||
|
params: list
|
||||||
|
"""完整参数列表"""
|
||||||
|
|
||||||
|
crt_params: list
|
||||||
|
"""当前子命令参数列表"""
|
||||||
|
|
||||||
|
session_name: str
|
||||||
|
"""会话名"""
|
||||||
|
|
||||||
|
text_message: str
|
||||||
|
"""命令完整文本"""
|
||||||
|
|
||||||
|
launcher_type: str
|
||||||
|
"""命令发起者类型"""
|
||||||
|
|
||||||
|
launcher_id: int
|
||||||
|
"""命令发起者ID"""
|
||||||
|
|
||||||
|
sender_id: int
|
||||||
|
"""命令发送者ID"""
|
||||||
|
|
||||||
|
is_admin: bool
|
||||||
|
"""[过时]命令发送者是否为管理员"""
|
||||||
|
|
||||||
|
privilege: int
|
||||||
|
"""命令发送者权限等级"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractCommandNode:
|
||||||
|
"""命令抽象类"""
|
||||||
|
|
||||||
|
parent: type
|
||||||
|
"""父命令类"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""命令名"""
|
||||||
|
|
||||||
|
description: str
|
||||||
|
"""命令描述"""
|
||||||
|
|
||||||
|
usage: str
|
||||||
|
"""命令用法"""
|
||||||
|
|
||||||
|
aliases: list[str]
|
||||||
|
"""命令别名"""
|
||||||
|
|
||||||
|
privilege: int
|
||||||
|
"""命令权限等级, 权限大于等于此值的用户才能执行命令"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: Context) -> tuple[bool, list]:
|
||||||
|
"""命令处理函数
|
||||||
|
|
||||||
|
:param ctx: 命令执行上下文
|
||||||
|
|
||||||
|
:return: (是否执行, 回复列表(若执行))
|
||||||
|
|
||||||
|
若未执行,将自动以下一个参数查找并执行子命令
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def help(cls) -> str:
|
||||||
|
"""获取命令帮助信息"""
|
||||||
|
return '命令: {}\n描述: {}\n用法: \n{}\n别名: {}\n权限: {}'.format(
|
||||||
|
cls.name,
|
||||||
|
cls.description,
|
||||||
|
cls.usage,
|
||||||
|
', '.join(cls.aliases),
|
||||||
|
cls.privilege
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def register(
|
||||||
|
parent: type = None,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
usage: str = None,
|
||||||
|
aliases: list[str] = None,
|
||||||
|
privilege: int = 0
|
||||||
|
):
|
||||||
|
"""注册命令
|
||||||
|
|
||||||
|
:param cls: 命令类
|
||||||
|
:param name: 命令名
|
||||||
|
:param parent: 父命令类
|
||||||
|
"""
|
||||||
|
global __command_list__, __tree_index__
|
||||||
|
|
||||||
|
def wrapper(cls):
|
||||||
|
cls.name = name
|
||||||
|
cls.parent = parent
|
||||||
|
cls.description = description
|
||||||
|
cls.usage = usage
|
||||||
|
cls.aliases = aliases
|
||||||
|
cls.privilege = privilege
|
||||||
|
|
||||||
|
logging.debug("cls: {}, name: {}, parent: {}".format(cls, name, parent))
|
||||||
|
|
||||||
|
if parent is None:
|
||||||
|
# 顶级命令注册
|
||||||
|
__command_list__[name] = {
|
||||||
|
'description': cls.description,
|
||||||
|
'usage': cls.usage,
|
||||||
|
'aliases': cls.aliases,
|
||||||
|
'privilege': cls.privilege,
|
||||||
|
'parent': None,
|
||||||
|
'cls': cls,
|
||||||
|
'sub': []
|
||||||
|
}
|
||||||
|
# 更新索引
|
||||||
|
__tree_index__[cls.__module__ + '.' + cls.__name__] = name
|
||||||
|
else:
|
||||||
|
# 获取父节点名称
|
||||||
|
path = __tree_index__[parent.__module__ + '.' + parent.__name__]
|
||||||
|
|
||||||
|
parent_node = __command_list__[path]
|
||||||
|
# 链接父子命令
|
||||||
|
__command_list__[path]['sub'].append(name)
|
||||||
|
# 注册子命令
|
||||||
|
__command_list__[path + '.' + name] = {
|
||||||
|
'description': cls.description,
|
||||||
|
'usage': cls.usage,
|
||||||
|
'aliases': cls.aliases,
|
||||||
|
'privilege': cls.privilege,
|
||||||
|
'parent': path,
|
||||||
|
'cls': cls,
|
||||||
|
'sub': []
|
||||||
|
}
|
||||||
|
# 更新索引
|
||||||
|
__tree_index__[cls.__module__ + '.' + cls.__name__] = path + '.' + name
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class CommandPrivilegeError(Exception):
|
||||||
|
"""命令权限不足或不存在异常"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# 传入Context对象,广搜命令树,返回执行结果
|
||||||
|
# 若命令被处理,返回reply列表
|
||||||
|
# 若命令未被处理,继续执行下一级命令
|
||||||
|
# 若命令不存在,报异常
|
||||||
|
def execute(context: Context) -> list:
|
||||||
|
"""执行命令
|
||||||
|
|
||||||
|
:param ctx: 命令执行上下文
|
||||||
|
|
||||||
|
:return: 回复列表
|
||||||
|
"""
|
||||||
|
global __command_list__
|
||||||
|
|
||||||
|
# 拷贝ctx
|
||||||
|
ctx: Context = copy.deepcopy(context)
|
||||||
|
|
||||||
|
# 从树取出顶级命令
|
||||||
|
node = __command_list__
|
||||||
|
|
||||||
|
path = ctx.command
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
node = __command_list__[path]
|
||||||
|
logging.debug('执行命令: {}'.format(path))
|
||||||
|
|
||||||
|
# 检查权限
|
||||||
|
if ctx.privilege < node['privilege']:
|
||||||
|
raise CommandPrivilegeError(tips_custom.command_admin_message+"{}".format(path))
|
||||||
|
|
||||||
|
# 执行
|
||||||
|
execed, reply = node['cls'].process(ctx)
|
||||||
|
if execed:
|
||||||
|
return reply
|
||||||
|
else:
|
||||||
|
# 删除crt_params第一个参数
|
||||||
|
ctx.crt_command = ctx.crt_params.pop(0)
|
||||||
|
# 下一个path
|
||||||
|
path = path + '.' + ctx.crt_command
|
||||||
|
except KeyError:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise CommandPrivilegeError(tips_custom.command_err_message+"{}".format(path))
|
||||||
|
|
||||||
|
|
||||||
|
def register_all():
|
||||||
|
"""启动时调用此函数注册所有命令
|
||||||
|
|
||||||
|
递归处理pkg.qqbot.cmds包下及其子包下所有模块的所有继承于AbstractCommand的类
|
||||||
|
"""
|
||||||
|
# 模块:遍历其中的继承于AbstractCommand的类,进行注册
|
||||||
|
# 包:递归处理包下的模块
|
||||||
|
# 排除__开头的属性
|
||||||
|
global __command_list__, __tree_index__
|
||||||
|
|
||||||
|
import pkg.qqbot.cmds
|
||||||
|
|
||||||
|
def walk(module, prefix, path_prefix):
|
||||||
|
# 排除不处于pkg.qqbot.cmds中的包
|
||||||
|
if not module.__name__.startswith('pkg.qqbot.cmds'):
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.debug('walk: {}, path: {}'.format(module.__name__, module.__path__))
|
||||||
|
for item in pkgutil.iter_modules(module.__path__):
|
||||||
|
if item.name.startswith('__'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.ispkg:
|
||||||
|
walk(__import__(module.__name__ + '.' + item.name, fromlist=['']), prefix + item.name + '.', path_prefix + item.name + '/')
|
||||||
|
else:
|
||||||
|
m = __import__(module.__name__ + '.' + item.name, fromlist=[''])
|
||||||
|
# for name, cls in inspect.getmembers(m, inspect.isclass):
|
||||||
|
# # 检查是否为命令类
|
||||||
|
# if cls.__module__ == m.__name__ and issubclass(cls, AbstractCommandNode) and cls != AbstractCommandNode:
|
||||||
|
# cls.register(cls, cls.name, cls.parent)
|
||||||
|
|
||||||
|
walk(pkg.qqbot.cmds, '', '')
|
||||||
|
logging.debug(__command_list__)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_privileges():
|
||||||
|
"""读取cmdpriv.json并应用命令权限"""
|
||||||
|
# 读取内容
|
||||||
|
json_str = ""
|
||||||
|
with open('cmdpriv.json', 'r', encoding="utf-8") as f:
|
||||||
|
json_str = f.read()
|
||||||
|
|
||||||
|
data = json.loads(json_str)
|
||||||
|
for path, priv in data.items():
|
||||||
|
if path == 'comment':
|
||||||
|
continue
|
||||||
|
|
||||||
|
if path not in __command_list__:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if __command_list__[path]['privilege'] != priv:
|
||||||
|
logging.debug('应用权限: {} -> {}(default: {})'.format(path, priv, __command_list__[path]['privilege']))
|
||||||
|
|
||||||
|
__command_list__[path]['privilege'] = priv
|
||||||
0
pkg/qqbot/cmds/funcs/__init__.py
Normal file
0
pkg/qqbot/cmds/funcs/__init__.py
Normal file
37
pkg/qqbot/cmds/funcs/draw.py
Normal file
37
pkg/qqbot/cmds/funcs/draw.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import mirai
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
from ....utils import context
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="draw",
|
||||||
|
description="使用DALL·E生成图片",
|
||||||
|
usage="!draw <图片提示语>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class DrawCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
if len(ctx.params) == 0:
|
||||||
|
reply = ["[bot]err: 未提供图片描述文字"]
|
||||||
|
else:
|
||||||
|
session = pkg.openai.session.get_session(ctx.session_name)
|
||||||
|
|
||||||
|
res = session.draw_image(" ".join(ctx.params))
|
||||||
|
|
||||||
|
logging.debug("draw_image result:{}".format(res))
|
||||||
|
reply = [mirai.Image(url=res.data[0].url)]
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if config['include_image_description']:
|
||||||
|
reply.append(" ".join(ctx.params))
|
||||||
|
|
||||||
|
return True, reply
|
||||||
32
pkg/qqbot/cmds/funcs/func.py
Normal file
32
pkg/qqbot/cmds/funcs/func.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="func",
|
||||||
|
description="管理内容函数",
|
||||||
|
usage="!func",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class FuncCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
from pkg.plugin.models import host
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
reply_str = "当前已加载的内容函数:\n\n"
|
||||||
|
|
||||||
|
logging.debug("host.__callable_functions__: {}".format(json.dumps(host.__callable_functions__, indent=4)))
|
||||||
|
|
||||||
|
index = 1
|
||||||
|
for func in host.__callable_functions__:
|
||||||
|
reply_str += "{}. {}{}:\n{}\n\n".format(index, ("(已禁用) " if not func['enabled'] else ""), func['name'], func['description'])
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
0
pkg/qqbot/cmds/plugin/__init__.py
Normal file
0
pkg/qqbot/cmds/plugin/__init__.py
Normal file
198
pkg/qqbot/cmds/plugin/plugin.py
Normal file
198
pkg/qqbot/cmds/plugin/plugin.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
from ....plugin import host as plugin_host
|
||||||
|
from ....utils import updater
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="plugin",
|
||||||
|
description="插件管理",
|
||||||
|
usage="!plugin\n!plugin get <插件仓库地址>\n!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class PluginCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
reply = []
|
||||||
|
plugin_list = plugin_host.__plugins__
|
||||||
|
if len(ctx.params) == 0:
|
||||||
|
# 列出所有插件
|
||||||
|
|
||||||
|
reply_str = "[bot]所有插件({}):\n".format(len(plugin_host.__plugins__))
|
||||||
|
idx = 0
|
||||||
|
for key in plugin_host.iter_plugins_name():
|
||||||
|
plugin = plugin_list[key]
|
||||||
|
reply_str += "\n#{} {} {}\n{}\nv{}\n作者: {}\n"\
|
||||||
|
.format((idx+1), plugin['name'],
|
||||||
|
"[已禁用]" if not plugin['enabled'] else "",
|
||||||
|
plugin['description'],
|
||||||
|
plugin['version'], plugin['author'])
|
||||||
|
|
||||||
|
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
|
||||||
|
remote_url = updater.get_remote_url("/".join(plugin['path'].split('/')[:-1]))
|
||||||
|
if remote_url != "https://github.com/RockChinQ/QChatGPT" and remote_url != "https://gitee.com/RockChin/QChatGPT":
|
||||||
|
reply_str += "源码: "+remote_url+"\n"
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
return True, reply
|
||||||
|
else:
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=PluginCommand,
|
||||||
|
name="get",
|
||||||
|
description="安装插件",
|
||||||
|
usage="!plugin get <插件仓库地址>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class PluginGetCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
|
if len(ctx.crt_params) == 0:
|
||||||
|
reply = ["[bot]err: 请提供插件仓库地址"]
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
def closure():
|
||||||
|
try:
|
||||||
|
plugin_host.install_plugin(ctx.crt_params[0])
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装成功,请发送 !reload 命令重载插件")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("插件安装失败:{}".format(e))
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("插件安装失败:{}".format(e))
|
||||||
|
|
||||||
|
threading.Thread(target=closure, args=()).start()
|
||||||
|
reply = ["[bot]正在安装插件..."]
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=PluginCommand,
|
||||||
|
name="update",
|
||||||
|
description="更新指定插件或全部插件",
|
||||||
|
usage="!plugin update",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class PluginUpdateCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
plugin_list = plugin_host.__plugins__
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
if len(ctx.crt_params) > 0:
|
||||||
|
def closure():
|
||||||
|
try:
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
|
updated = []
|
||||||
|
|
||||||
|
if ctx.crt_params[0] == 'all':
|
||||||
|
for key in plugin_list:
|
||||||
|
plugin_host.update_plugin(key)
|
||||||
|
updated.append(key)
|
||||||
|
else:
|
||||||
|
plugin_path_name = plugin_host.get_plugin_path_name_by_plugin_name(ctx.crt_params[0])
|
||||||
|
|
||||||
|
if plugin_path_name is not None:
|
||||||
|
plugin_host.update_plugin(ctx.crt_params[0])
|
||||||
|
updated.append(ctx.crt_params[0])
|
||||||
|
else:
|
||||||
|
raise Exception("未找到插件: {}".format(ctx.crt_params[0]))
|
||||||
|
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}, 请发送 !reload 重载插件".format(", ".join(updated)))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("插件更新失败:{}".format(e))
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("插件更新失败:{} 请使用 !plugin 命令确认插件名称或尝试手动更新插件".format(e))
|
||||||
|
|
||||||
|
reply = ["[bot]正在更新插件,请勿重复发起..."]
|
||||||
|
threading.Thread(target=closure).start()
|
||||||
|
else:
|
||||||
|
reply = ["[bot]请指定要更新的插件, 或使用 !plugin update all 更新所有插件"]
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=PluginCommand,
|
||||||
|
name="del",
|
||||||
|
description="删除插件",
|
||||||
|
usage="!plugin del <插件名>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class PluginDelCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
plugin_list = plugin_host.__plugins__
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
if len(ctx.crt_params) < 1:
|
||||||
|
reply = ["[bot]err: 未指定插件名"]
|
||||||
|
else:
|
||||||
|
plugin_name = ctx.crt_params[0]
|
||||||
|
if plugin_name in plugin_list:
|
||||||
|
unin_path = plugin_host.uninstall_plugin(plugin_name)
|
||||||
|
reply = ["[bot]已删除插件: {} ({}), 请发送 !reload 重载插件".format(plugin_name, unin_path)]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]err:未找到插件: {}, 请使用!plugin命令查看插件列表".format(plugin_name)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=PluginCommand,
|
||||||
|
name="on",
|
||||||
|
description="启用指定插件",
|
||||||
|
usage="!plugin on <插件名>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=PluginCommand,
|
||||||
|
name="off",
|
||||||
|
description="禁用指定插件",
|
||||||
|
usage="!plugin off <插件名>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class PluginOnOffCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.plugin.switch as plugin_switch
|
||||||
|
|
||||||
|
plugin_list = plugin_host.__plugins__
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
print(ctx.params)
|
||||||
|
new_status = ctx.params[0] == 'on'
|
||||||
|
|
||||||
|
if len(ctx.crt_params) < 1:
|
||||||
|
reply = ["[bot]err: 未指定插件名"]
|
||||||
|
else:
|
||||||
|
plugin_name = ctx.crt_params[0]
|
||||||
|
if plugin_name in plugin_list:
|
||||||
|
plugin_list[plugin_name]['enabled'] = new_status
|
||||||
|
|
||||||
|
for func in plugin_host.__callable_functions__:
|
||||||
|
if func['name'].startswith(plugin_name+"-"):
|
||||||
|
func['enabled'] = new_status
|
||||||
|
|
||||||
|
plugin_switch.dump_switch()
|
||||||
|
reply = ["[bot]已{}插件: {}".format("启用" if new_status else "禁用", plugin_name)]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]err:未找到插件: {}, 请使用!plugin命令查看插件列表".format(plugin_name)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
0
pkg/qqbot/cmds/session/__init__.py
Normal file
0
pkg/qqbot/cmds/session/__init__.py
Normal file
71
pkg/qqbot/cmds/session/default.py
Normal file
71
pkg/qqbot/cmds/session/default.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
from ....utils import context
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="default",
|
||||||
|
description="操作情景预设",
|
||||||
|
usage="!default\n!default set [指定情景预设为默认]",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class DefaultCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
params = ctx.params
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if len(params) == 0:
|
||||||
|
# 输出目前所有情景预设
|
||||||
|
import pkg.openai.dprompt as dprompt
|
||||||
|
reply_str = "[bot]当前所有情景预设({}模式):\n\n".format(config['preset_mode'])
|
||||||
|
|
||||||
|
prompts = dprompt.mode_inst().list()
|
||||||
|
|
||||||
|
for key in prompts:
|
||||||
|
pro = prompts[key]
|
||||||
|
reply_str += "名称: {}".format(key)
|
||||||
|
|
||||||
|
for r in pro:
|
||||||
|
reply_str += "\n - [{}]: {}".format(r['role'], r['content'])
|
||||||
|
|
||||||
|
reply_str += "\n\n"
|
||||||
|
|
||||||
|
reply_str += "\n当前默认情景预设:{}\n".format(dprompt.mode_inst().get_using_name())
|
||||||
|
reply_str += "请使用 !default set <情景预设名称> 来设置默认情景预设"
|
||||||
|
reply = [reply_str]
|
||||||
|
else:
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=DefaultCommand,
|
||||||
|
name="set",
|
||||||
|
description="设置默认情景预设",
|
||||||
|
usage="!default set <情景预设名称>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class DefaultSetCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
if len(ctx.crt_params) == 0:
|
||||||
|
reply = ["[bot]err: 请指定情景预设名称"]
|
||||||
|
elif len(ctx.crt_params) > 0:
|
||||||
|
import pkg.openai.dprompt as dprompt
|
||||||
|
try:
|
||||||
|
full_name = dprompt.mode_inst().set_using_name(ctx.crt_params[0])
|
||||||
|
reply = ["[bot]已设置默认情景预设为:{}".format(full_name)]
|
||||||
|
except Exception as e:
|
||||||
|
reply = ["[bot]err: {}".format(e)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
51
pkg/qqbot/cmds/session/del.py
Normal file
51
pkg/qqbot/cmds/session/del.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="del",
|
||||||
|
description="删除当前会话的历史记录",
|
||||||
|
usage="!del <序号>\n!del all",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class DelCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
params = ctx.params
|
||||||
|
reply = []
|
||||||
|
if len(params) == 0:
|
||||||
|
reply = ["[bot]参数不足, 格式: !del <序号>\n可以通过!list查看序号"]
|
||||||
|
else:
|
||||||
|
if params[0] == 'all':
|
||||||
|
return False, []
|
||||||
|
elif params[0].isdigit():
|
||||||
|
if pkg.openai.session.get_session(session_name).delete_history(int(params[0])):
|
||||||
|
reply = ["[bot]已删除历史会话 #{}".format(params[0])]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]没有历史会话 #{}".format(params[0])]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]参数错误, 格式: !del <序号>\n可以通过!list查看序号"]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=DelCommand,
|
||||||
|
name="all",
|
||||||
|
description="删除当前会话的全部历史记录",
|
||||||
|
usage="!del all",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class DelAllCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
reply = []
|
||||||
|
pkg.openai.session.get_session(session_name).delete_all_history()
|
||||||
|
reply = ["[bot]已删除所有历史会话"]
|
||||||
|
return True, reply
|
||||||
50
pkg/qqbot/cmds/session/delhst.py
Normal file
50
pkg/qqbot/cmds/session/delhst.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="delhst",
|
||||||
|
description="删除指定会话的所有历史记录",
|
||||||
|
usage="!delhst <会话名称>\n!delhst all",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class DelHistoryCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
import pkg.utils.context
|
||||||
|
params = ctx.params
|
||||||
|
reply = []
|
||||||
|
if len(params) == 0:
|
||||||
|
reply = [
|
||||||
|
"[bot]err:请输入要删除的会话名: group_<群号> 或者 person_<QQ号>, 或使用 !delhst all 删除所有会话的历史记录"]
|
||||||
|
else:
|
||||||
|
if params[0] == 'all':
|
||||||
|
return False, []
|
||||||
|
else:
|
||||||
|
if pkg.utils.context.get_database_manager().delete_all_history(params[0]):
|
||||||
|
reply = ["[bot]已删除会话 {} 的所有历史记录".format(params[0])]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]未找到会话 {} 的历史记录".format(params[0])]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=DelHistoryCommand,
|
||||||
|
name="all",
|
||||||
|
description="删除所有会话的全部历史记录",
|
||||||
|
usage="!delhst all",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class DelAllHistoryCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.utils.context
|
||||||
|
reply = []
|
||||||
|
pkg.utils.context.get_database_manager().delete_all_session_history()
|
||||||
|
reply = ["[bot]已删除所有会话的历史记录"]
|
||||||
|
return True, reply
|
||||||
|
|
||||||
29
pkg/qqbot/cmds/session/last.py
Normal file
29
pkg/qqbot/cmds/session/last.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="last",
|
||||||
|
description="切换前一次对话",
|
||||||
|
usage="!last",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class LastCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
result = pkg.openai.session.get_session(session_name).last_session()
|
||||||
|
if result is None:
|
||||||
|
reply = ["[bot]没有前一次的对话"]
|
||||||
|
else:
|
||||||
|
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
||||||
|
'%Y-%m-%d %H:%M:%S')
|
||||||
|
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(datetime_str)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
65
pkg/qqbot/cmds/session/list.py
Normal file
65
pkg/qqbot/cmds/session/list.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name='list',
|
||||||
|
description='列出当前会话的所有历史记录',
|
||||||
|
usage='!list\n!list [页数]',
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class ListCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
params = ctx.params
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
pkg.openai.session.get_session(session_name).persistence()
|
||||||
|
page = 0
|
||||||
|
|
||||||
|
if len(params) > 0:
|
||||||
|
try:
|
||||||
|
page = int(params[0])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
results = pkg.openai.session.get_session(session_name).list_history(page=page)
|
||||||
|
if len(results) == 0:
|
||||||
|
reply_str = "[bot]第{}页没有历史会话".format(page)
|
||||||
|
else:
|
||||||
|
reply_str = "[bot]历史会话 第{}页:\n".format(page)
|
||||||
|
current = -1
|
||||||
|
for i in range(len(results)):
|
||||||
|
# 时间(使用create_timestamp转换) 序号 部分内容
|
||||||
|
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
|
||||||
|
msg = ""
|
||||||
|
|
||||||
|
msg = json.loads(results[i]['prompt'])
|
||||||
|
|
||||||
|
if len(msg) >= 2:
|
||||||
|
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
|
||||||
|
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
msg[0]['content'])
|
||||||
|
else:
|
||||||
|
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
|
||||||
|
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"无内容")
|
||||||
|
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
|
||||||
|
session_name).create_timestamp:
|
||||||
|
current = i + page * 10
|
||||||
|
|
||||||
|
reply_str += "\n以上信息倒序排列"
|
||||||
|
if current != -1:
|
||||||
|
reply_str += ",当前会话是 #{}\n".format(current)
|
||||||
|
else:
|
||||||
|
reply_str += ",当前处于全新会话或不在此页"
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
29
pkg/qqbot/cmds/session/next.py
Normal file
29
pkg/qqbot/cmds/session/next.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="next",
|
||||||
|
description="切换后一次对话",
|
||||||
|
usage="!next",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class NextCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
result = pkg.openai.session.get_session(session_name).next_session()
|
||||||
|
if result is None:
|
||||||
|
reply = ["[bot]没有后一次的对话"]
|
||||||
|
else:
|
||||||
|
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
||||||
|
'%Y-%m-%d %H:%M:%S')
|
||||||
|
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(datetime_str)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
31
pkg/qqbot/cmds/session/prompt.py
Normal file
31
pkg/qqbot/cmds/session/prompt.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="prompt",
|
||||||
|
description="获取当前会话的前文",
|
||||||
|
usage="!prompt",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class PromptCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import pkg.openai.session
|
||||||
|
session_name = ctx.session_name
|
||||||
|
params = ctx.params
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
msgs = ""
|
||||||
|
session: list = pkg.openai.session.get_session(session_name).prompt
|
||||||
|
for msg in session:
|
||||||
|
if len(params) != 0 and params[0] in ['-all', '-a']:
|
||||||
|
msgs = msgs + "{}: {}\n\n".format(msg['role'], msg['content'])
|
||||||
|
elif len(msg['content']) > 30:
|
||||||
|
msgs = msgs + "[{}]: {}...\n\n".format(msg['role'], msg['content'][:30])
|
||||||
|
else:
|
||||||
|
msgs = msgs + "[{}]: {}\n\n".format(msg['role'], msg['content'])
|
||||||
|
reply = ["[bot]当前对话所有内容:\n{}".format(msgs)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
33
pkg/qqbot/cmds/session/resend.py
Normal file
33
pkg/qqbot/cmds/session/resend.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="resend",
|
||||||
|
description="重新获取上一次问题的回复",
|
||||||
|
usage="!resend",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class ResendCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
from ....openai import session as openai_session
|
||||||
|
from ....utils import context
|
||||||
|
from ....qqbot import message
|
||||||
|
|
||||||
|
session_name = ctx.session_name
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
session = openai_session.get_session(session_name)
|
||||||
|
to_send = session.undo()
|
||||||
|
|
||||||
|
mgr = context.get_qqbot_manager()
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
reply = message.process_normal_message(to_send, mgr, config,
|
||||||
|
ctx.launcher_type, ctx.launcher_id,
|
||||||
|
ctx.sender_id)
|
||||||
|
|
||||||
|
return True, reply
|
||||||
35
pkg/qqbot/cmds/session/reset.py
Normal file
35
pkg/qqbot/cmds/session/reset.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import tips as tips_custom
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
from ....openai import session
|
||||||
|
from ....utils import context
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name='reset',
|
||||||
|
description='重置当前会话',
|
||||||
|
usage='!reset',
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class ResetCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
params = ctx.params
|
||||||
|
session_name = ctx.session_name
|
||||||
|
|
||||||
|
reply = ""
|
||||||
|
|
||||||
|
if len(params) == 0:
|
||||||
|
session.get_session(session_name).reset(explicit=True)
|
||||||
|
reply = [tips_custom.command_reset_message]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import pkg.openai.dprompt as dprompt
|
||||||
|
session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
|
||||||
|
reply = [tips_custom.command_reset_name_message+"{}".format(dprompt.mode_inst().get_full_name(params[0]))]
|
||||||
|
except Exception as e:
|
||||||
|
reply = ["[bot]会话重置失败:{}".format(e)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
0
pkg/qqbot/cmds/system/__init__.py
Normal file
0
pkg/qqbot/cmds/system/__init__.py
Normal file
93
pkg/qqbot/cmds/system/cconfig.py
Normal file
93
pkg/qqbot/cmds/system/cconfig.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
def config_operation(cmd, params):
|
||||||
|
reply = []
|
||||||
|
import pkg.utils.context
|
||||||
|
# config = pkg.utils.context.get_config()
|
||||||
|
cfg_mgr = pkg.utils.context.get_config_manager()
|
||||||
|
|
||||||
|
false = False
|
||||||
|
true = True
|
||||||
|
|
||||||
|
reply_str = ""
|
||||||
|
if len(params) == 0:
|
||||||
|
reply = ["[bot]err:请输入!cmd cfg查看使用方法"]
|
||||||
|
else:
|
||||||
|
cfg_name = params[0]
|
||||||
|
if cfg_name == 'all':
|
||||||
|
reply_str = "[bot]所有配置项:\n\n"
|
||||||
|
for cfg in cfg_mgr.data.keys():
|
||||||
|
if not cfg.startswith('__') and not cfg == 'logging':
|
||||||
|
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
||||||
|
if isinstance(cfg_mgr.data[cfg], str):
|
||||||
|
reply_str += "{}: \"{}\"\n".format(cfg, cfg_mgr.data[cfg])
|
||||||
|
elif isinstance(cfg_mgr.data[cfg], dict):
|
||||||
|
# 不进行unicode转义,并格式化
|
||||||
|
reply_str += "{}: {}\n".format(cfg,
|
||||||
|
json.dumps(cfg_mgr.data[cfg],
|
||||||
|
ensure_ascii=False, indent=4))
|
||||||
|
else:
|
||||||
|
reply_str += "{}: {}\n".format(cfg, cfg_mgr.data[cfg])
|
||||||
|
reply = [reply_str]
|
||||||
|
else:
|
||||||
|
cfg_entry_path = cfg_name.split('.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if len(params) == 1: # 未指定配置值,返回配置项值
|
||||||
|
cfg_entry = cfg_mgr.data[cfg_entry_path[0]]
|
||||||
|
if len(cfg_entry_path) > 1:
|
||||||
|
for i in range(1, len(cfg_entry_path)):
|
||||||
|
cfg_entry = cfg_entry[cfg_entry_path[i]]
|
||||||
|
|
||||||
|
if isinstance(cfg_entry, str):
|
||||||
|
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, cfg_entry)
|
||||||
|
elif isinstance(cfg_entry, dict):
|
||||||
|
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
||||||
|
json.dumps(cfg_entry,
|
||||||
|
ensure_ascii=False, indent=4))
|
||||||
|
else:
|
||||||
|
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, cfg_entry)
|
||||||
|
reply = [reply_str]
|
||||||
|
else:
|
||||||
|
cfg_value = " ".join(params[1:])
|
||||||
|
|
||||||
|
cfg_value = eval(cfg_value)
|
||||||
|
|
||||||
|
cfg_entry = cfg_mgr.data[cfg_entry_path[0]]
|
||||||
|
if len(cfg_entry_path) > 1:
|
||||||
|
for i in range(1, len(cfg_entry_path) - 1):
|
||||||
|
cfg_entry = cfg_entry[cfg_entry_path[i]]
|
||||||
|
if isinstance(cfg_entry[cfg_entry_path[-1]], type(cfg_value)):
|
||||||
|
cfg_entry[cfg_entry_path[-1]] = cfg_value
|
||||||
|
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
||||||
|
else:
|
||||||
|
cfg_mgr.data[cfg_entry_path[0]] = cfg_value
|
||||||
|
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||||
|
except KeyError:
|
||||||
|
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||||
|
except NameError:
|
||||||
|
reply = ["[bot]err:值{}不合法(字符串需要使用双引号包裹)".format(cfg_value)]
|
||||||
|
except ValueError:
|
||||||
|
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="cfg",
|
||||||
|
description="配置项管理",
|
||||||
|
usage="!cfg <配置项> [配置值]\n!cfg all",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class CfgCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
return True, config_operation(ctx.command, ctx.params)
|
||||||
|
|
||||||
39
pkg/qqbot/cmds/system/cmd.py
Normal file
39
pkg/qqbot/cmds/system/cmd.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="cmd",
|
||||||
|
description="显示命令列表",
|
||||||
|
usage="!cmd\n!cmd <命令名称>",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class CmdCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
command_list = aamgr.__command_list__
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
if len(ctx.params) == 0:
|
||||||
|
reply_str = "[bot]当前所有命令:\n\n"
|
||||||
|
|
||||||
|
# 遍历顶级命令
|
||||||
|
for key in command_list:
|
||||||
|
command = command_list[key]
|
||||||
|
if command['parent'] is None:
|
||||||
|
reply_str += "!{} - {}\n".format(key, command['description'])
|
||||||
|
|
||||||
|
reply_str += "\n请使用 !cmd <命令名称> 来查看命令的详细信息"
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
else:
|
||||||
|
command_name = ctx.params[0]
|
||||||
|
if command_name in command_list:
|
||||||
|
reply = [command_list[command_name]['cls'].help()]
|
||||||
|
else:
|
||||||
|
reply = ["[bot]命令 {} 不存在".format(command_name)]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
24
pkg/qqbot/cmds/system/help.py
Normal file
24
pkg/qqbot/cmds/system/help.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="help",
|
||||||
|
description="显示自定义的帮助信息",
|
||||||
|
usage="!help",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class HelpCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import tips
|
||||||
|
reply = ["[bot] "+tips.help_message + "\n请输入 !cmd 查看命令列表"]
|
||||||
|
|
||||||
|
# 警告config.help_message过时
|
||||||
|
import config
|
||||||
|
if hasattr(config, "help_message"):
|
||||||
|
reply[0] += "\n\n警告:config.py中的help_message已过时,不再生效,请使用tips.py中的help_message替代"
|
||||||
|
|
||||||
|
return True, reply
|
||||||
|
|
||||||
25
pkg/qqbot/cmds/system/reload.py
Normal file
25
pkg/qqbot/cmds/system/reload.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import threading
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="reload",
|
||||||
|
description="执行热重载",
|
||||||
|
usage="!reload",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class ReloadCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
import pkg.utils.reloader
|
||||||
|
def reload_task():
|
||||||
|
pkg.utils.reloader.reload_all()
|
||||||
|
|
||||||
|
threading.Thread(target=reload_task, daemon=True).start()
|
||||||
|
|
||||||
|
return True, reply
|
||||||
38
pkg/qqbot/cmds/system/update.py
Normal file
38
pkg/qqbot/cmds/system/update.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="update",
|
||||||
|
description="更新程序",
|
||||||
|
usage="!update",
|
||||||
|
aliases=[],
|
||||||
|
privilege=2
|
||||||
|
)
|
||||||
|
class UpdateCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
reply = []
|
||||||
|
import pkg.utils.updater
|
||||||
|
import pkg.utils.reloader
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
|
def update_task():
|
||||||
|
try:
|
||||||
|
if pkg.utils.updater.update_all():
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成, 请手动重启程序。")
|
||||||
|
else:
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("无新版本")
|
||||||
|
except Exception as e0:
|
||||||
|
traceback.print_exc()
|
||||||
|
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
|
||||||
|
return
|
||||||
|
|
||||||
|
threading.Thread(target=update_task, daemon=True).start()
|
||||||
|
|
||||||
|
reply = ["[bot]正在更新,请耐心等待,请勿重复发起更新..."]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
33
pkg/qqbot/cmds/system/usage.py
Normal file
33
pkg/qqbot/cmds/system/usage.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="usage",
|
||||||
|
description="获取使用情况",
|
||||||
|
usage="!usage",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class UsageCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
import config
|
||||||
|
import pkg.utils.context
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
|
||||||
|
reply_str = "[bot]各api-key使用情况:\n\n"
|
||||||
|
|
||||||
|
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
||||||
|
for key_name in api_keys:
|
||||||
|
text_length = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||||
|
.get_text_length_of_key(api_keys[key_name])
|
||||||
|
image_count = pkg.utils.context.get_openai_manager().audit_mgr \
|
||||||
|
.get_image_count_of_key(api_keys[key_name])
|
||||||
|
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length),
|
||||||
|
int(image_count))
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
27
pkg/qqbot/cmds/system/version.py
Normal file
27
pkg/qqbot/cmds/system/version.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from .. import aamgr
|
||||||
|
|
||||||
|
|
||||||
|
@aamgr.AbstractCommandNode.register(
|
||||||
|
parent=None,
|
||||||
|
name="version",
|
||||||
|
description="查看版本信息",
|
||||||
|
usage="!version",
|
||||||
|
aliases=[],
|
||||||
|
privilege=1
|
||||||
|
)
|
||||||
|
class VersionCommand(aamgr.AbstractCommandNode):
|
||||||
|
@classmethod
|
||||||
|
def process(cls, ctx: aamgr.Context) -> tuple[bool, list]:
|
||||||
|
reply = []
|
||||||
|
import pkg.utils.updater
|
||||||
|
|
||||||
|
reply_str = "[bot]当前版本:\n{}\n".format(pkg.utils.updater.get_current_version_info())
|
||||||
|
try:
|
||||||
|
if pkg.utils.updater.is_new_version_available():
|
||||||
|
reply_str += "\n有新版本可用,请使用命令 !update 进行更新"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
reply = [reply_str]
|
||||||
|
|
||||||
|
return True, reply
|
||||||
49
pkg/qqbot/command.py
Normal file
49
pkg/qqbot/command.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 命令处理模块
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..qqbot.cmds import aamgr as cmdmgr
|
||||||
|
|
||||||
|
|
||||||
|
def process_command(session_name: str, text_message: str, mgr, config: dict,
|
||||||
|
launcher_type: str, launcher_id: int, sender_id: int, is_admin: bool) -> list:
|
||||||
|
reply = []
|
||||||
|
try:
|
||||||
|
logging.info(
|
||||||
|
"[{}]发起命令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
||||||
|
"..." if len(text_message) > 20 else "")))
|
||||||
|
|
||||||
|
cmd = text_message[1:].strip().split(' ')[0]
|
||||||
|
|
||||||
|
params = text_message[1:].strip().split(' ')[1:]
|
||||||
|
|
||||||
|
# 把!~开头的转换成!cfg
|
||||||
|
if cmd.startswith('~'):
|
||||||
|
params = [cmd[1:]] + params
|
||||||
|
cmd = 'cfg'
|
||||||
|
|
||||||
|
# 包装参数
|
||||||
|
context = cmdmgr.Context(
|
||||||
|
command=cmd,
|
||||||
|
crt_command=cmd,
|
||||||
|
params=params,
|
||||||
|
crt_params=params[:],
|
||||||
|
session_name=session_name,
|
||||||
|
text_message=text_message,
|
||||||
|
launcher_type=launcher_type,
|
||||||
|
launcher_id=launcher_id,
|
||||||
|
sender_id=sender_id,
|
||||||
|
is_admin=is_admin,
|
||||||
|
privilege=2 if is_admin else 1, # 普通用户1,管理员2
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
reply = cmdmgr.execute(context)
|
||||||
|
except cmdmgr.CommandPrivilegeError as e:
|
||||||
|
reply = ["{}".format(e)]
|
||||||
|
|
||||||
|
return reply
|
||||||
|
except Exception as e:
|
||||||
|
mgr.notify_admin("{}命令执行失败:{}".format(session_name, e))
|
||||||
|
logging.exception(e)
|
||||||
|
reply = ["[bot]err:{}".format(e)]
|
||||||
|
|
||||||
|
return reply
|
||||||
@@ -1,18 +1,87 @@
|
|||||||
|
# 敏感词过滤模块
|
||||||
import re
|
import re
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
class ReplyFilter:
|
class ReplyFilter:
|
||||||
|
|
||||||
sensitive_words = []
|
sensitive_words = []
|
||||||
|
mask = "*"
|
||||||
|
mask_word = ""
|
||||||
|
|
||||||
def __init__(self, sensitive_words: list):
|
# 默认值( 兼容性考虑 )
|
||||||
|
baidu_check = False
|
||||||
|
baidu_api_key = ""
|
||||||
|
baidu_secret_key = ""
|
||||||
|
inappropriate_message_tips = "[百度云]请珍惜机器人,当前返回内容不合规"
|
||||||
|
|
||||||
|
def __init__(self, sensitive_words: list, mask: str = "*", mask_word: str = ""):
|
||||||
self.sensitive_words = sensitive_words
|
self.sensitive_words = sensitive_words
|
||||||
|
self.mask = mask
|
||||||
|
self.mask_word = mask_word
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
self.baidu_check = config['baidu_check']
|
||||||
|
self.baidu_api_key = config['baidu_api_key']
|
||||||
|
self.baidu_secret_key = config['baidu_secret_key']
|
||||||
|
self.inappropriate_message_tips = config['inappropriate_message_tips']
|
||||||
|
|
||||||
|
def is_illegal(self, message: str) -> bool:
|
||||||
|
processed = self.process(message)
|
||||||
|
if processed != message:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def process(self, message: str) -> str:
|
def process(self, message: str) -> str:
|
||||||
|
|
||||||
|
# 本地关键词屏蔽
|
||||||
for word in self.sensitive_words:
|
for word in self.sensitive_words:
|
||||||
match = re.findall(word, message)
|
match = re.findall(word, message)
|
||||||
if len(match) > 0:
|
if len(match) > 0:
|
||||||
for i in range(len(match)):
|
for i in range(len(match)):
|
||||||
message = message.replace(match[i], "*" * len(match[i]))
|
if self.mask_word == "":
|
||||||
|
message = message.replace(match[i], self.mask * len(match[i]))
|
||||||
|
else:
|
||||||
|
message = message.replace(match[i], self.mask_word)
|
||||||
|
|
||||||
|
# 百度云审核
|
||||||
|
if self.baidu_check:
|
||||||
|
|
||||||
|
# 百度云审核URL
|
||||||
|
baidu_url = "https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token=" + \
|
||||||
|
str(requests.post("https://aip.baidubce.com/oauth/2.0/token",
|
||||||
|
params={"grant_type": "client_credentials",
|
||||||
|
"client_id": self.baidu_api_key,
|
||||||
|
"client_secret": self.baidu_secret_key}).json().get("access_token"))
|
||||||
|
|
||||||
|
# 百度云审核
|
||||||
|
payload = "text=" + message
|
||||||
|
logging.info("向百度云发送:" + payload)
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'}
|
||||||
|
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode('utf-8')
|
||||||
|
|
||||||
|
response = requests.request("POST", baidu_url, headers=headers, data=payload)
|
||||||
|
response_dict = json.loads(response.text)
|
||||||
|
|
||||||
|
if "error_code" in response_dict:
|
||||||
|
error_msg = response_dict.get("error_msg")
|
||||||
|
logging.warning(f"百度云判定出错,错误信息:{error_msg}")
|
||||||
|
conclusion = f"百度云判定出错,错误信息:{error_msg}\n以下是原消息:{message}"
|
||||||
|
else:
|
||||||
|
conclusion = response_dict["conclusion"]
|
||||||
|
if conclusion in ("合规"):
|
||||||
|
logging.info(f"百度云判定结果:{conclusion}")
|
||||||
|
return message
|
||||||
|
else:
|
||||||
|
logging.warning(f"百度云判定结果:{conclusion}")
|
||||||
|
conclusion = self.inappropriate_message_tips
|
||||||
|
# 返回百度云审核结果
|
||||||
|
return conclusion
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|||||||
18
pkg/qqbot/ignore.py
Normal file
18
pkg/qqbot/ignore.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
|
def ignore(msg: str) -> bool:
|
||||||
|
"""检查消息是否应该被忽略"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if 'prefix' in config['ignore_rules']:
|
||||||
|
for rule in config['ignore_rules']['prefix']:
|
||||||
|
if msg.startswith(rule):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if 'regexp' in config['ignore_rules']:
|
||||||
|
for rule in config['ignore_rules']['regexp']:
|
||||||
|
if re.search(rule, msg):
|
||||||
|
return True
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
|
|
||||||
import mirai.models.bus
|
|
||||||
from mirai import At, GroupMessage, MessageEvent, Mirai, Plain, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
|
|
||||||
FriendMessage, Image
|
|
||||||
|
|
||||||
import pkg.openai.session
|
|
||||||
import pkg.openai.manager
|
|
||||||
from func_timeout import FunctionTimedOut
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pkg.qqbot.filter
|
from mirai import At, GroupMessage, MessageEvent, StrangerMessage, \
|
||||||
import pkg.qqbot.process as processor
|
FriendMessage, Image, MessageChain, Plain
|
||||||
import pkg.utils.context
|
import func_timeout
|
||||||
|
|
||||||
|
from ..openai import session as openai_session
|
||||||
|
|
||||||
# 并行运行
|
from ..qqbot import filter as qqbot_filter
|
||||||
def go(func, args=()):
|
from ..qqbot import process as processor
|
||||||
thread = threading.Thread(target=func, args=args, daemon=True)
|
from ..utils import context
|
||||||
thread.start()
|
from ..plugin import host as plugin_host
|
||||||
|
from ..plugin import models as plugin_models
|
||||||
|
import tips as tips_custom
|
||||||
|
from ..qqbot import adapter as msadapter
|
||||||
|
|
||||||
|
|
||||||
# 检查消息是否符合泛响应匹配机制
|
# 检查消息是否符合泛响应匹配机制
|
||||||
def check_response_rule(text: str) -> (bool, str):
|
def check_response_rule(group_id:int, text: str):
|
||||||
config = pkg.utils.context.get_config()
|
config = context.get_config_manager().data
|
||||||
if not hasattr(config, 'response_rules'):
|
|
||||||
return False, ''
|
rules = config['response_rules']
|
||||||
|
|
||||||
|
# 检查是否有特定规则
|
||||||
|
if 'prefix' not in config['response_rules']:
|
||||||
|
if str(group_id) in config['response_rules']:
|
||||||
|
rules = config['response_rules'][str(group_id)]
|
||||||
|
else:
|
||||||
|
rules = config['response_rules']['default']
|
||||||
|
|
||||||
rules = config.response_rules
|
|
||||||
# 检查前缀匹配
|
# 检查前缀匹配
|
||||||
if 'prefix' in rules:
|
if 'prefix' in rules:
|
||||||
for rule in rules['prefix']:
|
for rule in rules['prefix']:
|
||||||
@@ -47,104 +47,252 @@ def check_response_rule(text: str) -> (bool, str):
|
|||||||
return False, ""
|
return False, ""
|
||||||
|
|
||||||
|
|
||||||
|
def response_at(group_id: int):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
use_response_rule = config['response_rules']
|
||||||
|
|
||||||
|
# 检查是否有特定规则
|
||||||
|
if 'prefix' not in config['response_rules']:
|
||||||
|
if str(group_id) in config['response_rules']:
|
||||||
|
use_response_rule = config['response_rules'][str(group_id)]
|
||||||
|
else:
|
||||||
|
use_response_rule = config['response_rules']['default']
|
||||||
|
|
||||||
|
if 'at' not in use_response_rule:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return use_response_rule['at']
|
||||||
|
|
||||||
|
|
||||||
|
def random_responding(group_id):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
use_response_rule = config['response_rules']
|
||||||
|
|
||||||
|
# 检查是否有特定规则
|
||||||
|
if 'prefix' not in config['response_rules']:
|
||||||
|
if str(group_id) in config['response_rules']:
|
||||||
|
use_response_rule = config['response_rules'][str(group_id)]
|
||||||
|
else:
|
||||||
|
use_response_rule = config['response_rules']['default']
|
||||||
|
|
||||||
|
if 'random_rate' in use_response_rule:
|
||||||
|
import random
|
||||||
|
return random.random() < use_response_rule['random_rate']
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# 控制QQ消息输入输出的类
|
# 控制QQ消息输入输出的类
|
||||||
class QQBotManager:
|
class QQBotManager:
|
||||||
retry = 3
|
retry = 3
|
||||||
|
|
||||||
bot = None
|
adapter: msadapter.MessageSourceAdapter = None
|
||||||
|
|
||||||
|
bot_account_id: int = 0
|
||||||
|
|
||||||
reply_filter = None
|
reply_filter = None
|
||||||
|
|
||||||
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, first_time_init=True):
|
enable_banlist = False
|
||||||
|
|
||||||
self.timeout = timeout
|
enable_private = True
|
||||||
self.retry = retry
|
enable_group = True
|
||||||
|
|
||||||
config = pkg.utils.context.get_config()
|
ban_person = []
|
||||||
if os.path.exists("sensitive.json") \
|
ban_group = []
|
||||||
and config.sensitive_word_filter is not None \
|
|
||||||
and config.sensitive_word_filter:
|
def __init__(self, first_time_init=True):
|
||||||
with open("sensitive.json", "r", encoding="utf-8") as f:
|
config = context.get_config_manager().data
|
||||||
self.reply_filter = pkg.qqbot.filter.ReplyFilter(json.load(f)['words'])
|
|
||||||
else:
|
self.timeout = config['process_message_timeout']
|
||||||
self.reply_filter = pkg.qqbot.filter.ReplyFilter([])
|
self.retry = config['retry_times']
|
||||||
|
|
||||||
# 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用
|
# 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用
|
||||||
# 故只在第一次初始化时创建bot对象,重载之后使用原bot对象
|
# 故只在第一次初始化时创建bot对象,重载之后使用原bot对象
|
||||||
# 因此,bot的配置不支持热重载
|
# 因此,bot的配置不支持热重载
|
||||||
if first_time_init:
|
if first_time_init:
|
||||||
self.first_time_init(mirai_http_api_config)
|
logging.debug("Use adapter:" + config['msg_source_adapter'])
|
||||||
|
if config['msg_source_adapter'] == 'yirimirai':
|
||||||
|
from pkg.qqbot.sources.yirimirai import YiriMiraiAdapter
|
||||||
|
|
||||||
|
mirai_http_api_config = config['mirai_http_api_config']
|
||||||
|
self.bot_account_id = config['mirai_http_api_config']['qq']
|
||||||
|
self.adapter = YiriMiraiAdapter(mirai_http_api_config)
|
||||||
|
elif config['msg_source_adapter'] == 'nakuru':
|
||||||
|
from pkg.qqbot.sources.nakuru import NakuruProjectAdapter
|
||||||
|
self.adapter = NakuruProjectAdapter(config['nakuru_config'])
|
||||||
|
self.bot_account_id = self.adapter.bot_account_id
|
||||||
else:
|
else:
|
||||||
self.bot = pkg.utils.context.get_qqbot_manager().bot
|
self.adapter = context.get_qqbot_manager().adapter
|
||||||
|
self.bot_account_id = context.get_qqbot_manager().bot_account_id
|
||||||
|
|
||||||
|
# 保存 account_id 到审计模块
|
||||||
|
from ..utils.center import apigroup
|
||||||
|
apigroup.APIGroup._runtime_info['account_id'] = "{}".format(self.bot_account_id)
|
||||||
|
|
||||||
pkg.utils.context.set_qqbot_manager(self)
|
context.set_qqbot_manager(self)
|
||||||
|
|
||||||
|
# 注册诸事件
|
||||||
# Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码
|
# Caution: 注册新的事件处理器之后,请务必在unsubscribe_all中编写相应的取消订阅代码
|
||||||
@self.bot.on(FriendMessage)
|
def on_friend_message(event: FriendMessage):
|
||||||
async def on_friend_message(event: FriendMessage):
|
|
||||||
go(self.on_person_message, (event,))
|
|
||||||
|
|
||||||
@self.bot.on(StrangerMessage)
|
def friend_message_handler():
|
||||||
async def on_stranger_message(event: StrangerMessage):
|
# 触发事件
|
||||||
go(self.on_person_message, (event,))
|
args = {
|
||||||
|
"launcher_type": "person",
|
||||||
|
"launcher_id": event.sender.id,
|
||||||
|
"sender_id": event.sender.id,
|
||||||
|
"message_chain": event.message_chain,
|
||||||
|
}
|
||||||
|
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
|
||||||
|
|
||||||
@self.bot.on(GroupMessage)
|
if plugin_event.is_prevented_default():
|
||||||
async def on_group_message(event: GroupMessage):
|
return
|
||||||
go(self.on_group_message, (event,))
|
|
||||||
|
self.on_person_message(event)
|
||||||
|
|
||||||
|
context.get_thread_ctl().submit_user_task(
|
||||||
|
friend_message_handler,
|
||||||
|
)
|
||||||
|
self.adapter.register_listener(
|
||||||
|
FriendMessage,
|
||||||
|
on_friend_message
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_stranger_message(event: StrangerMessage):
|
||||||
|
|
||||||
|
def stranger_message_handler():
|
||||||
|
# 触发事件
|
||||||
|
args = {
|
||||||
|
"launcher_type": "person",
|
||||||
|
"launcher_id": event.sender.id,
|
||||||
|
"sender_id": event.sender.id,
|
||||||
|
"message_chain": event.message_chain,
|
||||||
|
}
|
||||||
|
plugin_event = plugin_host.emit(plugin_models.PersonMessageReceived, **args)
|
||||||
|
|
||||||
|
if plugin_event.is_prevented_default():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_person_message(event)
|
||||||
|
|
||||||
|
context.get_thread_ctl().submit_user_task(
|
||||||
|
stranger_message_handler,
|
||||||
|
)
|
||||||
|
# nakuru不区分好友和陌生人,故仅为yirimirai注册陌生人事件
|
||||||
|
if config['msg_source_adapter'] == 'yirimirai':
|
||||||
|
self.adapter.register_listener(
|
||||||
|
StrangerMessage,
|
||||||
|
on_stranger_message
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_group_message(event: GroupMessage):
|
||||||
|
|
||||||
|
def group_message_handler(event: GroupMessage):
|
||||||
|
# 触发事件
|
||||||
|
args = {
|
||||||
|
"launcher_type": "group",
|
||||||
|
"launcher_id": event.group.id,
|
||||||
|
"sender_id": event.sender.id,
|
||||||
|
"message_chain": event.message_chain,
|
||||||
|
}
|
||||||
|
plugin_event = plugin_host.emit(plugin_models.GroupMessageReceived, **args)
|
||||||
|
|
||||||
|
if plugin_event.is_prevented_default():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.on_group_message(event)
|
||||||
|
|
||||||
|
context.get_thread_ctl().submit_user_task(
|
||||||
|
group_message_handler,
|
||||||
|
event
|
||||||
|
)
|
||||||
|
self.adapter.register_listener(
|
||||||
|
GroupMessage,
|
||||||
|
on_group_message
|
||||||
|
)
|
||||||
|
|
||||||
def unsubscribe_all():
|
def unsubscribe_all():
|
||||||
"""取消所有订阅
|
"""取消所有订阅
|
||||||
|
|
||||||
用于在热重载流程中卸载所有事件处理器
|
用于在热重载流程中卸载所有事件处理器
|
||||||
"""
|
"""
|
||||||
assert isinstance(self.bot, Mirai)
|
self.adapter.unregister_listener(
|
||||||
bus = self.bot.bus
|
FriendMessage,
|
||||||
assert isinstance(bus, mirai.models.bus.ModelEventBus)
|
on_friend_message
|
||||||
|
)
|
||||||
bus.unsubscribe(FriendMessage, on_friend_message)
|
if config['msg_source_adapter'] == 'yirimirai':
|
||||||
bus.unsubscribe(StrangerMessage, on_stranger_message)
|
self.adapter.unregister_listener(
|
||||||
bus.unsubscribe(GroupMessage, on_group_message)
|
StrangerMessage,
|
||||||
|
on_stranger_message
|
||||||
|
)
|
||||||
|
self.adapter.unregister_listener(
|
||||||
|
GroupMessage,
|
||||||
|
on_group_message
|
||||||
|
)
|
||||||
|
|
||||||
self.unsubscribe_all = unsubscribe_all
|
self.unsubscribe_all = unsubscribe_all
|
||||||
|
|
||||||
def first_time_init(self, mirai_http_api_config: dict):
|
# 加载禁用列表
|
||||||
"""热重载后不再运行此函数"""
|
if os.path.exists("banlist.py"):
|
||||||
|
import banlist
|
||||||
|
self.enable_banlist = banlist.enable
|
||||||
|
self.ban_person = banlist.person
|
||||||
|
self.ban_group = banlist.group
|
||||||
|
logging.info("加载禁用列表: person: {}, group: {}".format(self.ban_person, self.ban_group))
|
||||||
|
|
||||||
if 'adapter' not in mirai_http_api_config or mirai_http_api_config['adapter'] == "WebSocketAdapter":
|
if hasattr(banlist, "enable_private"):
|
||||||
bot = Mirai(
|
self.enable_private = banlist.enable_private
|
||||||
qq=mirai_http_api_config['qq'],
|
if hasattr(banlist, "enable_group"):
|
||||||
adapter=WebSocketAdapter(
|
self.enable_group = banlist.enable_group
|
||||||
verify_key=mirai_http_api_config['verifyKey'],
|
|
||||||
host=mirai_http_api_config['host'],
|
|
||||||
port=mirai_http_api_config['port']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif mirai_http_api_config['adapter'] == "HTTPAdapter":
|
|
||||||
bot = Mirai(
|
|
||||||
qq=mirai_http_api_config['qq'],
|
|
||||||
adapter=HTTPAdapter(
|
|
||||||
verify_key=mirai_http_api_config['verifyKey'],
|
|
||||||
host=mirai_http_api_config['host'],
|
|
||||||
port=mirai_http_api_config['port']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if os.path.exists("sensitive.json") \
|
||||||
|
and config['sensitive_word_filter'] is not None \
|
||||||
|
and config['sensitive_word_filter']:
|
||||||
|
with open("sensitive.json", "r", encoding="utf-8") as f:
|
||||||
|
sensitive_json = json.load(f)
|
||||||
|
self.reply_filter = qqbot_filter.ReplyFilter(
|
||||||
|
sensitive_words=sensitive_json['words'],
|
||||||
|
mask=sensitive_json['mask'] if 'mask' in sensitive_json else '*',
|
||||||
|
mask_word=sensitive_json['mask_word'] if 'mask_word' in sensitive_json else ''
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("未知的适配器类型")
|
self.reply_filter = qqbot_filter.ReplyFilter([])
|
||||||
|
|
||||||
self.bot = bot
|
def send(self, event, msg, check_quote=True, check_at_sender=True):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if check_at_sender and config['at_sender']:
|
||||||
|
msg.insert(
|
||||||
|
0,
|
||||||
|
Plain(" \n")
|
||||||
|
)
|
||||||
|
|
||||||
def send(self, event, msg, check_quote=True):
|
# 当回复的正文中包含换行时,quote可能会自带at,此时就不再单独添加at,只添加换行
|
||||||
config = pkg.utils.context.get_config()
|
if "\n" not in str(msg[1]) or config['msg_source_adapter'] == 'nakuru':
|
||||||
asyncio.run(
|
msg.insert(
|
||||||
self.bot.send(event, msg, quote=True if hasattr(config,
|
0,
|
||||||
"quote_origin") and config.quote_origin and check_quote else False))
|
At(
|
||||||
|
event.sender.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.adapter.reply_message(
|
||||||
|
event,
|
||||||
|
msg,
|
||||||
|
quote_origin=True if config['quote_origin'] and check_quote else False
|
||||||
|
)
|
||||||
|
|
||||||
# 私聊消息处理
|
# 私聊消息处理
|
||||||
def on_person_message(self, event: MessageEvent):
|
def on_person_message(self, event: MessageEvent):
|
||||||
reply = ''
|
reply = ''
|
||||||
|
|
||||||
if event.sender.id == self.bot.qq:
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if not self.enable_private:
|
||||||
|
logging.debug("已在banlist.py中禁用所有私聊")
|
||||||
|
elif event.sender.id == self.bot_account_id:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if Image in event.message_chain:
|
if Image in event.message_chain:
|
||||||
@@ -154,72 +302,126 @@ class QQBotManager:
|
|||||||
failed = 0
|
failed = 0
|
||||||
for i in range(self.retry):
|
for i in range(self.retry):
|
||||||
try:
|
try:
|
||||||
reply = processor.process_message('person', event.sender.id, str(event.message_chain),
|
|
||||||
event.message_chain,
|
@func_timeout.func_set_timeout(config['process_message_timeout'])
|
||||||
event.sender.id)
|
def time_ctrl_wrapper():
|
||||||
|
reply = processor.process_message('person', event.sender.id, str(event.message_chain),
|
||||||
|
event.message_chain,
|
||||||
|
event.sender.id)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
reply = time_ctrl_wrapper()
|
||||||
break
|
break
|
||||||
except FunctionTimedOut:
|
except func_timeout.FunctionTimedOut:
|
||||||
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
logging.warning("person_{}: 超时,重试中({})".format(event.sender.id, i))
|
||||||
|
openai_session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
||||||
|
if "person_{}".format(event.sender.id) in processor.processing:
|
||||||
|
processor.processing.remove('person_{}'.format(event.sender.id))
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if failed == self.retry:
|
if failed == self.retry:
|
||||||
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
openai_session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
||||||
self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id)))
|
self.notify_admin("{} 请求超时".format("person_{}".format(event.sender.id)))
|
||||||
reply = ["[bot]err:请求超时"]
|
reply = [tips_custom.reply_message]
|
||||||
|
|
||||||
if reply:
|
if reply:
|
||||||
return self.send(event, reply, check_quote=False)
|
return self.send(event, reply, check_quote=False, check_at_sender=False)
|
||||||
|
|
||||||
# 群消息处理
|
# 群消息处理
|
||||||
def on_group_message(self, event: GroupMessage):
|
def on_group_message(self, event: GroupMessage):
|
||||||
|
|
||||||
reply = ''
|
reply = ''
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
def process(text=None) -> str:
|
def process(text=None) -> str:
|
||||||
replys = ""
|
replys = ""
|
||||||
if At(self.bot.qq) in event.message_chain:
|
if At(self.bot_account_id) in event.message_chain:
|
||||||
event.message_chain.remove(At(self.bot.qq))
|
event.message_chain.remove(At(self.bot_account_id))
|
||||||
|
|
||||||
# 超时则重试,重试超过次数则放弃
|
# 超时则重试,重试超过次数则放弃
|
||||||
failed = 0
|
failed = 0
|
||||||
for i in range(self.retry):
|
for i in range(self.retry):
|
||||||
try:
|
try:
|
||||||
replys = processor.process_message('group', event.group.id,
|
@func_timeout.func_set_timeout(config['process_message_timeout'])
|
||||||
str(event.message_chain).strip() if text is None else text,
|
def time_ctrl_wrapper():
|
||||||
event.message_chain,
|
replys = processor.process_message('group', event.group.id,
|
||||||
event.sender.id)
|
str(event.message_chain).strip() if text is None else text,
|
||||||
|
event.message_chain,
|
||||||
|
event.sender.id)
|
||||||
|
return replys
|
||||||
|
|
||||||
|
replys = time_ctrl_wrapper()
|
||||||
break
|
break
|
||||||
except FunctionTimedOut:
|
except func_timeout.FunctionTimedOut:
|
||||||
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
logging.warning("group_{}: 超时,重试中({})".format(event.group.id, i))
|
||||||
|
openai_session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
||||||
|
if "group_{}".format(event.group.id) in processor.processing:
|
||||||
|
processor.processing.remove('group_{}'.format(event.group.id))
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if failed == self.retry:
|
if failed == self.retry:
|
||||||
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
openai_session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
||||||
self.notify_admin("{} 请求超时".format("group_{}".format(event.group.id)))
|
self.notify_admin("{} 请求超时".format("group_{}".format(event.group.id)))
|
||||||
replys = ["[bot]err:请求超时"]
|
replys = [tips_custom.replys_message]
|
||||||
|
|
||||||
return replys
|
return replys
|
||||||
|
|
||||||
if Image in event.message_chain:
|
if not self.enable_group:
|
||||||
|
logging.debug("已在banlist.py中禁用所有群聊")
|
||||||
|
elif Image in event.message_chain:
|
||||||
pass
|
pass
|
||||||
elif At(self.bot.qq) not in event.message_chain:
|
|
||||||
check, result = check_response_rule(str(event.message_chain).strip())
|
|
||||||
|
|
||||||
if check:
|
|
||||||
reply = process(result.strip())
|
|
||||||
else:
|
else:
|
||||||
# 直接调用
|
if At(self.bot_account_id) in event.message_chain and response_at(event.group.id):
|
||||||
reply = process()
|
# 直接调用
|
||||||
|
reply = process()
|
||||||
|
else:
|
||||||
|
check, result = check_response_rule(event.group.id, str(event.message_chain).strip())
|
||||||
|
|
||||||
|
if check:
|
||||||
|
reply = process(result.strip())
|
||||||
|
# 检查是否随机响应
|
||||||
|
elif random_responding(event.group.id):
|
||||||
|
logging.info("随机响应group_{}消息".format(event.group.id))
|
||||||
|
reply = process()
|
||||||
|
|
||||||
if reply:
|
if reply:
|
||||||
return self.send(event, reply)
|
return self.send(event, reply)
|
||||||
|
|
||||||
# 通知系统管理员
|
# 通知系统管理员
|
||||||
def notify_admin(self, message: str):
|
def notify_admin(self, message: str):
|
||||||
config = pkg.utils.context.get_config()
|
config = context.get_config_manager().data
|
||||||
if hasattr(config, "admin_qq") and config.admin_qq != 0:
|
if config['admin_qq'] != 0 and config['admin_qq'] != []:
|
||||||
logging.info("通知管理员:{}".format(message))
|
logging.info("通知管理员:{}".format(message))
|
||||||
send_task = self.bot.send_friend_message(config.admin_qq, "[bot]{}".format(message))
|
if type(config['admin_qq']) == int:
|
||||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
self.adapter.send_message(
|
||||||
|
"person",
|
||||||
|
config['admin_qq'],
|
||||||
|
MessageChain([Plain("[bot]{}".format(message))])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for adm in config['admin_qq']:
|
||||||
|
self.adapter.send_message(
|
||||||
|
"person",
|
||||||
|
adm,
|
||||||
|
MessageChain([Plain("[bot]{}".format(message))])
|
||||||
|
)
|
||||||
|
|
||||||
|
def notify_admin_message_chain(self, message):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if config['admin_qq'] != 0 and config['admin_qq'] != []:
|
||||||
|
logging.info("通知管理员:{}".format(message))
|
||||||
|
if type(config['admin_qq']) == int:
|
||||||
|
self.adapter.send_message(
|
||||||
|
"person",
|
||||||
|
config['admin_qq'],
|
||||||
|
message
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for adm in config['admin_qq']:
|
||||||
|
self.adapter.send_message(
|
||||||
|
"person",
|
||||||
|
adm,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
|||||||
134
pkg/qqbot/message.py
Normal file
134
pkg/qqbot/message.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# 普通消息处理模块
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import openai
|
||||||
|
|
||||||
|
from ..utils import context
|
||||||
|
from ..openai import session as openai_session
|
||||||
|
|
||||||
|
from ..plugin import host as plugin_host
|
||||||
|
from ..plugin import models as plugin_models
|
||||||
|
import tips as tips_custom
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(notify_admin: str = "", set_reply: str = "") -> list:
|
||||||
|
"""处理异常,当notify_admin不为空时,会通知管理员,返回通知用户的消息"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
context.get_qqbot_manager().notify_admin(notify_admin)
|
||||||
|
if config['hide_exce_info_to_user']:
|
||||||
|
return [tips_custom.alter_tip_message] if tips_custom.alter_tip_message else []
|
||||||
|
else:
|
||||||
|
return [set_reply]
|
||||||
|
|
||||||
|
|
||||||
|
def process_normal_message(text_message: str, mgr, config: dict, launcher_type: str,
|
||||||
|
launcher_id: int, sender_id: int) -> list:
|
||||||
|
session_name = f"{launcher_type}_{launcher_id}"
|
||||||
|
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
||||||
|
"..." if len(text_message) > 20 else "")))
|
||||||
|
|
||||||
|
session = openai_session.get_session(session_name)
|
||||||
|
|
||||||
|
unexpected_exception_times = 0
|
||||||
|
|
||||||
|
max_unexpected_exception_times = 3
|
||||||
|
|
||||||
|
reply = []
|
||||||
|
while True:
|
||||||
|
if unexpected_exception_times >= max_unexpected_exception_times:
|
||||||
|
reply = handle_exception(notify_admin=f"{session_name},多次尝试失败。", set_reply=f"[bot]多次尝试失败,请重试或联系管理员")
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
prefix = "[GPT]" if config['show_prefix'] else ""
|
||||||
|
|
||||||
|
text, finish_reason, funcs = session.query(text_message)
|
||||||
|
|
||||||
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
"launcher_type": launcher_type,
|
||||||
|
"launcher_id": launcher_id,
|
||||||
|
"sender_id": sender_id,
|
||||||
|
"session": session,
|
||||||
|
"prefix": prefix,
|
||||||
|
"response_text": text,
|
||||||
|
"finish_reason": finish_reason,
|
||||||
|
"funcs_called": funcs,
|
||||||
|
}
|
||||||
|
|
||||||
|
event = plugin_host.emit(plugin_models.NormalMessageResponded, **args)
|
||||||
|
|
||||||
|
if event.get_return_value("prefix") is not None:
|
||||||
|
prefix = event.get_return_value("prefix")
|
||||||
|
|
||||||
|
if event.get_return_value("reply") is not None:
|
||||||
|
reply = event.get_return_value("reply")
|
||||||
|
|
||||||
|
if not event.is_prevented_default():
|
||||||
|
reply = [prefix + text]
|
||||||
|
|
||||||
|
except openai.APIConnectionError as e:
|
||||||
|
err_msg = str(e)
|
||||||
|
if err_msg.__contains__('Error communicating with OpenAI'):
|
||||||
|
reply = handle_exception("{}会话调用API失败:{}\n您的网络无法访问OpenAI接口或网络代理不正常".format(session_name, e),
|
||||||
|
"[bot]err:调用API失败,请重试或联系管理员,或等待修复")
|
||||||
|
else:
|
||||||
|
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e), "[bot]err:调用API失败,请重试或联系管理员,或等待修复")
|
||||||
|
except openai.RateLimitError as e:
|
||||||
|
logging.debug(type(e))
|
||||||
|
logging.debug(e.error['message'])
|
||||||
|
|
||||||
|
if 'message' in e.error and e.error['message'].__contains__('You exceeded your current quota'):
|
||||||
|
# 尝试切换api-key
|
||||||
|
current_key_name = context.get_openai_manager().key_mgr.get_key_name(
|
||||||
|
context.get_openai_manager().key_mgr.using_key
|
||||||
|
)
|
||||||
|
context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||||
|
|
||||||
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
'key_name': current_key_name,
|
||||||
|
'usage': context.get_openai_manager().audit_mgr
|
||||||
|
.get_usage(context.get_openai_manager().key_mgr.get_using_key_md5()),
|
||||||
|
'exceeded_keys': context.get_openai_manager().key_mgr.exceeded,
|
||||||
|
}
|
||||||
|
event = plugin_host.emit(plugin_models.KeyExceeded, **args)
|
||||||
|
|
||||||
|
if not event.is_prevented_default():
|
||||||
|
switched, name = context.get_openai_manager().key_mgr.auto_switch()
|
||||||
|
|
||||||
|
if not switched:
|
||||||
|
reply = handle_exception(
|
||||||
|
"api-key调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key;如果你认为这是误判,请尝试重启程序。".format(
|
||||||
|
current_key_name), "[bot]err:API调用额度超额,请联系管理员,或等待修复")
|
||||||
|
else:
|
||||||
|
openai.api_key = context.get_openai_manager().key_mgr.get_using_key()
|
||||||
|
mgr.notify_admin("api-key调用额度超限({}),接口报错,已切换到{}".format(current_key_name, name))
|
||||||
|
reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"]
|
||||||
|
continue
|
||||||
|
elif 'message' in e.error and e.error['message'].__contains__('You can retry your request'):
|
||||||
|
# 重试
|
||||||
|
unexpected_exception_times += 1
|
||||||
|
continue
|
||||||
|
elif 'message' in e.error and e.error['message']\
|
||||||
|
.__contains__('The server had an error while processing your request'):
|
||||||
|
# 重试
|
||||||
|
unexpected_exception_times += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e),
|
||||||
|
"[bot]err:RateLimitError,请重试或联系作者,或等待修复")
|
||||||
|
except openai.BadRequestError as e:
|
||||||
|
if config['auto_reset'] and "This model's maximum context length is" in str(e):
|
||||||
|
session.reset(persist=True)
|
||||||
|
reply = [tips_custom.session_auto_reset_message]
|
||||||
|
else:
|
||||||
|
reply = handle_exception("{}API调用参数错误:{}\n".format(
|
||||||
|
session_name, e), "[bot]err:API调用参数错误,请联系管理员,或等待修复")
|
||||||
|
except openai.APIStatusError as e:
|
||||||
|
reply = handle_exception("{}API调用服务不可用:{}".format(session_name, e), "[bot]err:API调用服务不可用,请重试或联系管理员,或等待修复")
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(e)
|
||||||
|
reply = handle_exception("{}会话处理异常:{}".format(session_name, e), "[bot]err:{}".format(e))
|
||||||
|
break
|
||||||
|
|
||||||
|
return reply
|
||||||
@@ -1,330 +1,191 @@
|
|||||||
# 此模块提供了消息处理的具体逻辑的接口
|
# 此模块提供了消息处理的具体逻辑的接口
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import time
|
||||||
import json
|
import traceback
|
||||||
import threading
|
|
||||||
|
|
||||||
from func_timeout import func_set_timeout
|
import mirai
|
||||||
import logging
|
import logging
|
||||||
import openai
|
|
||||||
|
|
||||||
from mirai import Image, MessageChain
|
|
||||||
|
|
||||||
# 这里不使用动态引入config
|
# 这里不使用动态引入config
|
||||||
# 因为在这里动态引入会卡死程序
|
# 因为在这里动态引入会卡死程序
|
||||||
# 而此模块静态引用config与动态引入的表现一致
|
# 而此模块静态引用config与动态引入的表现一致
|
||||||
import config as config_init_import
|
# 已弃用,由于超时时间现已动态使用
|
||||||
|
# import config as config_init_import
|
||||||
|
|
||||||
import pkg.openai.session
|
from ..qqbot import ratelimit
|
||||||
import pkg.openai.manager
|
from ..qqbot import command, message
|
||||||
import pkg.utils.reloader
|
from ..openai import session as openai_session
|
||||||
import pkg.utils.updater
|
from ..utils import context
|
||||||
import pkg.utils.context
|
|
||||||
|
from ..plugin import host as plugin_host
|
||||||
|
from ..plugin import models as plugin_models
|
||||||
|
from ..qqbot import ignore
|
||||||
|
from ..qqbot import banlist
|
||||||
|
from ..qqbot import blob
|
||||||
|
import tips as tips_custom
|
||||||
|
|
||||||
processing = []
|
processing = []
|
||||||
|
|
||||||
|
|
||||||
def config_operation(cmd, params):
|
def is_admin(qq: int) -> bool:
|
||||||
reply = []
|
"""兼容list和int类型的管理员判断"""
|
||||||
config = pkg.utils.context.get_config()
|
config = context.get_config_manager().data
|
||||||
reply_str = ""
|
if type(config['admin_qq']) == list:
|
||||||
if len(params) == 0:
|
return qq in config['admin_qq']
|
||||||
reply = ["[bot]err:请输入配置项"]
|
|
||||||
else:
|
else:
|
||||||
cfg_name = params[0]
|
return qq == config['admin_qq']
|
||||||
if cfg_name == 'all':
|
|
||||||
reply_str = "[bot]所有配置项:\n\n"
|
|
||||||
for cfg in dir(config):
|
|
||||||
if not cfg.startswith('__') and not cfg == 'logging':
|
|
||||||
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
|
||||||
if isinstance(getattr(config, cfg), str):
|
|
||||||
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
|
|
||||||
elif isinstance(getattr(config, cfg), dict):
|
|
||||||
# 不进行unicode转义,并格式化
|
|
||||||
reply_str += "{}: {}\n".format(cfg,
|
|
||||||
json.dumps(getattr(config, cfg),
|
|
||||||
ensure_ascii=False, indent=4))
|
|
||||||
else:
|
|
||||||
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
|
|
||||||
reply = [reply_str]
|
|
||||||
elif cfg_name in dir(config):
|
|
||||||
if len(params) == 1:
|
|
||||||
# 按照配置项类型进行格式化
|
|
||||||
if isinstance(getattr(config, cfg_name), str):
|
|
||||||
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
|
|
||||||
elif isinstance(getattr(config, cfg_name), dict):
|
|
||||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
|
||||||
json.dumps(getattr(config, cfg_name),
|
|
||||||
ensure_ascii=False, indent=4))
|
|
||||||
else:
|
|
||||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
|
|
||||||
reply = [reply_str]
|
|
||||||
else:
|
|
||||||
cfg_value = " ".join(params[1:])
|
|
||||||
# 类型转换,如果是json则转换为字典
|
|
||||||
if cfg_value == 'true':
|
|
||||||
cfg_value = True
|
|
||||||
elif cfg_value == 'false':
|
|
||||||
cfg_value = False
|
|
||||||
elif cfg_value.isdigit():
|
|
||||||
cfg_value = int(cfg_value)
|
|
||||||
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
|
|
||||||
cfg_value = json.loads(cfg_value)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
cfg_value = float(cfg_value)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 检查类型是否匹配
|
|
||||||
if isinstance(getattr(config, cfg_name), type(cfg_value)):
|
|
||||||
setattr(config, cfg_name, cfg_value)
|
|
||||||
pkg.utils.context.set_config(config)
|
|
||||||
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
|
||||||
else:
|
|
||||||
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
|
||||||
|
|
||||||
else:
|
|
||||||
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
@func_set_timeout(config_init_import.process_message_timeout)
|
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: mirai.MessageChain,
|
||||||
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain,
|
sender_id: int) -> mirai.MessageChain:
|
||||||
sender_id: int) -> MessageChain:
|
|
||||||
global processing
|
global processing
|
||||||
|
|
||||||
mgr = pkg.utils.context.get_qqbot_manager()
|
mgr = context.get_qqbot_manager()
|
||||||
|
|
||||||
reply = []
|
reply = []
|
||||||
session_name = "{}_{}".format(launcher_type, launcher_id)
|
session_name = "{}_{}".format(launcher_type, launcher_id)
|
||||||
|
|
||||||
|
# 检查发送方是否被禁用
|
||||||
|
if banlist.is_banned(launcher_type, launcher_id, sender_id):
|
||||||
|
logging.info("根据禁用列表忽略{}_{}的消息".format(launcher_type, launcher_id))
|
||||||
|
return []
|
||||||
|
|
||||||
|
if ignore.ignore(text_message):
|
||||||
|
logging.info("根据忽略规则忽略消息: {}".format(text_message))
|
||||||
|
return []
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if not config['wait_last_done'] and session_name in processing:
|
||||||
|
return mirai.MessageChain([mirai.Plain(tips_custom.message_drop_tip)])
|
||||||
|
|
||||||
# 检查是否被禁言
|
# 检查是否被禁言
|
||||||
if launcher_type == 'group':
|
if launcher_type == 'group':
|
||||||
result = mgr.bot.member_info(target=launcher_id, member_id=mgr.bot.qq).get()
|
is_muted = mgr.adapter.is_muted(launcher_id)
|
||||||
result = asyncio.run(result)
|
if is_muted:
|
||||||
if result.mute_time_remaining > 0:
|
logging.info("机器人被禁言,跳过消息处理(group_{})".format(launcher_id))
|
||||||
logging.info("机器人被禁言,跳过消息处理(group_{},剩余{}s)".format(launcher_id,
|
|
||||||
result.mute_time_remaining))
|
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
pkg.openai.session.get_session(session_name).acquire_response_lock()
|
if config['income_msg_check']:
|
||||||
|
if mgr.reply_filter.is_illegal(text_message):
|
||||||
|
return mirai.MessageChain(mirai.Plain("[bot] 消息中存在不合适的内容, 请更换措辞"))
|
||||||
|
|
||||||
|
openai_session.get_session(session_name).acquire_response_lock()
|
||||||
|
|
||||||
|
text_message = text_message.strip()
|
||||||
|
|
||||||
|
|
||||||
|
# 为强制消息延迟计时
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 处理消息
|
||||||
try:
|
try:
|
||||||
if session_name in processing:
|
|
||||||
pkg.openai.session.get_session(session_name).release_response_lock()
|
|
||||||
return ["[bot]err:正在处理中,请稍后再试"]
|
|
||||||
|
|
||||||
processing.append(session_name)
|
processing.append(session_name)
|
||||||
|
|
||||||
config = pkg.utils.context.get_config()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
msg_type = ''
|
||||||
|
if text_message.startswith('!') or text_message.startswith("!"): # 命令
|
||||||
|
msg_type = 'command'
|
||||||
|
# 触发插件事件
|
||||||
|
args = {
|
||||||
|
'launcher_type': launcher_type,
|
||||||
|
'launcher_id': launcher_id,
|
||||||
|
'sender_id': sender_id,
|
||||||
|
'command': text_message[1:].strip().split(' ')[0],
|
||||||
|
'params': text_message[1:].strip().split(' ')[1:],
|
||||||
|
'text_message': text_message,
|
||||||
|
'is_admin': is_admin(sender_id),
|
||||||
|
}
|
||||||
|
event = plugin_host.emit(plugin_models.PersonCommandSent
|
||||||
|
if launcher_type == 'person'
|
||||||
|
else plugin_models.GroupCommandSent, **args)
|
||||||
|
|
||||||
if text_message.startswith('!') or text_message.startswith("!"): # 指令
|
if event.get_return_value("alter") is not None:
|
||||||
try:
|
text_message = event.get_return_value("alter")
|
||||||
logging.info(
|
|
||||||
"[{}]发起指令:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
|
||||||
"..." if len(text_message) > 20 else "")))
|
|
||||||
|
|
||||||
cmd = text_message[1:].strip().split(' ')[0]
|
# 取出插件提交的返回值赋值给reply
|
||||||
|
if event.get_return_value("reply") is not None:
|
||||||
|
reply = event.get_return_value("reply")
|
||||||
|
|
||||||
params = text_message[1:].strip().split(' ')[1:]
|
if not event.is_prevented_default():
|
||||||
if cmd == 'help':
|
reply = command.process_command(session_name, text_message,
|
||||||
reply = ["[bot]" + config.help_message]
|
mgr, config, launcher_type, launcher_id, sender_id, is_admin(sender_id))
|
||||||
elif cmd == 'reset':
|
|
||||||
pkg.openai.session.get_session(session_name).reset(explicit=True)
|
|
||||||
reply = ["[bot]会话已重置"]
|
|
||||||
elif cmd == 'last':
|
|
||||||
result = pkg.openai.session.get_session(session_name).last_session()
|
|
||||||
if result is None:
|
|
||||||
reply = ["[bot]没有前一次的对话"]
|
|
||||||
else:
|
|
||||||
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
|
||||||
'%Y-%m-%d %H:%M:%S')
|
|
||||||
reply = ["[bot]已切换到前一次的对话:\n创建时间:{}\n".format(
|
|
||||||
datetime_str) + result.prompt[
|
|
||||||
:min(100,
|
|
||||||
len(result.prompt))] + \
|
|
||||||
("..." if len(result.prompt) > 100 else "#END#")]
|
|
||||||
elif cmd == 'next':
|
|
||||||
result = pkg.openai.session.get_session(session_name).next_session()
|
|
||||||
if result is None:
|
|
||||||
reply = ["[bot]没有后一次的对话"]
|
|
||||||
else:
|
|
||||||
datetime_str = datetime.datetime.fromtimestamp(result.create_timestamp).strftime(
|
|
||||||
'%Y-%m-%d %H:%M:%S')
|
|
||||||
reply = ["[bot]已切换到后一次的对话:\n创建时间:{}\n".format(
|
|
||||||
datetime_str) + result.prompt[
|
|
||||||
:min(100,
|
|
||||||
len(result.prompt))] + \
|
|
||||||
("..." if len(result.prompt) > 100 else "#END#")]
|
|
||||||
elif cmd == 'prompt':
|
|
||||||
reply = ["[bot]当前对话所有内容:\n" + pkg.openai.session.get_session(session_name).prompt]
|
|
||||||
elif cmd == 'list':
|
|
||||||
pkg.openai.session.get_session(session_name).persistence()
|
|
||||||
page = 0
|
|
||||||
|
|
||||||
if len(params) > 0:
|
|
||||||
try:
|
|
||||||
page = int(params[0])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
results = pkg.openai.session.get_session(session_name).list_history(page=page)
|
|
||||||
if len(results) == 0:
|
|
||||||
reply = ["[bot]第{}页没有历史会话".format(page)]
|
|
||||||
else:
|
|
||||||
reply_str = "[bot]历史会话 第{}页:\n".format(page)
|
|
||||||
current = -1
|
|
||||||
for i in range(len(results)):
|
|
||||||
# 时间(使用create_timestamp转换) 序号 部分内容
|
|
||||||
datetime_obj = datetime.datetime.fromtimestamp(results[i]['create_timestamp'])
|
|
||||||
reply_str += "#{} 创建:{} {}\n".format(i + page * 10,
|
|
||||||
datetime_obj.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
results[i]['prompt'][
|
|
||||||
:min(20, len(results[i]['prompt']))])
|
|
||||||
if results[i]['create_timestamp'] == pkg.openai.session.get_session(
|
|
||||||
session_name).create_timestamp:
|
|
||||||
current = i + page * 10
|
|
||||||
|
|
||||||
reply_str += "\n以上信息倒序排列"
|
|
||||||
if current != -1:
|
|
||||||
reply_str += ",当前会话是 #{}\n".format(current)
|
|
||||||
else:
|
|
||||||
reply_str += ",当前处于全新会话或不在此页"
|
|
||||||
|
|
||||||
reply = [reply_str]
|
|
||||||
elif cmd == 'fee':
|
|
||||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
|
||||||
reply_str = "[bot]api-key费用情况(估算):(阈值:{})\n\n".format(
|
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.api_key_fee_threshold)
|
|
||||||
|
|
||||||
using_key_name = ""
|
|
||||||
for api_key in api_keys:
|
|
||||||
reply_str += "{}:\n - {}美元 {}%\n".format(api_key,
|
|
||||||
round(
|
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
|
||||||
api_keys[api_key]), 6),
|
|
||||||
round(
|
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
|
||||||
api_keys[
|
|
||||||
api_key]) / pkg.utils.context.get_openai_manager().key_mgr.api_key_fee_threshold * 100,
|
|
||||||
3))
|
|
||||||
if api_keys[api_key] == pkg.utils.context.get_openai_manager().key_mgr.using_key:
|
|
||||||
using_key_name = api_key
|
|
||||||
reply_str += "\n当前使用:{}".format(using_key_name)
|
|
||||||
|
|
||||||
reply = [reply_str]
|
|
||||||
elif cmd == 'usage':
|
|
||||||
reply_str = "[bot]各api-key使用情况:\n\n"
|
|
||||||
|
|
||||||
api_keys = pkg.utils.context.get_openai_manager().key_mgr.api_key
|
|
||||||
for key_name in api_keys:
|
|
||||||
text_length = pkg.utils.context.get_openai_manager().audit_mgr\
|
|
||||||
.get_text_length_of_key(api_keys[key_name])
|
|
||||||
image_count = pkg.utils.context.get_openai_manager().audit_mgr\
|
|
||||||
.get_image_count_of_key(api_keys[key_name])
|
|
||||||
reply_str += "{}:\n - 文本长度:{}\n - 图片数量:{}\n".format(key_name, int(text_length), int(image_count))
|
|
||||||
|
|
||||||
reply = [reply_str]
|
|
||||||
elif cmd == 'draw':
|
|
||||||
if len(params) == 0:
|
|
||||||
reply = ["[bot]err:请输入图片描述文字"]
|
|
||||||
else:
|
|
||||||
session = pkg.openai.session.get_session(session_name)
|
|
||||||
|
|
||||||
res = session.draw_image(" ".join(params))
|
|
||||||
|
|
||||||
logging.debug("draw_image result:{}".format(res))
|
|
||||||
reply = [Image(url=res['data'][0]['url'])]
|
|
||||||
if not (hasattr(config, 'include_image_description')
|
|
||||||
and not config.include_image_description):
|
|
||||||
reply.append(" ".join(params))
|
|
||||||
elif cmd == 'reload' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
|
||||||
def reload_task():
|
|
||||||
pkg.utils.reloader.reload_all()
|
|
||||||
|
|
||||||
threading.Thread(target=reload_task, daemon=True).start()
|
|
||||||
elif cmd == 'update' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
|
||||||
def update_task():
|
|
||||||
try:
|
|
||||||
pkg.utils.updater.update_all()
|
|
||||||
except Exception as e0:
|
|
||||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新失败:{}".format(e0))
|
|
||||||
return
|
|
||||||
pkg.utils.reloader.reload_all(notify=False)
|
|
||||||
pkg.utils.context.get_qqbot_manager().notify_admin("更新完成")
|
|
||||||
|
|
||||||
threading.Thread(target=update_task, daemon=True).start()
|
|
||||||
elif cmd == 'cfg' and launcher_type == 'person' and launcher_id == config.admin_qq:
|
|
||||||
reply = config_operation(cmd, params)
|
|
||||||
else:
|
|
||||||
if cmd.startswith("~") and launcher_type == 'person' and launcher_id == config.admin_qq:
|
|
||||||
config_item = cmd[1:]
|
|
||||||
params = [config_item] + params
|
|
||||||
reply = config_operation("cfg", params)
|
|
||||||
else:
|
|
||||||
reply = ["[bot]err:未知的指令或权限不足: "+cmd]
|
|
||||||
except Exception as e:
|
|
||||||
mgr.notify_admin("{}指令执行失败:{}".format(session_name, e))
|
|
||||||
logging.exception(e)
|
|
||||||
reply = ["[bot]err:{}".format(e)]
|
|
||||||
else: # 消息
|
else: # 消息
|
||||||
logging.info("[{}]发送消息:{}".format(session_name, text_message[:min(20, len(text_message))] + (
|
msg_type = 'message'
|
||||||
"..." if len(text_message) > 20 else "")))
|
# 限速丢弃检查
|
||||||
|
# print(ratelimit.__crt_minute_usage__[session_name])
|
||||||
|
if config['rate_limit_strategy'] == "drop":
|
||||||
|
if ratelimit.is_reach_limit(session_name):
|
||||||
|
logging.info("根据限速策略丢弃[{}]消息: {}".format(session_name, text_message))
|
||||||
|
|
||||||
session = pkg.openai.session.get_session(session_name)
|
return mirai.MessageChain(["[bot]"+tips_custom.rate_limit_drop_tip]) if tips_custom.rate_limit_drop_tip != "" else []
|
||||||
|
|
||||||
while True:
|
before = time.time()
|
||||||
try:
|
# 触发插件事件
|
||||||
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
|
args = {
|
||||||
reply = [prefix + session.append(text_message)]
|
"launcher_type": launcher_type,
|
||||||
except openai.error.APIConnectionError as e:
|
"launcher_id": launcher_id,
|
||||||
mgr.notify_admin("{}会话调用API失败:{}".format(session_name, e))
|
"sender_id": sender_id,
|
||||||
reply = ["[bot]err:调用API失败,请重试或联系作者,或等待修复"]
|
"text_message": text_message,
|
||||||
except openai.error.RateLimitError as e:
|
}
|
||||||
# 尝试切换api-key
|
event = plugin_host.emit(plugin_models.PersonNormalMessageReceived
|
||||||
current_tokens_amt = pkg.utils.context.get_openai_manager().key_mgr.get_fee(
|
if launcher_type == 'person'
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.get_using_key())
|
else plugin_models.GroupNormalMessageReceived, **args)
|
||||||
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
|
|
||||||
switched, name = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
|
|
||||||
|
|
||||||
if not switched:
|
if event.get_return_value("alter") is not None:
|
||||||
mgr.notify_admin("API调用额度超限({}),无可用api_key,请向OpenAI账户充值或在config.py中更换api_key".format(
|
text_message = event.get_return_value("alter")
|
||||||
current_tokens_amt))
|
|
||||||
reply = ["[bot]err:API调用额度超额,请联系作者,或等待修复"]
|
|
||||||
else:
|
|
||||||
openai.api_key = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
|
|
||||||
mgr.notify_admin("API调用额度超限({}),接口报错,已切换到{}".format(current_tokens_amt, name))
|
|
||||||
reply = ["[bot]err:API调用额度超额,已自动切换,请重新发送消息"]
|
|
||||||
continue
|
|
||||||
except openai.error.InvalidRequestError as e:
|
|
||||||
mgr.notify_admin("{}API调用参数错误:{}\n\n这可能是由于config.py中的prompt_submit_length参数或"
|
|
||||||
"completion_api_params中的max_tokens参数数值过大导致的,请尝试将其降低".format(
|
|
||||||
session_name, e))
|
|
||||||
reply = ["[bot]err:API调用参数错误,请联系作者,或等待修复"]
|
|
||||||
except openai.error.ServiceUnavailableError as e:
|
|
||||||
# mgr.notify_admin("{}API调用服务不可用:{}".format(session_name, e))
|
|
||||||
reply = ["[bot]err:API调用服务暂不可用,请尝试重试"]
|
|
||||||
except Exception as e:
|
|
||||||
logging.exception(e)
|
|
||||||
reply = ["[bot]err:{}".format(e)]
|
|
||||||
break
|
|
||||||
|
|
||||||
if reply is not None and type(reply[0]) == str:
|
# 取出插件提交的返回值赋值给reply
|
||||||
|
if event.get_return_value("reply") is not None:
|
||||||
|
reply = event.get_return_value("reply")
|
||||||
|
|
||||||
|
if not event.is_prevented_default():
|
||||||
|
reply = message.process_normal_message(text_message,
|
||||||
|
mgr, config, launcher_type, launcher_id, sender_id)
|
||||||
|
|
||||||
|
# 限速等待时间
|
||||||
|
if config['rate_limit_strategy'] == "wait":
|
||||||
|
time.sleep(ratelimit.get_rest_wait_time(session_name, time.time() - before))
|
||||||
|
|
||||||
|
ratelimit.add_usage(session_name)
|
||||||
|
|
||||||
|
if reply is not None and len(reply) > 0 and (type(reply[0]) == str or type(reply[0]) == mirai.Plain):
|
||||||
|
if type(reply[0]) == mirai.Plain:
|
||||||
|
reply[0] = reply[0].text
|
||||||
logging.info(
|
logging.info(
|
||||||
"回复[{}]文字消息:{}".format(session_name,
|
"回复[{}]文字消息:{}".format(session_name,
|
||||||
reply[0][:min(100, len(reply[0]))] + (
|
reply[0][:min(100, len(reply[0]))] + (
|
||||||
"..." if len(reply[0]) > 100 else "")))
|
"..." if len(reply[0]) > 100 else "")))
|
||||||
reply = [mgr.reply_filter.process(reply[0])]
|
if msg_type == 'message':
|
||||||
|
reply = [mgr.reply_filter.process(reply[0])]
|
||||||
|
|
||||||
|
reply = blob.check_text(reply[0])
|
||||||
else:
|
else:
|
||||||
logging.info("回复[{}]图片消息:{}".format(session_name, reply))
|
logging.info("回复[{}]消息".format(session_name))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
processing.remove(session_name)
|
processing.remove(session_name)
|
||||||
finally:
|
finally:
|
||||||
pkg.openai.session.get_session(session_name).release_response_lock()
|
openai_session.get_session(session_name).release_response_lock()
|
||||||
|
|
||||||
return MessageChain(reply)
|
# 检查延迟时间
|
||||||
|
if config['force_delay_range'][1] == 0:
|
||||||
|
delay_time = 0
|
||||||
|
else:
|
||||||
|
import random
|
||||||
|
|
||||||
|
# 从延迟范围中随机取一个值(浮点)
|
||||||
|
rdm = random.uniform(config['force_delay_range'][0], config['force_delay_range'][1])
|
||||||
|
|
||||||
|
spent = time.time() - start_time
|
||||||
|
|
||||||
|
# 如果花费时间小于延迟时间,则延迟
|
||||||
|
delay_time = rdm - spent if rdm - spent > 0 else 0
|
||||||
|
|
||||||
|
# 延迟
|
||||||
|
if delay_time > 0:
|
||||||
|
logging.info("[风控] 强制延迟{:.2f}秒(如需关闭,请到config.py修改force_delay_range字段)".format(delay_time))
|
||||||
|
time.sleep(delay_time)
|
||||||
|
|
||||||
|
return mirai.MessageChain(reply)
|
||||||
|
|||||||
89
pkg/qqbot/ratelimit.py
Normal file
89
pkg/qqbot/ratelimit.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# 限速相关模块
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from ..utils import context
|
||||||
|
|
||||||
|
|
||||||
|
__crt_minute_usage__ = {}
|
||||||
|
"""当前分钟每个会话的对话次数"""
|
||||||
|
|
||||||
|
|
||||||
|
__timer_thr__: threading.Thread = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_limitation(session_name: str) -> int:
|
||||||
|
"""获取会话的限制次数"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
if session_name in config['rate_limitation']:
|
||||||
|
return config['rate_limitation'][session_name]
|
||||||
|
else:
|
||||||
|
return config['rate_limitation']["default"]
|
||||||
|
|
||||||
|
|
||||||
|
def add_usage(session_name: str):
|
||||||
|
"""增加会话的对话次数"""
|
||||||
|
global __crt_minute_usage__
|
||||||
|
if session_name in __crt_minute_usage__:
|
||||||
|
__crt_minute_usage__[session_name] += 1
|
||||||
|
else:
|
||||||
|
__crt_minute_usage__[session_name] = 1
|
||||||
|
|
||||||
|
|
||||||
|
def start_timer():
|
||||||
|
"""启动定时器"""
|
||||||
|
global __timer_thr__
|
||||||
|
__timer_thr__ = threading.Thread(target=run_timer, daemon=True)
|
||||||
|
__timer_thr__.start()
|
||||||
|
|
||||||
|
|
||||||
|
def run_timer():
|
||||||
|
"""启动定时器,每分钟清空一次对话次数"""
|
||||||
|
global __crt_minute_usage__
|
||||||
|
global __timer_thr__
|
||||||
|
|
||||||
|
# 等待直到整分钟
|
||||||
|
time.sleep(60 - time.time() % 60)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if __timer_thr__ != threading.current_thread():
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.debug("清空当前分钟的对话次数")
|
||||||
|
__crt_minute_usage__ = {}
|
||||||
|
time.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
def get_usage(session_name: str) -> int:
|
||||||
|
"""获取会话的对话次数"""
|
||||||
|
global __crt_minute_usage__
|
||||||
|
if session_name in __crt_minute_usage__:
|
||||||
|
return __crt_minute_usage__[session_name]
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_rest_wait_time(session_name: str, spent: float) -> float:
|
||||||
|
"""获取会话此回合的剩余等待时间"""
|
||||||
|
global __crt_minute_usage__
|
||||||
|
|
||||||
|
min_seconds_per_round = 60.0 / get_limitation(session_name)
|
||||||
|
|
||||||
|
if session_name in __crt_minute_usage__:
|
||||||
|
return max(0, min_seconds_per_round - spent)
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def is_reach_limit(session_name: str) -> bool:
|
||||||
|
"""判断会话是否超过限制"""
|
||||||
|
global __crt_minute_usage__
|
||||||
|
|
||||||
|
if session_name in __crt_minute_usage__:
|
||||||
|
return __crt_minute_usage__[session_name] >= get_limitation(session_name)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start_timer()
|
||||||
0
pkg/qqbot/sources/__init__.py
Normal file
0
pkg/qqbot/sources/__init__.py
Normal file
328
pkg/qqbot/sources/nakuru.py
Normal file
328
pkg/qqbot/sources/nakuru.py
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import mirai
|
||||||
|
|
||||||
|
import nakuru
|
||||||
|
import nakuru.entities.components as nkc
|
||||||
|
|
||||||
|
from .. import adapter as adapter_model
|
||||||
|
from ...qqbot import blob
|
||||||
|
from ...utils import context
|
||||||
|
|
||||||
|
|
||||||
|
class NakuruProjectMessageConverter(adapter_model.MessageConverter):
|
||||||
|
"""消息转换器"""
|
||||||
|
@staticmethod
|
||||||
|
def yiri2target(message_chain: mirai.MessageChain) -> list:
|
||||||
|
msg_list = []
|
||||||
|
if type(message_chain) is mirai.MessageChain:
|
||||||
|
msg_list = message_chain.__root__
|
||||||
|
elif type(message_chain) is list:
|
||||||
|
msg_list = message_chain
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown message type: " + str(message_chain) + str(type(message_chain)))
|
||||||
|
|
||||||
|
nakuru_msg_list = []
|
||||||
|
|
||||||
|
# 遍历并转换
|
||||||
|
for component in msg_list:
|
||||||
|
if type(component) is mirai.Plain:
|
||||||
|
nakuru_msg_list.append(nkc.Plain(component.text, False))
|
||||||
|
elif type(component) is mirai.Image:
|
||||||
|
if component.url is not None:
|
||||||
|
nakuru_msg_list.append(nkc.Image.fromURL(component.url))
|
||||||
|
elif component.base64 is not None:
|
||||||
|
nakuru_msg_list.append(nkc.Image.fromBase64(component.base64))
|
||||||
|
elif component.path is not None:
|
||||||
|
nakuru_msg_list.append(nkc.Image.fromFileSystem(component.path))
|
||||||
|
elif type(component) is mirai.Face:
|
||||||
|
nakuru_msg_list.append(nkc.Face(id=component.face_id))
|
||||||
|
elif type(component) is mirai.At:
|
||||||
|
nakuru_msg_list.append(nkc.At(qq=component.target))
|
||||||
|
elif type(component) is mirai.AtAll:
|
||||||
|
nakuru_msg_list.append(nkc.AtAll())
|
||||||
|
elif type(component) is mirai.Voice:
|
||||||
|
if component.url is not None:
|
||||||
|
nakuru_msg_list.append(nkc.Record.fromURL(component.url))
|
||||||
|
elif component.path is not None:
|
||||||
|
nakuru_msg_list.append(nkc.Record.fromFileSystem(component.path))
|
||||||
|
elif type(component) is blob.Forward:
|
||||||
|
# 转发消息
|
||||||
|
yiri_forward_node_list = component.node_list
|
||||||
|
nakuru_forward_node_list = []
|
||||||
|
|
||||||
|
# 遍历并转换
|
||||||
|
for yiri_forward_node in yiri_forward_node_list:
|
||||||
|
try:
|
||||||
|
content_list = NakuruProjectMessageConverter.yiri2target(yiri_forward_node.message_chain)
|
||||||
|
nakuru_forward_node = nkc.Node(
|
||||||
|
name=yiri_forward_node.sender_name,
|
||||||
|
uin=yiri_forward_node.sender_id,
|
||||||
|
time=int(yiri_forward_node.time.timestamp()) if yiri_forward_node.time is not None else None,
|
||||||
|
content=content_list
|
||||||
|
)
|
||||||
|
nakuru_forward_node_list.append(nakuru_forward_node)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
nakuru_msg_list.append(nakuru_forward_node_list)
|
||||||
|
else:
|
||||||
|
nakuru_msg_list.append(nkc.Plain(str(component)))
|
||||||
|
|
||||||
|
return nakuru_msg_list
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target2yiri(message_chain: typing.Any, message_id: int = -1) -> mirai.MessageChain:
|
||||||
|
"""将Yiri的消息链转换为YiriMirai的消息链"""
|
||||||
|
assert type(message_chain) is list
|
||||||
|
|
||||||
|
yiri_msg_list = []
|
||||||
|
import datetime
|
||||||
|
# 添加Source组件以标记message_id等信息
|
||||||
|
yiri_msg_list.append(mirai.models.message.Source(id=message_id, time=datetime.datetime.now()))
|
||||||
|
for component in message_chain:
|
||||||
|
if type(component) is nkc.Plain:
|
||||||
|
yiri_msg_list.append(mirai.Plain(text=component.text))
|
||||||
|
elif type(component) is nkc.Image:
|
||||||
|
yiri_msg_list.append(mirai.Image(url=component.url))
|
||||||
|
elif type(component) is nkc.Face:
|
||||||
|
yiri_msg_list.append(mirai.Face(face_id=component.id))
|
||||||
|
elif type(component) is nkc.At:
|
||||||
|
yiri_msg_list.append(mirai.At(target=component.qq))
|
||||||
|
elif type(component) is nkc.AtAll:
|
||||||
|
yiri_msg_list.append(mirai.AtAll())
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
logging.debug("转换后的消息链: " + str(yiri_msg_list))
|
||||||
|
chain = mirai.MessageChain(yiri_msg_list)
|
||||||
|
return chain
|
||||||
|
|
||||||
|
|
||||||
|
class NakuruProjectEventConverter(adapter_model.EventConverter):
|
||||||
|
"""事件转换器"""
|
||||||
|
@staticmethod
|
||||||
|
def yiri2target(event: typing.Type[mirai.Event]):
|
||||||
|
if event is mirai.GroupMessage:
|
||||||
|
return nakuru.GroupMessage
|
||||||
|
elif event is mirai.FriendMessage:
|
||||||
|
return nakuru.FriendMessage
|
||||||
|
else:
|
||||||
|
raise Exception("未支持转换的事件类型: " + str(event))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target2yiri(event: typing.Any) -> mirai.Event:
|
||||||
|
yiri_chain = NakuruProjectMessageConverter.target2yiri(event.message, event.message_id)
|
||||||
|
if type(event) is nakuru.FriendMessage: # 私聊消息事件
|
||||||
|
return mirai.FriendMessage(
|
||||||
|
sender=mirai.models.entities.Friend(
|
||||||
|
id=event.sender.user_id,
|
||||||
|
nickname=event.sender.nickname,
|
||||||
|
remark=event.sender.nickname
|
||||||
|
),
|
||||||
|
message_chain=yiri_chain,
|
||||||
|
time=event.time
|
||||||
|
)
|
||||||
|
elif type(event) is nakuru.GroupMessage: # 群聊消息事件
|
||||||
|
permission = "MEMBER"
|
||||||
|
|
||||||
|
if event.sender.role == "admin":
|
||||||
|
permission = "ADMINISTRATOR"
|
||||||
|
elif event.sender.role == "owner":
|
||||||
|
permission = "OWNER"
|
||||||
|
|
||||||
|
import mirai.models.entities as entities
|
||||||
|
return mirai.GroupMessage(
|
||||||
|
sender=mirai.models.entities.GroupMember(
|
||||||
|
id=event.sender.user_id,
|
||||||
|
member_name=event.sender.nickname,
|
||||||
|
permission=permission,
|
||||||
|
group=mirai.models.entities.Group(
|
||||||
|
id=event.group_id,
|
||||||
|
name=event.sender.nickname,
|
||||||
|
permission=entities.Permission.Member
|
||||||
|
),
|
||||||
|
special_title=event.sender.title,
|
||||||
|
join_timestamp=0,
|
||||||
|
last_speak_timestamp=0,
|
||||||
|
mute_time_remaining=0,
|
||||||
|
),
|
||||||
|
message_chain=yiri_chain,
|
||||||
|
time=event.time
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception("未支持转换的事件类型: " + str(event))
|
||||||
|
|
||||||
|
|
||||||
|
class NakuruProjectAdapter(adapter_model.MessageSourceAdapter):
|
||||||
|
"""nakuru-project适配器"""
|
||||||
|
bot: nakuru.CQHTTP
|
||||||
|
bot_account_id: int
|
||||||
|
|
||||||
|
message_converter: NakuruProjectMessageConverter = NakuruProjectMessageConverter()
|
||||||
|
event_converter: NakuruProjectEventConverter = NakuruProjectEventConverter()
|
||||||
|
|
||||||
|
listener_list: list[dict]
|
||||||
|
|
||||||
|
def __init__(self, cfg: dict):
|
||||||
|
"""初始化nakuru-project的对象"""
|
||||||
|
self.bot = nakuru.CQHTTP(**cfg)
|
||||||
|
self.listener_list = []
|
||||||
|
# nakuru库有bug,这个接口没法带access_token,会失败
|
||||||
|
# 所以目前自行发请求
|
||||||
|
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
import requests
|
||||||
|
resp = requests.get(
|
||||||
|
url="http://{}:{}/get_login_info".format(config['nakuru_config']['host'], config['nakuru_config']['http_port']),
|
||||||
|
headers={
|
||||||
|
'Authorization': "Bearer " + config['nakuru_config']['token'] if 'token' in config['nakuru_config']else ""
|
||||||
|
},
|
||||||
|
timeout=5,
|
||||||
|
proxies=None
|
||||||
|
)
|
||||||
|
if resp.status_code == 403:
|
||||||
|
logging.error("go-cqhttp拒绝访问,请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")
|
||||||
|
raise Exception("go-cqhttp拒绝访问,请检查config.py中nakuru_config的token是否与go-cqhttp设置的access-token匹配")
|
||||||
|
try:
|
||||||
|
self.bot_account_id = int(resp.json()['data']['user_id'])
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("获取go-cqhttp账号信息失败: {}, 请检查是否已启动go-cqhttp并配置正确".format(e))
|
||||||
|
raise Exception("获取go-cqhttp账号信息失败: {}, 请检查是否已启动go-cqhttp并配置正确".format(e))
|
||||||
|
|
||||||
|
def send_message(
|
||||||
|
self,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
message: typing.Union[mirai.MessageChain, list],
|
||||||
|
converted: bool = False
|
||||||
|
):
|
||||||
|
task = None
|
||||||
|
|
||||||
|
converted_msg = self.message_converter.yiri2target(message) if not converted else message
|
||||||
|
|
||||||
|
# 检查是否有转发消息
|
||||||
|
has_forward = False
|
||||||
|
for msg in converted_msg:
|
||||||
|
if type(msg) is list: # 转发消息,仅回复此消息组件
|
||||||
|
has_forward = True
|
||||||
|
converted_msg = msg
|
||||||
|
break
|
||||||
|
if has_forward:
|
||||||
|
if target_type == "group":
|
||||||
|
task = self.bot.sendGroupForwardMessage(int(target_id), converted_msg)
|
||||||
|
elif target_type == "person":
|
||||||
|
task = self.bot.sendPrivateForwardMessage(int(target_id), converted_msg)
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown target type: " + target_type)
|
||||||
|
else:
|
||||||
|
if target_type == "group":
|
||||||
|
task = self.bot.sendGroupMessage(int(target_id), converted_msg)
|
||||||
|
elif target_type == "person":
|
||||||
|
task = self.bot.sendFriendMessage(int(target_id), converted_msg)
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown target type: " + target_type)
|
||||||
|
|
||||||
|
asyncio.run(task)
|
||||||
|
|
||||||
|
def reply_message(
|
||||||
|
self,
|
||||||
|
message_source: mirai.MessageEvent,
|
||||||
|
message: mirai.MessageChain,
|
||||||
|
quote_origin: bool = False
|
||||||
|
):
|
||||||
|
message = self.message_converter.yiri2target(message)
|
||||||
|
if quote_origin:
|
||||||
|
# 在前方添加引用组件
|
||||||
|
message.insert(0, nkc.Reply(
|
||||||
|
id=message_source.message_chain.message_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if type(message_source) is mirai.GroupMessage:
|
||||||
|
self.send_message(
|
||||||
|
"group",
|
||||||
|
message_source.sender.group.id,
|
||||||
|
message,
|
||||||
|
converted=True
|
||||||
|
)
|
||||||
|
elif type(message_source) is mirai.FriendMessage:
|
||||||
|
self.send_message(
|
||||||
|
"person",
|
||||||
|
message_source.sender.id,
|
||||||
|
message,
|
||||||
|
converted=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown message source type: " + str(type(message_source)))
|
||||||
|
|
||||||
|
def is_muted(self, group_id: int) -> bool:
|
||||||
|
import time
|
||||||
|
# 检查是否被禁言
|
||||||
|
group_member_info = asyncio.run(self.bot.getGroupMemberInfo(group_id, self.bot_account_id))
|
||||||
|
return group_member_info.shut_up_timestamp > int(time.time())
|
||||||
|
|
||||||
|
def register_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
logging.debug("注册监听器: " + str(event_type) + " -> " + str(callback))
|
||||||
|
|
||||||
|
# 包装函数
|
||||||
|
async def listener_wrapper(app: nakuru.CQHTTP, source: NakuruProjectAdapter.event_converter.yiri2target(event_type)):
|
||||||
|
callback(self.event_converter.target2yiri(source))
|
||||||
|
|
||||||
|
# 将包装函数和原函数的对应关系存入列表
|
||||||
|
self.listener_list.append(
|
||||||
|
{
|
||||||
|
"event_type": event_type,
|
||||||
|
"callable": callback,
|
||||||
|
"wrapper": listener_wrapper,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 注册监听器
|
||||||
|
self.bot.receiver(self.event_converter.yiri2target(event_type).__name__)(listener_wrapper)
|
||||||
|
logging.debug("注册完成")
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def unregister_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
nakuru_event_name = self.event_converter.yiri2target(event_type).__name__
|
||||||
|
|
||||||
|
new_event_list = []
|
||||||
|
|
||||||
|
# 从本对象的监听器列表中查找并删除
|
||||||
|
target_wrapper = None
|
||||||
|
for listener in self.listener_list:
|
||||||
|
if listener["event_type"] == event_type and listener["callable"] == callback:
|
||||||
|
target_wrapper = listener["wrapper"]
|
||||||
|
self.listener_list.remove(listener)
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_wrapper is None:
|
||||||
|
raise Exception("未找到对应的监听器")
|
||||||
|
|
||||||
|
for func in self.bot.event[nakuru_event_name]:
|
||||||
|
if func.callable != target_wrapper:
|
||||||
|
new_event_list.append(func)
|
||||||
|
|
||||||
|
self.bot.event[nakuru_event_name] = new_event_list
|
||||||
|
|
||||||
|
def run_sync(self):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
self.bot.run()
|
||||||
|
|
||||||
|
def kill(self) -> bool:
|
||||||
|
return False
|
||||||
123
pkg/qqbot/sources/yirimirai.py
Normal file
123
pkg/qqbot/sources/yirimirai.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import mirai
|
||||||
|
import mirai.models.bus
|
||||||
|
from mirai.bot import MiraiRunner
|
||||||
|
|
||||||
|
from .. import adapter as adapter_model
|
||||||
|
|
||||||
|
|
||||||
|
class YiriMiraiAdapter(adapter_model.MessageSourceAdapter):
|
||||||
|
"""YiriMirai适配器"""
|
||||||
|
bot: mirai.Mirai
|
||||||
|
|
||||||
|
def __init__(self, config: dict):
|
||||||
|
"""初始化YiriMirai的对象"""
|
||||||
|
if 'adapter' not in config or \
|
||||||
|
config['adapter'] == 'WebSocketAdapter':
|
||||||
|
self.bot = mirai.Mirai(
|
||||||
|
qq=config['qq'],
|
||||||
|
adapter=mirai.WebSocketAdapter(
|
||||||
|
host=config['host'],
|
||||||
|
port=config['port'],
|
||||||
|
verify_key=config['verifyKey']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif config['adapter'] == 'HTTPAdapter':
|
||||||
|
self.bot = mirai.Mirai(
|
||||||
|
qq=config['qq'],
|
||||||
|
adapter=mirai.HTTPAdapter(
|
||||||
|
host=config['host'],
|
||||||
|
port=config['port'],
|
||||||
|
verify_key=config['verifyKey']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown adapter for YiriMirai: ' + config['adapter'])
|
||||||
|
|
||||||
|
def send_message(
|
||||||
|
self,
|
||||||
|
target_type: str,
|
||||||
|
target_id: str,
|
||||||
|
message: mirai.MessageChain
|
||||||
|
):
|
||||||
|
"""发送消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_type (str): 目标类型,`person`或`group`
|
||||||
|
target_id (str): 目标ID
|
||||||
|
message (mirai.MessageChain): YiriMirai库的消息链
|
||||||
|
"""
|
||||||
|
task = None
|
||||||
|
if target_type == 'person':
|
||||||
|
task = self.bot.send_friend_message(int(target_id), message)
|
||||||
|
elif target_type == 'group':
|
||||||
|
task = self.bot.send_group_message(int(target_id), message)
|
||||||
|
else:
|
||||||
|
raise Exception('Unknown target type: ' + target_type)
|
||||||
|
|
||||||
|
asyncio.run(task)
|
||||||
|
|
||||||
|
def reply_message(
|
||||||
|
self,
|
||||||
|
message_source: mirai.MessageEvent,
|
||||||
|
message: mirai.MessageChain,
|
||||||
|
quote_origin: bool = False
|
||||||
|
):
|
||||||
|
"""回复消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_source (mirai.MessageEvent): YiriMirai消息源事件
|
||||||
|
message (mirai.MessageChain): YiriMirai库的消息链
|
||||||
|
quote_origin (bool, optional): 是否引用原消息. Defaults to False.
|
||||||
|
"""
|
||||||
|
asyncio.run(self.bot.send(message_source, message, quote_origin))
|
||||||
|
|
||||||
|
def is_muted(self, group_id: int) -> bool:
|
||||||
|
result = self.bot.member_info(target=group_id, member_id=self.bot.qq).get()
|
||||||
|
result = asyncio.run(result)
|
||||||
|
if result.mute_time_remaining > 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def register_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
"""注册事件监听器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
|
||||||
|
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
|
||||||
|
"""
|
||||||
|
self.bot.on(event_type)(callback)
|
||||||
|
|
||||||
|
def unregister_listener(
|
||||||
|
self,
|
||||||
|
event_type: typing.Type[mirai.Event],
|
||||||
|
callback: typing.Callable[[mirai.Event], None]
|
||||||
|
):
|
||||||
|
"""注销事件监听器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_type (typing.Type[mirai.Event]): YiriMirai事件类型
|
||||||
|
callback (typing.Callable[[mirai.Event], None]): 回调函数,接收一个参数,为YiriMirai事件
|
||||||
|
"""
|
||||||
|
assert isinstance(self.bot, mirai.Mirai)
|
||||||
|
bus = self.bot.bus
|
||||||
|
assert isinstance(bus, mirai.models.bus.ModelEventBus)
|
||||||
|
|
||||||
|
bus.unsubscribe(event_type, callback)
|
||||||
|
|
||||||
|
def run_sync(self):
|
||||||
|
"""运行YiriMirai"""
|
||||||
|
|
||||||
|
# 创建新的
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
|
||||||
|
loop.run_until_complete(MiraiRunner(self.bot)._run())
|
||||||
|
|
||||||
|
def kill(self) -> bool:
|
||||||
|
return False
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .threadctl import ThreadCtl
|
||||||
68
pkg/utils/announcement.py
Normal file
68
pkg/utils/announcement.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def read_latest() -> list:
|
||||||
|
import pkg.utils.network as network
|
||||||
|
resp = requests.get(
|
||||||
|
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement.json",
|
||||||
|
proxies=network.wrapper_proxies()
|
||||||
|
)
|
||||||
|
obj_json = resp.json()
|
||||||
|
b64_content = obj_json["content"]
|
||||||
|
# 解码
|
||||||
|
content = base64.b64decode(b64_content).decode("utf-8")
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
|
||||||
|
def read_saved() -> list:
|
||||||
|
# 已保存的在res/announcement_saved
|
||||||
|
# 检查是否存在
|
||||||
|
if not os.path.exists("res/announcement_saved.json"):
|
||||||
|
with open("res/announcement_saved.json", "w", encoding="utf-8") as f:
|
||||||
|
f.write("[]")
|
||||||
|
|
||||||
|
with open("res/announcement_saved.json", "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
|
||||||
|
def write_saved(content: list):
|
||||||
|
# 已保存的在res/announcement_saved
|
||||||
|
with open("res/announcement_saved.json", "w", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(content, indent=4, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_new() -> list:
|
||||||
|
latest = read_latest()
|
||||||
|
saved = read_saved()
|
||||||
|
|
||||||
|
to_show: list = []
|
||||||
|
|
||||||
|
for item in latest:
|
||||||
|
# 遍历saved检查是否有相同id的公告
|
||||||
|
for saved_item in saved:
|
||||||
|
if saved_item["id"] == item["id"]:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 没有相同id的公告
|
||||||
|
to_show.append(item)
|
||||||
|
|
||||||
|
write_saved(latest)
|
||||||
|
return to_show
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
resp = requests.get(
|
||||||
|
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement.json",
|
||||||
|
)
|
||||||
|
obj_json = resp.json()
|
||||||
|
b64_content = obj_json["content"]
|
||||||
|
# 解码
|
||||||
|
content = base64.b64decode(b64_content).decode("utf-8")
|
||||||
|
print(json.dumps(json.loads(content), indent=4, ensure_ascii=False))
|
||||||
0
pkg/utils/center/__init__.py
Normal file
0
pkg/utils/center/__init__.py
Normal file
88
pkg/utils/center/apigroup.py
Normal file
88
pkg/utils/center/apigroup.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import abc
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
class APIGroup(metaclass=abc.ABCMeta):
|
||||||
|
"""API 组抽象类"""
|
||||||
|
_basic_info: dict = None
|
||||||
|
_runtime_info: dict = None
|
||||||
|
|
||||||
|
prefix = None
|
||||||
|
|
||||||
|
def __init__(self, prefix: str):
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
def do(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
data: dict = None,
|
||||||
|
params: dict = None,
|
||||||
|
headers: dict = {},
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""执行一个请求"""
|
||||||
|
def thr_wrapper(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
data: dict = None,
|
||||||
|
params: dict = None,
|
||||||
|
headers: dict = {},
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
url = self.prefix + path
|
||||||
|
data = json.dumps(data)
|
||||||
|
headers['Content-Type'] = 'application/json'
|
||||||
|
|
||||||
|
ret = requests.request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data=data,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.debug("data: %s", data)
|
||||||
|
|
||||||
|
logging.debug("ret: %s", ret.json())
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug("上报数据失败: %s", e)
|
||||||
|
|
||||||
|
thr = threading.Thread(target=thr_wrapper, args=(
|
||||||
|
self,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
headers,
|
||||||
|
), kwargs=kwargs)
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
|
||||||
|
def gen_rid(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
"""生成一个请求 ID"""
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def basic_info(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
"""获取基本信息"""
|
||||||
|
basic_info = APIGroup._basic_info.copy()
|
||||||
|
basic_info['rid'] = self.gen_rid()
|
||||||
|
return basic_info
|
||||||
|
|
||||||
|
def runtime_info(
|
||||||
|
self
|
||||||
|
):
|
||||||
|
"""获取运行时信息"""
|
||||||
|
return APIGroup._runtime_info
|
||||||
0
pkg/utils/center/groups/__init__.py
Normal file
0
pkg/utils/center/groups/__init__.py
Normal file
55
pkg/utils/center/groups/main.py
Normal file
55
pkg/utils/center/groups/main.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .. import apigroup
|
||||||
|
from ... import context
|
||||||
|
|
||||||
|
|
||||||
|
class V2MainDataAPI(apigroup.APIGroup):
|
||||||
|
"""主程序相关 数据API"""
|
||||||
|
|
||||||
|
def __init__(self, prefix: str):
|
||||||
|
super().__init__(prefix+"/main")
|
||||||
|
|
||||||
|
def do(self, *args, **kwargs):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if not config['report_usage']:
|
||||||
|
return None
|
||||||
|
return super().do(*args, **kwargs)
|
||||||
|
|
||||||
|
def post_update_record(
|
||||||
|
self,
|
||||||
|
spent_seconds: int,
|
||||||
|
infer_reason: str,
|
||||||
|
old_version: str,
|
||||||
|
new_version: str,
|
||||||
|
):
|
||||||
|
"""提交更新记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/update",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"update_info": {
|
||||||
|
"spent_seconds": spent_seconds,
|
||||||
|
"infer_reason": infer_reason,
|
||||||
|
"old_version": old_version,
|
||||||
|
"new_version": new_version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_announcement_showed(
|
||||||
|
self,
|
||||||
|
ids: list[int],
|
||||||
|
):
|
||||||
|
"""提交公告已阅"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/announcement",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"announcement_info": {
|
||||||
|
"ids": ids,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
65
pkg/utils/center/groups/plugin.py
Normal file
65
pkg/utils/center/groups/plugin.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .. import apigroup
|
||||||
|
from ... import context
|
||||||
|
|
||||||
|
|
||||||
|
class V2PluginDataAPI(apigroup.APIGroup):
|
||||||
|
"""插件数据相关 API"""
|
||||||
|
|
||||||
|
def __init__(self, prefix: str):
|
||||||
|
super().__init__(prefix+"/plugin")
|
||||||
|
|
||||||
|
def do(self, *args, **kwargs):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if not config['report_usage']:
|
||||||
|
return None
|
||||||
|
return super().do(*args, **kwargs)
|
||||||
|
|
||||||
|
def post_install_record(
|
||||||
|
self,
|
||||||
|
plugin: dict
|
||||||
|
):
|
||||||
|
"""提交插件安装记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/install",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"plugin": plugin,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_remove_record(
|
||||||
|
self,
|
||||||
|
plugin: dict
|
||||||
|
):
|
||||||
|
"""提交插件卸载记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/remove",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"plugin": plugin,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_update_record(
|
||||||
|
self,
|
||||||
|
plugin: dict,
|
||||||
|
old_version: str,
|
||||||
|
new_version: str,
|
||||||
|
):
|
||||||
|
"""提交插件更新记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/update",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"plugin": plugin,
|
||||||
|
"update_info": {
|
||||||
|
"old_version": old_version,
|
||||||
|
"new_version": new_version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
88
pkg/utils/center/groups/usage.py
Normal file
88
pkg/utils/center/groups/usage.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .. import apigroup
|
||||||
|
from ... import context
|
||||||
|
|
||||||
|
|
||||||
|
class V2UsageDataAPI(apigroup.APIGroup):
|
||||||
|
"""使用量数据相关 API"""
|
||||||
|
|
||||||
|
def __init__(self, prefix: str):
|
||||||
|
super().__init__(prefix+"/usage")
|
||||||
|
|
||||||
|
def do(self, *args, **kwargs):
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
if not config['report_usage']:
|
||||||
|
return None
|
||||||
|
return super().do(*args, **kwargs)
|
||||||
|
|
||||||
|
def post_query_record(
|
||||||
|
self,
|
||||||
|
session_type: str,
|
||||||
|
session_id: str,
|
||||||
|
query_ability_provider: str,
|
||||||
|
usage: int,
|
||||||
|
model_name: str,
|
||||||
|
response_seconds: int,
|
||||||
|
retry_times: int,
|
||||||
|
):
|
||||||
|
"""提交请求记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/query",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"runtime": self.runtime_info(),
|
||||||
|
"session_info": {
|
||||||
|
"type": session_type,
|
||||||
|
"id": session_id,
|
||||||
|
},
|
||||||
|
"query_info": {
|
||||||
|
"ability_provider": query_ability_provider,
|
||||||
|
"usage": usage,
|
||||||
|
"model_name": model_name,
|
||||||
|
"response_seconds": response_seconds,
|
||||||
|
"retry_times": retry_times,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_event_record(
|
||||||
|
self,
|
||||||
|
plugins: list[dict],
|
||||||
|
event_name: str,
|
||||||
|
):
|
||||||
|
"""提交事件触发记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/event",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"runtime": self.runtime_info(),
|
||||||
|
"plugins": plugins,
|
||||||
|
"event_info": {
|
||||||
|
"name": event_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_function_record(
|
||||||
|
self,
|
||||||
|
plugin: dict,
|
||||||
|
function_name: str,
|
||||||
|
function_description: str,
|
||||||
|
):
|
||||||
|
"""提交内容函数使用记录"""
|
||||||
|
return self.do(
|
||||||
|
"POST",
|
||||||
|
"/function",
|
||||||
|
data={
|
||||||
|
"basic": self.basic_info(),
|
||||||
|
"plugin": plugin,
|
||||||
|
"function_info": {
|
||||||
|
"name": function_name,
|
||||||
|
"description": function_description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
35
pkg/utils/center/v2.py
Normal file
35
pkg/utils/center/v2.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from . import apigroup
|
||||||
|
from .groups import main
|
||||||
|
from .groups import usage
|
||||||
|
from .groups import plugin
|
||||||
|
|
||||||
|
|
||||||
|
BACKEND_URL = "https://api.qchatgpt.rockchin.top/api/v2"
|
||||||
|
|
||||||
|
class V2CenterAPI:
|
||||||
|
"""中央服务器 v2 API 交互类"""
|
||||||
|
|
||||||
|
main: main.V2MainDataAPI = None
|
||||||
|
"""主 API 组"""
|
||||||
|
|
||||||
|
usage: usage.V2UsageDataAPI = None
|
||||||
|
"""使用量 API 组"""
|
||||||
|
|
||||||
|
plugin: plugin.V2PluginDataAPI = None
|
||||||
|
"""插件 API 组"""
|
||||||
|
|
||||||
|
def __init__(self, basic_info: dict = None, runtime_info: dict = None):
|
||||||
|
"""初始化"""
|
||||||
|
|
||||||
|
logging.debug("basic_info: %s, runtime_info: %s", basic_info, runtime_info)
|
||||||
|
|
||||||
|
apigroup.APIGroup._basic_info = basic_info
|
||||||
|
apigroup.APIGroup._runtime_info = runtime_info
|
||||||
|
|
||||||
|
self.main = main.V2MainDataAPI(BACKEND_URL)
|
||||||
|
self.usage = usage.V2UsageDataAPI(BACKEND_URL)
|
||||||
|
self.plugin = plugin.V2PluginDataAPI(BACKEND_URL)
|
||||||
5
pkg/utils/constants.py
Normal file
5
pkg/utils/constants.py
Normal file
File diff suppressed because one or more lines are too long
@@ -1,45 +1,130 @@
|
|||||||
import pkg.database.manager
|
from __future__ import annotations
|
||||||
import pkg.openai.manager
|
|
||||||
import pkg.qqbot.manager
|
import threading
|
||||||
|
from . import threadctl
|
||||||
|
|
||||||
|
from ..database import manager as db_mgr
|
||||||
|
from ..openai import manager as openai_mgr
|
||||||
|
from ..qqbot import manager as qqbot_mgr
|
||||||
|
from ..config import manager as config_mgr
|
||||||
|
from ..plugin import host as plugin_host
|
||||||
|
from .center import v2 as center_v2
|
||||||
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'inst': {
|
'inst': {
|
||||||
'database.manager.DatabaseManager': None,
|
'database.manager.DatabaseManager': None,
|
||||||
'openai.manager.OpenAIInteract': None,
|
'openai.manager.OpenAIInteract': None,
|
||||||
'qqbot.manager.QQBotManager': None,
|
'qqbot.manager.QQBotManager': None,
|
||||||
|
'config.manager.ConfigManager': None,
|
||||||
},
|
},
|
||||||
|
'pool_ctl': None,
|
||||||
'logger_handler': None,
|
'logger_handler': None,
|
||||||
'config': None,
|
'config': None,
|
||||||
|
'plugin_host': None,
|
||||||
}
|
}
|
||||||
|
context_lock = threading.Lock()
|
||||||
|
|
||||||
|
### context耦合度非常高,需要大改 ###
|
||||||
def set_config(inst):
|
def set_config(inst):
|
||||||
|
context_lock.acquire()
|
||||||
context['config'] = inst
|
context['config'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def get_config():
|
def get_config():
|
||||||
return context['config']
|
context_lock.acquire()
|
||||||
|
t = context['config']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
def set_database_manager(inst):
|
def set_database_manager(inst: db_mgr.DatabaseManager):
|
||||||
|
context_lock.acquire()
|
||||||
context['inst']['database.manager.DatabaseManager'] = inst
|
context['inst']['database.manager.DatabaseManager'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def get_database_manager():
|
def get_database_manager() -> db_mgr.DatabaseManager:
|
||||||
return context['inst']['database.manager.DatabaseManager']
|
context_lock.acquire()
|
||||||
|
t = context['inst']['database.manager.DatabaseManager']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
def set_openai_manager(inst):
|
def set_openai_manager(inst: openai_mgr.OpenAIInteract):
|
||||||
|
context_lock.acquire()
|
||||||
context['inst']['openai.manager.OpenAIInteract'] = inst
|
context['inst']['openai.manager.OpenAIInteract'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def get_openai_manager():
|
def get_openai_manager() -> openai_mgr.OpenAIInteract:
|
||||||
return context['inst']['openai.manager.OpenAIInteract']
|
context_lock.acquire()
|
||||||
|
t = context['inst']['openai.manager.OpenAIInteract']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
def set_qqbot_manager(inst):
|
def set_qqbot_manager(inst: qqbot_mgr.QQBotManager):
|
||||||
|
context_lock.acquire()
|
||||||
context['inst']['qqbot.manager.QQBotManager'] = inst
|
context['inst']['qqbot.manager.QQBotManager'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def get_qqbot_manager():
|
def get_qqbot_manager() -> qqbot_mgr.QQBotManager:
|
||||||
return context['inst']['qqbot.manager.QQBotManager']
|
context_lock.acquire()
|
||||||
|
t = context['inst']['qqbot.manager.QQBotManager']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def set_config_manager(inst: config_mgr.ConfigManager):
|
||||||
|
context_lock.acquire()
|
||||||
|
context['inst']['config.manager.ConfigManager'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_manager() -> config_mgr.ConfigManager:
|
||||||
|
context_lock.acquire()
|
||||||
|
t = context['inst']['config.manager.ConfigManager']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def set_plugin_host(inst: plugin_host.PluginHost):
|
||||||
|
context_lock.acquire()
|
||||||
|
context['plugin_host'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_plugin_host() -> plugin_host.PluginHost:
|
||||||
|
context_lock.acquire()
|
||||||
|
t = context['plugin_host']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def set_thread_ctl(inst: threadctl.ThreadCtl):
|
||||||
|
context_lock.acquire()
|
||||||
|
context['pool_ctl'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_thread_ctl() -> threadctl.ThreadCtl:
|
||||||
|
context_lock.acquire()
|
||||||
|
t: threadctl.ThreadCtl = context['pool_ctl']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def set_center_v2_api(inst: center_v2.V2CenterAPI):
|
||||||
|
context_lock.acquire()
|
||||||
|
context['center_v2_api'] = inst
|
||||||
|
context_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def get_center_v2_api() -> center_v2.V2CenterAPI:
|
||||||
|
context_lock.acquire()
|
||||||
|
t: center_v2.V2CenterAPI = context['center_v2_api']
|
||||||
|
context_lock.release()
|
||||||
|
return t
|
||||||
67
pkg/utils/log.py
Normal file
67
pkg/utils/log.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from . import context
|
||||||
|
|
||||||
|
|
||||||
|
log_file_name = "qchatgpt.log"
|
||||||
|
|
||||||
|
|
||||||
|
log_colors_config = {
|
||||||
|
'DEBUG': 'green', # cyan white
|
||||||
|
'INFO': 'white',
|
||||||
|
'WARNING': 'yellow',
|
||||||
|
'ERROR': 'red',
|
||||||
|
'CRITICAL': 'cyan',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def init_runtime_log_file():
|
||||||
|
"""为此次运行生成日志文件
|
||||||
|
格式: qchatgpt-yyyy-MM-dd-HH-mm-ss.log
|
||||||
|
"""
|
||||||
|
global log_file_name
|
||||||
|
|
||||||
|
# 检查logs目录是否存在
|
||||||
|
if not os.path.exists("logs"):
|
||||||
|
os.mkdir("logs")
|
||||||
|
|
||||||
|
log_file_name = "logs/qchatgpt-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
|
||||||
|
|
||||||
|
|
||||||
|
def reset_logging():
|
||||||
|
global log_file_name
|
||||||
|
|
||||||
|
import pkg.utils.context
|
||||||
|
import colorlog
|
||||||
|
|
||||||
|
if pkg.utils.context.context['logger_handler'] is not None:
|
||||||
|
logging.getLogger().removeHandler(pkg.utils.context.context['logger_handler'])
|
||||||
|
|
||||||
|
for handler in logging.getLogger().handlers:
|
||||||
|
logging.getLogger().removeHandler(handler)
|
||||||
|
|
||||||
|
config_mgr = context.get_config_manager()
|
||||||
|
|
||||||
|
logging_level = logging.INFO if config_mgr is None else config_mgr.data['logging_level']
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging_level, # 设置日志输出格式
|
||||||
|
filename=log_file_name, # log日志输出的文件位置和文件名
|
||||||
|
format="[%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s",
|
||||||
|
# 日志输出的格式
|
||||||
|
# -8表示占位符,让输出左对齐,输出长度都为8位
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
||||||
|
)
|
||||||
|
sh = logging.StreamHandler()
|
||||||
|
sh.setLevel(logging_level)
|
||||||
|
sh.setFormatter(colorlog.ColoredFormatter(
|
||||||
|
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
||||||
|
"%(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
log_colors=log_colors_config
|
||||||
|
))
|
||||||
|
logging.getLogger().addHandler(sh)
|
||||||
|
pkg.utils.context.context['logger_handler'] = sh
|
||||||
|
return sh
|
||||||
11
pkg/utils/network.py
Normal file
11
pkg/utils/network.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from . import context
|
||||||
|
|
||||||
|
|
||||||
|
def wrapper_proxies() -> dict:
|
||||||
|
"""获取代理"""
|
||||||
|
config = context.get_config_manager().data
|
||||||
|
|
||||||
|
return {
|
||||||
|
"http": config['openai_config']['proxy'],
|
||||||
|
"https": config['openai_config']['proxy']
|
||||||
|
} if 'proxy' in config['openai_config'] and (config['openai_config']['proxy'] is not None) else None
|
||||||
46
pkg/utils/pkgmgr.py
Normal file
46
pkg/utils/pkgmgr.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from pip._internal import main as pipmain
|
||||||
|
|
||||||
|
from . import log
|
||||||
|
|
||||||
|
|
||||||
|
def install(package):
|
||||||
|
pipmain(['install', package])
|
||||||
|
log.reset_logging()
|
||||||
|
|
||||||
|
def install_upgrade(package):
|
||||||
|
pipmain(['install', '--upgrade', package, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||||
|
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
|
||||||
|
log.reset_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def run_pip(params: list):
|
||||||
|
pipmain(params)
|
||||||
|
log.reset_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def install_requirements(file):
|
||||||
|
pipmain(['install', '-r', file, "-i", "https://pypi.tuna.tsinghua.edu.cn/simple",
|
||||||
|
"--trusted-host", "pypi.tuna.tsinghua.edu.cn"])
|
||||||
|
log.reset_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dulwich():
|
||||||
|
# 尝试三次
|
||||||
|
for i in range(3):
|
||||||
|
try:
|
||||||
|
import dulwich
|
||||||
|
return
|
||||||
|
except ImportError:
|
||||||
|
install('dulwich')
|
||||||
|
|
||||||
|
raise ImportError("无法自动安装dulwich库")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
install("openai11")
|
||||||
|
except Exception as e:
|
||||||
|
print(111)
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
print(222)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user