mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
Compare commits
682 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0984c19fd9 | ||
|
|
a10d3213fd | ||
|
|
f52a0eb02f | ||
|
|
1ea8da69a2 | ||
|
|
5bbc38a7a3 | ||
|
|
aa433bd5ab | ||
|
|
2c5933da0b | ||
|
|
77bc6fbf59 | ||
|
|
701cb7be40 | ||
|
|
ab8d77c968 | ||
|
|
6c03fe678a | ||
|
|
41b30238c3 | ||
|
|
aa768459c0 | ||
|
|
28014512f7 | ||
|
|
f9a99eed66 | ||
|
|
461b574e09 | ||
|
|
36c192ff6b | ||
|
|
101625965c | ||
|
|
83177a3416 | ||
|
|
c3904786e1 | ||
|
|
b31c34905a | ||
|
|
41cbe91870 | ||
|
|
872b16b779 | ||
|
|
9f3cc9c293 | ||
|
|
2d148c4970 | ||
|
|
0869b57741 | ||
|
|
af225aa18f | ||
|
|
06f3c5d32b | ||
|
|
4e71a08b57 | ||
|
|
bf5ebc9245 | ||
|
|
fba81582ab | ||
|
|
b4645168f9 | ||
|
|
d00c68e329 | ||
|
|
cb636b96bf | ||
|
|
12468b5b15 | ||
|
|
6a5414b5fd | ||
|
|
db51fd0ad7 | ||
|
|
256bc4dc1e | ||
|
|
d2bd6e23b6 | ||
|
|
bb12b48887 | ||
|
|
a58e55daf3 | ||
|
|
23a05fe5b0 | ||
|
|
3a63630068 | ||
|
|
565066bbcd | ||
|
|
c10f72cf4c | ||
|
|
af8c21f3d4 | ||
|
|
6f6c3af302 | ||
|
|
61a47808c8 | ||
|
|
e02765bf95 | ||
|
|
b69f193a3e | ||
|
|
7c6526d1ea | ||
|
|
b8776fba65 | ||
|
|
38357dd68d | ||
|
|
d1c2453310 | ||
|
|
ebc1ac50c6 | ||
|
|
892610872f | ||
|
|
a990a40850 | ||
|
|
3f29464dbd | ||
|
|
998d07f3b4 | ||
|
|
949bc6268c | ||
|
|
2c03e5a77e | ||
|
|
aad62dfa6f | ||
|
|
08e27d07ea | ||
|
|
1fddd244e5 | ||
|
|
d85b4b1cf0 | ||
|
|
09fca2c292 | ||
|
|
feda3d18fb | ||
|
|
eb6e5d0756 | ||
|
|
7386daad28 | ||
|
|
3f290b2e1a | ||
|
|
43519ffe80 | ||
|
|
c8bb3d612a | ||
|
|
bc48b7e623 | ||
|
|
d59d5797f6 | ||
|
|
11d3c1e650 | ||
|
|
8cfd9e6694 | ||
|
|
d3f401c54d | ||
|
|
a889170d1a | ||
|
|
459e9f9322 | ||
|
|
707afdcdf9 | ||
|
|
ad1cf379c4 | ||
|
|
582277fe2d | ||
|
|
14b9f814c7 | ||
|
|
b11e5d99b0 | ||
|
|
9590718da4 | ||
|
|
8c2b53cffb | ||
|
|
5a85c073a8 | ||
|
|
2d2fbd0a8b | ||
|
|
1b25a05122 | ||
|
|
709cc1140b | ||
|
|
1730962636 | ||
|
|
a1de4f6f7a | ||
|
|
a5ccda5ed6 | ||
|
|
f035e654ba | ||
|
|
151d3e9f66 | ||
|
|
c79207e197 | ||
|
|
f9d461d9a1 | ||
|
|
3e17bbb90f | ||
|
|
549a7eff7f | ||
|
|
db2e366014 | ||
|
|
26e4215054 | ||
|
|
5f07ff8145 | ||
|
|
e396ba4649 | ||
|
|
d1dff6dedd | ||
|
|
419354cb07 | ||
|
|
7708eaa82c | ||
|
|
9fccf84987 | ||
|
|
0f59788184 | ||
|
|
0ad52bcd3f | ||
|
|
d7d710ec07 | ||
|
|
75a9a3e9af | ||
|
|
70503bedb7 | ||
|
|
7890eac3f8 | ||
|
|
e15f3595b3 | ||
|
|
eebd6a6ba3 | ||
|
|
0407f3e4ac | ||
|
|
5abca84437 | ||
|
|
d2776cc1e6 | ||
|
|
9fe0ee2b77 | ||
|
|
b68daac323 | ||
|
|
665de5dc43 | ||
|
|
e3b280758c | ||
|
|
374ae25d9c | ||
|
|
c86529ac99 | ||
|
|
6309f1fb78 | ||
|
|
c246fb6d8e | ||
|
|
ec6c041bcf | ||
|
|
2da5a9f3c7 | ||
|
|
4e0df52d7c | ||
|
|
71b8bf13e4 | ||
|
|
a8b1e6ce91 | ||
|
|
1419d7611d | ||
|
|
89c83ebf20 | ||
|
|
76d7db88ea | ||
|
|
67a208bc90 | ||
|
|
acbd55ded2 | ||
|
|
11a240a6d1 | ||
|
|
97c85abbe7 | ||
|
|
06a0cd2a3d | ||
|
|
572b215df8 | ||
|
|
2c542bf412 | ||
|
|
1576ba7a01 | ||
|
|
45e4096a12 | ||
|
|
8a1d4fe287 | ||
|
|
98f880ebc2 | ||
|
|
2b852853f3 | ||
|
|
c7a9988033 | ||
|
|
c475eebe1c | ||
|
|
0fe7355ae0 | ||
|
|
57de96e3a2 | ||
|
|
70571cef50 | ||
|
|
0b6deb3340 | ||
|
|
dcda85a825 | ||
|
|
9d3bff018b | ||
|
|
051376e0d2 | ||
|
|
a113785211 | ||
|
|
3f4ed4dc3c | ||
|
|
ac80764fae | ||
|
|
e43afd4891 | ||
|
|
f1aea1d495 | ||
|
|
0e2a5db104 | ||
|
|
3a4c9771fa | ||
|
|
f4f8ef9523 | ||
|
|
b9ace69a72 | ||
|
|
aef0b2a26e | ||
|
|
f7712d71ec | ||
|
|
e94b44e3b8 | ||
|
|
524e863c78 | ||
|
|
bbc80ac901 | ||
|
|
f969ddd6ca | ||
|
|
1cc9781333 | ||
|
|
a609801bae | ||
|
|
d8b606d372 | ||
|
|
572a440e65 | ||
|
|
6e4eeae9b7 | ||
|
|
1a73669df8 | ||
|
|
91ebaf1122 | ||
|
|
46703eb906 | ||
|
|
b9dd9d5193 | ||
|
|
884481a4ec | ||
|
|
9040b37a63 | ||
|
|
99d47b2fa2 | ||
|
|
6575359a94 | ||
|
|
a2fc726372 | ||
|
|
3bfce8ab51 | ||
|
|
ff9a9830f2 | ||
|
|
e2b59e8efe | ||
|
|
04dad9757f | ||
|
|
75ea1080ad | ||
|
|
e25b064319 | ||
|
|
5d0dbc40ce | ||
|
|
beae8de5eb | ||
|
|
c4ff30c722 | ||
|
|
6f4ecb101b | ||
|
|
9f9b0ef846 | ||
|
|
de6957062c | ||
|
|
0a9b43e6fa | ||
|
|
5b0edd9937 | ||
|
|
8a400d202a | ||
|
|
5a1e9f7fb2 | ||
|
|
e03af75cf8 | ||
|
|
0da4919255 | ||
|
|
914e566d1f | ||
|
|
6ec2b653fe | ||
|
|
ba0a088b9c | ||
|
|
478e83bcd9 | ||
|
|
386124a3b9 | ||
|
|
ff5e7c16d1 | ||
|
|
7ff7a66012 | ||
|
|
c99dfb8a86 | ||
|
|
10f9d4c6b3 | ||
|
|
d347813411 | ||
|
|
7a93898b3f | ||
|
|
c057ea900f | ||
|
|
512266e74f | ||
|
|
e36aee11c7 | ||
|
|
97421299f5 | ||
|
|
bc41e5aa80 | ||
|
|
2fa30e7def | ||
|
|
1c6a7d9ba5 | ||
|
|
47435c42a5 | ||
|
|
39a1b421e6 | ||
|
|
b5edf2295b | ||
|
|
fb650a3d7a | ||
|
|
521541f311 | ||
|
|
7020abadbf | ||
|
|
d95fb3b5be | ||
|
|
3e524dc790 | ||
|
|
a64940bff8 | ||
|
|
c739290f0b | ||
|
|
af292fe050 | ||
|
|
634c7fb302 | ||
|
|
33efb94013 | ||
|
|
549e4dc02e | ||
|
|
3d40909c02 | ||
|
|
1aef81e38f | ||
|
|
1b0ae8da58 | ||
|
|
7979a8e97f | ||
|
|
080e53d9a9 | ||
|
|
89bb364b16 | ||
|
|
3586cd941f | ||
|
|
054d0839ac | ||
|
|
dd75f98d85 | ||
|
|
ec23bb5268 | ||
|
|
bc99db4fc1 | ||
|
|
c8275fcfbf | ||
|
|
a345043c30 | ||
|
|
382d37d479 | ||
|
|
32c144a75d | ||
|
|
7ca2aa5e39 | ||
|
|
86cc4a23ac | ||
|
|
08d1e138bd | ||
|
|
a9fe86542f | ||
|
|
4e29776fcd | ||
|
|
ee3eae8f4d | ||
|
|
a84575858a | ||
|
|
ac472291c7 | ||
|
|
f304873c6a | ||
|
|
18caf8face | ||
|
|
d21115aaa8 | ||
|
|
a05ecd2e7f | ||
|
|
32a725126d | ||
|
|
0528690622 | ||
|
|
819339142e | ||
|
|
1d0573e7ff | ||
|
|
00623bc431 | ||
|
|
c872264456 | ||
|
|
1336d3cb9a | ||
|
|
d1459578cd | ||
|
|
8a67fcf40f | ||
|
|
7930370aa9 | ||
|
|
0b854bdcf1 | ||
|
|
cba6aab48d | ||
|
|
12a9ca7a77 | ||
|
|
a6cbd226e1 | ||
|
|
3577e62b41 | ||
|
|
f86e69fcd1 | ||
|
|
292e00b078 | ||
|
|
2a91497bcf | ||
|
|
b0cca0a4c2 | ||
|
|
a2bda85a9c | ||
|
|
20677cff86 | ||
|
|
c8af5d8445 | ||
|
|
2dbe984539 | ||
|
|
6b8fa664f1 | ||
|
|
2b9612e933 | ||
|
|
749d0219fb | ||
|
|
a11a152bd7 | ||
|
|
fc803a3742 | ||
|
|
13a1e15f24 | ||
|
|
3f41b94da5 | ||
|
|
0fb5bfda20 | ||
|
|
dc1fd73ebb | ||
|
|
161b694f71 | ||
|
|
45d1c89e45 | ||
|
|
e26664aa51 | ||
|
|
e29691efbd | ||
|
|
6d45327882 | ||
|
|
fbd41eef49 | ||
|
|
0a30c88322 | ||
|
|
4f5af0e8c8 | ||
|
|
df3f0fd159 | ||
|
|
f2493c79dd | ||
|
|
a86a035b6b | ||
|
|
7995793bfd | ||
|
|
a56b340646 | ||
|
|
7473cdfe16 | ||
|
|
24273ac158 | ||
|
|
fe6275000e | ||
|
|
5fbf369f82 | ||
|
|
4400475ffa | ||
|
|
796eb7c95d | ||
|
|
89a01378e7 | ||
|
|
f4735e5e30 | ||
|
|
f1bb3045aa | ||
|
|
96e474a555 | ||
|
|
833d29b101 | ||
|
|
dce6734ba2 | ||
|
|
0481167dc6 | ||
|
|
a002f93f7b | ||
|
|
3c894fe70e | ||
|
|
8c69b8a1d9 | ||
|
|
a9dae05303 | ||
|
|
ae6994e241 | ||
|
|
caa72fa40c | ||
|
|
46cc9220c3 | ||
|
|
ddb56d7a8e | ||
|
|
a0267416d7 | ||
|
|
56e1ef3602 | ||
|
|
b4fc1057d1 | ||
|
|
06037df607 | ||
|
|
dce134d08d | ||
|
|
cca471d068 | ||
|
|
ddb211b74a | ||
|
|
cef70751ff | ||
|
|
2d2219fc6e | ||
|
|
514a6b4192 | ||
|
|
7a552b3434 | ||
|
|
ecebd1b0e0 | ||
|
|
8dc34d2a88 | ||
|
|
d52644ceec | ||
|
|
3052510591 | ||
|
|
777a5617db | ||
|
|
e17c1087e9 | ||
|
|
633695175a | ||
|
|
9e78bf3d21 | ||
|
|
43aa68a55d | ||
|
|
b8308f8c57 | ||
|
|
466bfbddeb | ||
|
|
b6da07b225 | ||
|
|
2f2159239a | ||
|
|
67d1ca8a65 | ||
|
|
497a393e83 | ||
|
|
782c0e22ea | ||
|
|
2932fc6dfd | ||
|
|
0a9eab2113 | ||
|
|
50a673a8ec | ||
|
|
9e25d0f9e4 | ||
|
|
23cd7be711 | ||
|
|
025b9e33f1 | ||
|
|
bab2f64913 | ||
|
|
b00e09aa9c | ||
|
|
0b109fdc7a | ||
|
|
018fea2ddb | ||
|
|
f8a3cc4352 | ||
|
|
6ab853acc1 | ||
|
|
e825dea02f | ||
|
|
cf8740d16e | ||
|
|
9c4809e26f | ||
|
|
0a232fd9ef | ||
|
|
23016a0791 | ||
|
|
cdcc67ff23 | ||
|
|
92274bfc34 | ||
|
|
2fed6f61ba | ||
|
|
59b2cd26d2 | ||
|
|
f7b87e99d2 | ||
|
|
70bc985145 | ||
|
|
070dbe9108 | ||
|
|
a63fa6d955 | ||
|
|
c7703809b0 | ||
|
|
37eb74338f | ||
|
|
77d5585b7c | ||
|
|
6cab3ef029 | ||
|
|
820a7b78fc | ||
|
|
c51dffef3a | ||
|
|
983bc3da3c | ||
|
|
09be956a58 | ||
|
|
5eded50c53 | ||
|
|
6d8eebd314 | ||
|
|
19a0572b5f | ||
|
|
6272e98474 | ||
|
|
45042fe7d4 | ||
|
|
d85e840126 | ||
|
|
804889f1de | ||
|
|
919c996434 | ||
|
|
00823b3d62 | ||
|
|
af54efd24a | ||
|
|
b1c9b121f6 | ||
|
|
7b5649d153 | ||
|
|
52bf716d84 | ||
|
|
c149dd7b66 | ||
|
|
65d5a1ed63 | ||
|
|
5516754bbb | ||
|
|
08082f2ee3 | ||
|
|
8489266080 | ||
|
|
51c7e0b235 | ||
|
|
628b6b0bb4 | ||
|
|
7e024d860d | ||
|
|
c2f6273f70 | ||
|
|
96e401ec7b | ||
|
|
ae8ac65447 | ||
|
|
2d4f59f36e | ||
|
|
0e85467e02 | ||
|
|
eb41cf5481 | ||
|
|
b970a42d07 | ||
|
|
8c9d123e1c | ||
|
|
ab2a95e347 | ||
|
|
2184c558a4 | ||
|
|
83cb8588fd | ||
|
|
007e82c533 | ||
|
|
499f8580a7 | ||
|
|
a7dc3c5dab | ||
|
|
d01d3a3c53 | ||
|
|
580e062dbf | ||
|
|
c8cee8410c | ||
|
|
6bf331c2e3 | ||
|
|
4c4930737c | ||
|
|
9de01e9525 | ||
|
|
c6a16f5974 | ||
|
|
253ef44d17 | ||
|
|
15a1f00b73 | ||
|
|
b5fa2ea8b8 | ||
|
|
449e024771 | ||
|
|
1bee7a146b | ||
|
|
270a632789 | ||
|
|
418bb05b4c | ||
|
|
052b834151 | ||
|
|
58ee204a75 | ||
|
|
0a02ee8c04 | ||
|
|
950ef4a181 | ||
|
|
7b7cdd8adb | ||
|
|
471768e760 | ||
|
|
c7517d31a4 | ||
|
|
7d10d0398e | ||
|
|
a2bc25c08b | ||
|
|
3cb49fe2d8 | ||
|
|
5b96ac122f | ||
|
|
612033f478 | ||
|
|
48ee940d8e | ||
|
|
e74df0b37d | ||
|
|
640afdc49c | ||
|
|
6b39df5b9b | ||
|
|
e7e698765e | ||
|
|
43fea13dab | ||
|
|
bc899e5bd0 | ||
|
|
160086feb9 | ||
|
|
016391c976 | ||
|
|
91746448a3 | ||
|
|
5cb0543237 | ||
|
|
fac29a24a8 | ||
|
|
4d3a2a21d0 | ||
|
|
6d4f88041c | ||
|
|
18587d3690 | ||
|
|
423090dccd | ||
|
|
78e88baab3 | ||
|
|
6a276767b3 | ||
|
|
2cb26c7c70 | ||
|
|
ff66c88060 | ||
|
|
611e82b8f9 | ||
|
|
59bdee7137 | ||
|
|
e8dbd426ae | ||
|
|
40d6e809a0 | ||
|
|
236c540d18 | ||
|
|
d6ca059f6c | ||
|
|
52c06a60ca | ||
|
|
6353644ec3 | ||
|
|
20df9ded3d | ||
|
|
7569b18a4c | ||
|
|
b9da4f4951 | ||
|
|
89b9e29257 | ||
|
|
d605de9de4 | ||
|
|
d46c94d7c3 | ||
|
|
2db9c00530 | ||
|
|
66d8d159f9 | ||
|
|
9fa1446284 | ||
|
|
b3e4cb48c7 | ||
|
|
0bca7b2247 | ||
|
|
7812e03c9d | ||
|
|
7a852ae5af | ||
|
|
706d9e61c1 | ||
|
|
8f0ed4ff4b | ||
|
|
3415b6f121 | ||
|
|
256ba6fb86 | ||
|
|
d30b2b9afe | ||
|
|
be943ca1fc | ||
|
|
1ddab2a97a | ||
|
|
e15fd4695c | ||
|
|
ffa4b1b4a1 | ||
|
|
f8eee3a2a6 | ||
|
|
eeee7a8343 | ||
|
|
8447b73fcb | ||
|
|
2863945d5f | ||
|
|
cb1f8ca6f7 | ||
|
|
1d9964bcb1 | ||
|
|
15cb8016d3 | ||
|
|
895cc0a2c5 | ||
|
|
20bf349e4e | ||
|
|
e297763da1 | ||
|
|
e471970654 | ||
|
|
12faaaced8 | ||
|
|
083cbc55cc | ||
|
|
8aa7a3273d | ||
|
|
255e2c4385 | ||
|
|
9856306870 | ||
|
|
527ab8b8a7 | ||
|
|
f8e19ba9b3 | ||
|
|
7649dbfbbc | ||
|
|
81e734644d | ||
|
|
ae55cf5b1e | ||
|
|
af539546ef | ||
|
|
0031ce57d0 | ||
|
|
2f48a2ce57 | ||
|
|
6068ab7100 | ||
|
|
29a7dccef4 | ||
|
|
e2073da86e | ||
|
|
ae079526f7 | ||
|
|
947bae8e26 | ||
|
|
a68e29dff6 | ||
|
|
a588d7f960 | ||
|
|
66224e5a32 | ||
|
|
07abad6a14 | ||
|
|
83d02aaaac | ||
|
|
5a27ac165e | ||
|
|
bd9a523233 | ||
|
|
43959b158f | ||
|
|
d81b457bba | ||
|
|
b40d639785 | ||
|
|
0a8d8f4f66 | ||
|
|
d16cb25cde | ||
|
|
7aef1758e0 | ||
|
|
9758756fdd | ||
|
|
13ef35f96f | ||
|
|
6b8c1209b7 | ||
|
|
7184f3053a | ||
|
|
b83eac10e6 | ||
|
|
cb42eaef69 | ||
|
|
0dfd636a7e | ||
|
|
21ff0fd258 | ||
|
|
c2eaeb2c72 | ||
|
|
2a414a4bea | ||
|
|
fc0c38c8af | ||
|
|
595e6c8a0c | ||
|
|
ced16fd221 | ||
|
|
0817c3f148 | ||
|
|
fb40af81ac | ||
|
|
1c5ad05e89 | ||
|
|
86bef566c4 | ||
|
|
0983ccb61e | ||
|
|
a1d9f469c0 | ||
|
|
952124f783 | ||
|
|
6be12e8ace | ||
|
|
0799f380e1 | ||
|
|
f65270ee7e | ||
|
|
414910719c | ||
|
|
10a1e8faa6 | ||
|
|
4eea21927e | ||
|
|
48c7f659f9 | ||
|
|
b33333f4aa | ||
|
|
9edb32b081 | ||
|
|
c9b25fe806 | ||
|
|
b6ee3939be | ||
|
|
e5485cddd0 | ||
|
|
ac81597236 | ||
|
|
58d991df0a | ||
|
|
3f8e380da4 | ||
|
|
ae831a2654 | ||
|
|
ae72cf2283 | ||
|
|
8164f4b506 | ||
|
|
9617be0ca4 | ||
|
|
f079d7b9fa | ||
|
|
00afda452f | ||
|
|
70386abadd | ||
|
|
5865ac017c | ||
|
|
4061a92f8e | ||
|
|
d37c31b31c | ||
|
|
973ef0078f | ||
|
|
48dcd257da | ||
|
|
da03911610 | ||
|
|
aba9d945b5 | ||
|
|
b6f7f3b73f | ||
|
|
2050d20ea7 | ||
|
|
ac1fb4a63a | ||
|
|
ced38490e1 | ||
|
|
ad28b69198 | ||
|
|
8c67d3c58f | ||
|
|
7171817de8 | ||
|
|
73f9d674e1 | ||
|
|
5e046399f8 | ||
|
|
4966cd9ac7 | ||
|
|
da936ecfe3 | ||
|
|
89e10d43de | ||
|
|
3bf289af69 | ||
|
|
c7c9a6c5ca | ||
|
|
aee8446a23 | ||
|
|
2bb4f1fbb8 | ||
|
|
6e7b0ee4ff | ||
|
|
204f5b9a54 | ||
|
|
8c41e3506f | ||
|
|
c2c33e45b8 | ||
|
|
1acaf4e58b | ||
|
|
eca80d5a4c | ||
|
|
f538957be9 | ||
|
|
82a839a60a | ||
|
|
df494da9e4 | ||
|
|
1ea53f7f04 | ||
|
|
ac6d695f6d | ||
|
|
73dccb21f5 | ||
|
|
4221102ad5 | ||
|
|
b100f12e7f | ||
|
|
2069ba6836 | ||
|
|
ea57976808 | ||
|
|
4055d3542b | ||
|
|
0b0271a1f4 | ||
|
|
e03585ad4d | ||
|
|
11a385791e | ||
|
|
e228225178 | ||
|
|
1c96d971e1 | ||
|
|
b799de7995 | ||
|
|
b01d246555 | ||
|
|
9363b073cf | ||
|
|
12ca04ac6f | ||
|
|
51737c28bd | ||
|
|
50d5ec224a | ||
|
|
95a7397d14 | ||
|
|
aedac6d22c | ||
|
|
d522975ecc | ||
|
|
68fda8d7f3 | ||
|
|
b0cfec9913 | ||
|
|
ba8eba1581 | ||
|
|
f9eaed41c1 | ||
|
|
1202a62df7 | ||
|
|
8c1f7796f6 | ||
|
|
42aee35789 | ||
|
|
b628849caa | ||
|
|
031f08b0d4 | ||
|
|
fab6f9b93f | ||
|
|
564c5d937d | ||
|
|
2d3bb01487 | ||
|
|
607ea2d293 | ||
|
|
d817b53780 | ||
|
|
e8a2cbe06a | ||
|
|
d2b0577752 | ||
|
|
b4edd5cbad | ||
|
|
348477747e | ||
|
|
bb7ee174ea | ||
|
|
ab5add14ef | ||
|
|
44f4820cee | ||
|
|
8f1609b944 | ||
|
|
66b5b75631 | ||
|
|
17e293afe8 | ||
|
|
1cf35f59fd | ||
|
|
bb4b897934 | ||
|
|
0eaf1af2e3 | ||
|
|
f70c12540b | ||
|
|
479fe73c24 | ||
|
|
f6cad85476 | ||
|
|
888197e6ce | ||
|
|
e634305759 | ||
|
|
fe054211f4 | ||
|
|
f102a29ea0 | ||
|
|
2b8bd45bcd | ||
|
|
7f730c4be0 | ||
|
|
b6e31cac23 | ||
|
|
9fe4f218d5 | ||
|
|
cc38cc2676 | ||
|
|
f56c6876d1 | ||
|
|
196e424c88 | ||
|
|
cf678aa345 | ||
|
|
d1549b3df0 | ||
|
|
3aca987176 | ||
|
|
e0caeb5dd2 | ||
|
|
77076f3bdd |
@@ -1,34 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "QChatGPT 3.10",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/python:0-3.10",
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"repositories": {
|
||||
"RockChinQ/QChatGPT": {
|
||||
"permissions": "write-all"
|
||||
},
|
||||
"RockChinQ/revLibs": {
|
||||
"permissions": "write-all"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
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
|
||||
|
||||
24
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
24
.github/ISSUE_TEMPLATE/漏洞反馈.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: 漏洞反馈
|
||||
about: 报错或漏洞请使用这个模板创建,不使用此模板创建的异常、漏洞相关issue将被直接关闭
|
||||
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: ''
|
||||
|
||||
---
|
||||
|
||||
不是需求建议请勿填写此模板!!!!
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -10,6 +10,6 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allow:
|
||||
- dependency-name: "yiri-mirai"
|
||||
- dependency-name: "yiri-mirai-rc"
|
||||
- dependency-name: "dulwich"
|
||||
- dependency-name: "openai"
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
### 事务
|
||||
|
||||
- [ ] 已阅读仓库[贡献指引](../CONTRIBUTING.md)
|
||||
- [ ] 已阅读仓库[贡献指引](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
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
config.py
|
||||
/config.py
|
||||
.idea/
|
||||
__pycache__/
|
||||
database.db
|
||||
@@ -16,4 +16,20 @@ scenario/
|
||||
!scenario/default-template.json
|
||||
override.json
|
||||
cookies.json
|
||||
res/announcement_saved
|
||||
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
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "QChatGPT.wiki"]
|
||||
path = QChatGPT.wiki
|
||||
url = https://github.com/RockChinQ/QChatGPT.wiki.git
|
||||
@@ -17,3 +17,10 @@
|
||||
- 解决本项目或衍生项目的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" ]
|
||||
Submodule QChatGPT.wiki deleted from 68c4ef5d24
309
README.md
309
README.md
@@ -1,260 +1,51 @@
|
||||
# QChatGPT🤖
|
||||
|
||||
> 2023/3/18 现已支持GPT-4 API(内测),请查看`config-template.py`中的`completion_api_params`
|
||||
> 2023/3/15 逆向库已支持New Bing,使用方法查看[插件文档](https://github.com/RockChinQ/revLibs)
|
||||
> 2023/3/15 逆向库已支持GPT-4模型,使用方法查看[插件](https://github.com/RockChinQ/revLibs)
|
||||
> 2023/3/3 现已在主线支持官方ChatGPT接口,使用方法查看[#195](https://github.com/RockChinQ/QChatGPT/issues/195)
|
||||
|
||||
- 到[项目Wiki](https://github.com/RockChinQ/QChatGPT/wiki)可了解项目详细信息
|
||||
- ~~由bilibili TheLazy制作的[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)~~(寄了,求大佬做个新的)
|
||||
- 交流、答疑群: ~~204785790~~(已满)、~~691226829~~(已满)、656285629
|
||||
- **进群提问前请您`确保`已经找遍文档和issue均无法解决**
|
||||
- QQ频道机器人见[QQChannelChatGPT](https://github.com/Soulter/QQChannelChatGPT)
|
||||
|
||||
通过调用OpenAI的ChatGPT等语言模型来实现一个更加智能的QQ机器人
|
||||
|
||||
## 🍺模型适配一览
|
||||
|
||||
<details>
|
||||
<summary>点击此处展开</summary>
|
||||
|
||||
### 文字对话
|
||||
|
||||
- OpenAI GPT-3.5模型(ChatGPT API), 本项目原生支持, 默认使用
|
||||
- OpenAI GPT-3模型, 本项目原生支持, 部署完成后前往`config.py`切换
|
||||
- OpenAI GPT-4模型, 本项目原生支持, 目前需要您的账户通过OpenAI的内测申请, 请前往`config.py`切换
|
||||
- ChatGPT网页版GPT-3.5模型, 由[插件](https://github.com/RockChinQ/revLibs)接入
|
||||
- ChatGPT网页版GPT-4模型, 目前需要ChatGPT Plus订阅, 由[插件](https://github.com/RockChinQ/revLibs)接入
|
||||
- New Bing逆向库, 由[插件](https://github.com/RockChinQ/revLibs)接入
|
||||
|
||||
### 故事续写
|
||||
|
||||
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入
|
||||
|
||||
### 图片绘制
|
||||
|
||||
- OpenAI DALL·E模型, 本项目原生支持, 使用方法查看[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)
|
||||
- NovelAI API, 由[插件](https://github.com/dominoar/QCPNovelAi)接入
|
||||
|
||||
### 语音生成
|
||||
|
||||
- TTS+VITS, 由[插件](https://github.com/dominoar/QChatPlugins)接入
|
||||
- Plachta/VITS-Umamusume-voice-synthesizer, 由[插件](https://github.com/oliverkirk-sudo/chat_voice)接入
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## ✅功能
|
||||
|
||||
<details>
|
||||
<summary>✅支持敏感词过滤,避免账号风险</summary>
|
||||
|
||||
- 难以监测机器人与用户对话时的内容,故引入此功能以减少机器人风险
|
||||
- 加入了百度云内容审核,在`config.py`中修改`baidu_check`的值,并填写`baidu_api_key`和`baidu_secret_key`以开启此功能
|
||||
- 编辑`sensitive.json`,并在`config.py`中修改`sensitive_word_filter`的值以开启此功能
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅群内多种响应规则,不必at</summary>
|
||||
|
||||
- 默认回复`ai`作为前缀或`@`机器人的消息
|
||||
- 详细见`config.py`中的`response_rules`字段
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅完善的多api-key管理,超额自动切换</summary>
|
||||
|
||||
- 支持配置多个`api-key`,内部统计使用量并在超额时自动切换
|
||||
- 请在`config.py`中修改`openai_config`的值以设置`api-key`
|
||||
- 可以在`config.py`中修改`api_key_fee_threshold`来自定义切换阈值
|
||||
- 运行期间向机器人说`!usage`以查看当前使用情况
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅支持预设指令文字</summary>
|
||||
|
||||
- 支持以自然语言预设文字,自定义机器人人格等信息
|
||||
- 详见`config.py`中的`default_prompt`部分
|
||||
- 支持设置多个预设情景,并通过!reset、!default等指令控制,详细请查看[wiki指令](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)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>✅支持对话、绘图等模型,可玩性更高</summary>
|
||||
|
||||
- 现已支持OpenAI的对话`Completion API`和绘图`Image API`
|
||||
- 向机器人发送指令`!draw <prompt>`即可使用绘图模型
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅支持指令控制热重载、热更新</summary>
|
||||
|
||||
- 允许在运行期间修改`config.py`或其他代码后,以管理员账号向机器人发送指令`!reload`进行热重载,无需重启
|
||||
- 运行期间允许以管理员账号向机器人发送指令`!update`进行热更新,拉取远程最新代码并执行热重载
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅支持插件加载🧩</summary>
|
||||
|
||||
- 自行实现插件加载器及相关支持
|
||||
- 详细查看[插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅私聊、群聊黑名单机制</summary>
|
||||
|
||||
- 支持将人或群聊加入黑名单以忽略其消息
|
||||
- 详见Wiki`加入黑名单`节
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅长消息处理策略</summary>
|
||||
|
||||
- 支持将长消息转换成图片或消息记录组件,避免消息刷屏
|
||||
- 请查看`config.py`中`blob_message_strategy`等字段
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅回复速度限制</summary>
|
||||
|
||||
- 支持限制单会话内每分钟可进行的对话次数
|
||||
- 具有“等待”和“丢弃”两种策略
|
||||
- “等待”策略:在获取到回复后,等待直到此次响应时间达到对话响应时间均值
|
||||
- “丢弃”策略:此分钟内对话次数达到限制时,丢弃之后的对话
|
||||
- 详细请查看config.py中的相关配置
|
||||
</details>
|
||||
<details>
|
||||
<summary>✅支持使用网络代理</summary>
|
||||
|
||||
- 目前已支持正向代理访问接口
|
||||
- 详细请查看config.py中的`openai_config`的说明
|
||||
</details>
|
||||
|
||||
详情请查看[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)
|
||||
|
||||
## 🔩部署
|
||||
|
||||
**部署过程中遇到任何问题,请先在[QChatGPT](https://github.com/RockChinQ/QChatGPT/issues)或[qcg-installer](https://github.com/RockChinQ/qcg-installer/issues)的issue里进行搜索**
|
||||
|
||||
### - 注册OpenAI账号
|
||||
|
||||
> 若您要直接使用非OpenAI的模型(如New Bing),可跳过此步骤,直接进行之后的部署,完成后按照相关插件的文档进行配置即可
|
||||
|
||||
参考以下文章自行注册
|
||||
|
||||
> [国内注册ChatGPT的方法(100%可用)](https://www.pythonthree.com/register-openai-chatgpt/)
|
||||
> [手把手教你如何注册ChatGPT,超级详细](https://guxiaobei.com/51461)
|
||||
|
||||
注册成功后请前往[个人中心查看](https://beta.openai.com/account/api-keys)api_key
|
||||
完成注册后,使用以下自动化或手动部署步骤
|
||||
|
||||
### - 自动化部署
|
||||
|
||||
<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以上版本
|
||||
|
||||
#### 配置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 dulwich Pillow
|
||||
```
|
||||
|
||||
3. 运行一次主程序,生成配置文件
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
4. 编辑配置文件`config.py`
|
||||
|
||||
按照文件内注释填写配置信息
|
||||
|
||||
5. 运行主程序
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
无报错信息即为运行成功
|
||||
|
||||
**常见问题**
|
||||
|
||||
- mirai登录提示`QQ版本过低`,见[此issue](https://github.com/RockChinQ/QChatGPT/issues/137)
|
||||
- 如提示安装`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>
|
||||
|
||||
## 🚀使用
|
||||
|
||||
**部署完成后必看: [指令说明](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)**
|
||||
所有功能查看[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)
|
||||
|
||||
## 🧩插件生态
|
||||
|
||||
现已支持自行开发插件对功能进行扩展或自定义程序行为
|
||||
详见[Wiki插件使用页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8)
|
||||
开发教程见[Wiki插件开发页](https://github.com/RockChinQ/QChatGPT/wiki/%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91)
|
||||
|
||||
<details>
|
||||
<summary>查看插件列表</summary>
|
||||
|
||||
### 示例插件
|
||||
|
||||
在`tests/plugin_examples`目录下,将其整个目录复制到`plugins`目录下即可使用
|
||||
|
||||
- `cmdcn` - 主程序指令中文形式
|
||||
- `hello_plugin` - 在收到消息`hello`时回复相应消息
|
||||
- `urlikethisijustsix` - 收到冒犯性消息时回复相应消息
|
||||
|
||||
### 更多
|
||||
|
||||
欢迎提交新的插件
|
||||
|
||||
- [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)
|
||||
- [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) - 实时获取百度`文心一言`等待列表人数
|
||||
- [QChartGPT_Emoticon_Plugin](https://github.com/chordfish-k/QChartGPT_Emoticon_Plugin) - 使机器人根据回复内容发送表情包
|
||||
</details>
|
||||
|
||||
## 😘致谢
|
||||
|
||||
- [@the-lazy-me](https://github.com/the-lazy-me) 为本项目制作[视频教程](https://www.bilibili.com/video/BV15v4y1X7aP)
|
||||
- [@mikumifa](https://github.com/mikumifa) 本项目Docker部署仓库开发者
|
||||
- [@dominoar](https://github.com/dominoar) 为本项目开发多种插件
|
||||
- [@万神的星空](https://github.com/qq255204159) 整合包发行
|
||||
- [@ljcduo](https://github.com/ljcduo) GPT-4 API内测账号提供
|
||||
|
||||
以及所有[贡献者](https://github.com/RockChinQ/QChatGPT/graphs/contributors)和其他为本项目提供支持的朋友们。
|
||||
|
||||
<!-- ## 👍赞赏
|
||||
|
||||
<img alt="赞赏码" src="res/mm_reward_qrcode_1672840549070.png" width="400" height="400"/> -->
|
||||
<p align="center">
|
||||
<img src="https://qchatgpt.rockchin.top/logo.png" alt="QChatGPT" width="180" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
# QChatGPT
|
||||
|
||||
<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 | 3.10 | 3.11-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>
|
||||
|
||||
## 使用文档
|
||||
|
||||
<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>
|
||||
|
||||
## 相关链接
|
||||
|
||||
<a href="https://github.com/RockChinQ/qcg-installer">安装器源码</a> |
|
||||
<a href="https://github.com/RockChinQ/qcg-tester">测试工程源码</a> |
|
||||
<a href="https://github.com/the-lazy-me/QChatGPT-Wiki">官方文档储存库</a>
|
||||
|
||||
<img alt="回复效果(带有联网插件)" src="https://qchatgpt.rockchin.top/assets/image/QChatGPT-1211.png" width="500px"/>
|
||||
</div>
|
||||
|
||||
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
|
||||
|
||||
# [必需] Mirai的配置
|
||||
# 消息处理协议适配器
|
||||
# 目前支持以下适配器:
|
||||
# - "yirimirai": mirai的通信框架,YiriMirai框架适配器, 请同时填写下方mirai_http_api_config
|
||||
# - "nakuru": go-cqhttp通信框架,请同时填写下方nakuru_config
|
||||
msg_source_adapter = "yirimirai"
|
||||
|
||||
# [必需(与nakuru二选一,取决于msg_source_adapter)] Mirai的配置
|
||||
# 请到配置mirai的步骤中的教程查看每个字段的信息
|
||||
# adapter: 选择适配器,目前支持HTTPAdapter和WebSocketAdapter
|
||||
# host: 运行mirai的主机地址
|
||||
@@ -18,6 +24,15 @@ mirai_http_api_config = {
|
||||
"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的配置
|
||||
# api_key: OpenAI的API Key
|
||||
# http_proxy: 请求OpenAI时使用的代理,None为不使用,https和socks5暂不能使用
|
||||
@@ -33,20 +48,43 @@ mirai_http_api_config = {
|
||||
# },
|
||||
# "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 = {
|
||||
"api_key": {
|
||||
"default": "openai_api_key"
|
||||
},
|
||||
"http_proxy": None
|
||||
"http_proxy": None,
|
||||
"reverse_proxy": None
|
||||
}
|
||||
|
||||
# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别指令
|
||||
# api-key切换策略
|
||||
# active:每次请求时都会切换api-key
|
||||
# passive:仅当api-key超额时才会切换api-key
|
||||
switch_strategy = "active"
|
||||
|
||||
# [必需] 管理员QQ号,用于接收报错等通知及执行管理员级别命令
|
||||
# 支持多个管理员,可以使用list形式设置,例如:
|
||||
# admin_qq = [12345678, 87654321]
|
||||
admin_qq = 0
|
||||
|
||||
# 情景预设(机器人人格)
|
||||
# 每个会话的预设信息,影响所有会话,无视指令重置
|
||||
# 每个会话的预设信息,影响所有会话,无视命令重置
|
||||
# 可以通过这个字段指定某些情况的回复,可直接用自然语言描述指令
|
||||
# 例如:
|
||||
# default_prompt = "如果我之后想获取帮助,请你说“输入!help获取帮助”"
|
||||
@@ -60,14 +98,14 @@ admin_qq = 0
|
||||
# "en-dict": "我想让你充当英英词典,对于给出的英文单词,你要给出其中文意思以及英文解释,并且给出一个例句,此外不要有其他反馈。",
|
||||
# }
|
||||
#
|
||||
# 在使用期间即可通过指令:
|
||||
# 在使用期间即可通过命令:
|
||||
# !reset [名称]
|
||||
# 来使用指定的情景预设重置会话
|
||||
# 例如:
|
||||
# !reset linux-terminal
|
||||
# 若不指定名称,则使用默认情景预设
|
||||
#
|
||||
# 也可以使用指令:
|
||||
# 也可以使用命令:
|
||||
# !default <名称>
|
||||
# 将指定的情景预设设置为默认情景预设
|
||||
# 例如:
|
||||
@@ -76,37 +114,15 @@ admin_qq = 0
|
||||
#
|
||||
# 还可以加载文件中的预设文字,使用方法请查看: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获取帮助”",
|
||||
"default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。",
|
||||
}
|
||||
|
||||
# 情景预设格式
|
||||
# 参考值:旧版本方式:default | 完整情景:full_scenario
|
||||
# 旧版本的格式为上述default_prompt中的内容,或prompts目录下的文件名
|
||||
#
|
||||
# 完整情景预设的格式为JSON,在scenario目录下的JSON文件中列出对话的每个回合,编写方法见scenario/default-template.json
|
||||
# 编写方法例如:
|
||||
# {
|
||||
# "prompt": [
|
||||
# {
|
||||
# "role": "user",
|
||||
# "content": "之后当我需要帮助时,请说“输入!help获取帮助”"
|
||||
# },{
|
||||
# "role": "assistant",
|
||||
# "content": "好的,当你之后需要帮助时,我会说“输入!help获取帮助”"
|
||||
# },{
|
||||
# "role": "user",
|
||||
# "content": "帮助"
|
||||
# },{
|
||||
# "role": "assistant",
|
||||
# "content": "输入!help获取帮助"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# 您可以按照上述格式编写自己的情景预设,在prompt中列出对话的每个回合,
|
||||
# role为user或assistant,分别表示用户和机器人的回复
|
||||
# 每个JSON文件是一个情景预设,文件名即为情景预设的名称
|
||||
preset_mode = "default"
|
||||
# 参考值:默认方式: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机器人也会响应
|
||||
@@ -115,19 +131,41 @@ preset_mode = "default"
|
||||
# 注意:由消息前缀(prefix)匹配的消息中将会删除此前缀,正则表达式(regexp)匹配的消息不会删除匹配的部分
|
||||
# 前缀匹配优先级高于正则表达式匹配
|
||||
# 正则表达式简明教程: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 = {
|
||||
"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为响应所有消息, 仅在前几项判断不通过时生效
|
||||
"default": {
|
||||
"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为响应所有消息, 仅在前几项判断不通过时生效
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# 消息忽略规则
|
||||
# 适用于私聊及群聊
|
||||
# 符合此规则的消息将不会被响应
|
||||
# 支持消息前缀匹配及正则表达式匹配
|
||||
# 此设置优先级高于response_rules
|
||||
# 用以过滤mirai等其他层级的指令
|
||||
# 用以过滤mirai等其他层级的命令
|
||||
# @see https://github.com/RockChinQ/QChatGPT/issues/165
|
||||
ignore_rules = {
|
||||
"prefix": ["/"],
|
||||
@@ -162,54 +200,97 @@ encourage_sponsor_at_start = True
|
||||
# 每次向OpenAI接口发送对话记录上下文的字符数
|
||||
# 最大不超过(4096 - max_tokens)个字符,max_tokens为下方completion_api_params中的max_tokens
|
||||
# 注意:较大的prompt_submit_length会导致OpenAI账户额度消耗更快
|
||||
prompt_submit_length = 2048
|
||||
prompt_submit_length = 3072
|
||||
|
||||
# 是否在token超限报错时自动重置会话
|
||||
# 可在tips.py中编辑提示语
|
||||
auto_reset = True
|
||||
|
||||
# OpenAI补全API的参数
|
||||
# 请在下方填写模型,程序自动选择接口
|
||||
# 模型文档:https://platform.openai.com/docs/models
|
||||
# 现已支持的模型有:
|
||||
#
|
||||
# 'gpt-4'
|
||||
# 'gpt-4-0314'
|
||||
# 'gpt-4-32k'
|
||||
# 'gpt-4-32k-0314'
|
||||
# 'gpt-3.5-turbo'
|
||||
# 'gpt-3.5-turbo-0301'
|
||||
# 'text-davinci-003'
|
||||
# 'text-davinci-002'
|
||||
# 'code-davinci-002'
|
||||
# 'code-cushman-001'
|
||||
# 'text-curie-001'
|
||||
# 'text-babbage-001'
|
||||
# 'text-ada-001'
|
||||
# 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
|
||||
# 请将内容修改到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 = {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"temperature": 0.9, # 数值越低得到的回答越理性,取值范围[0, 1]
|
||||
"top_p": 1, # 生成的文本的文本与要求的符合度, 取值范围[0, 1]
|
||||
"frequency_penalty": 0.2,
|
||||
"presence_penalty": 1.0,
|
||||
}
|
||||
|
||||
# OpenAI的Image API的参数
|
||||
# 具体请查看OpenAI的文档: https://beta.openai.com/docs/api-reference/images/create
|
||||
# 具体请查看OpenAI的文档: https://platform.openai.com/docs/api-reference/images/create
|
||||
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",
|
||||
}
|
||||
|
||||
# 跟踪函数调用
|
||||
# 为True时,在每次GPT进行Function Calling时都会输出发送一条回复给用户
|
||||
# 同时,一次提问内所有的Function Calling和普通回复消息都会单独发送给用户
|
||||
trace_function_calls = False
|
||||
|
||||
# 群内回复消息时是否引用原消息
|
||||
quote_origin = True
|
||||
quote_origin = False
|
||||
|
||||
# 群内回复消息时是否at发送者
|
||||
at_sender = False
|
||||
|
||||
# 回复绘图时是否包含图片描述
|
||||
include_image_description = True
|
||||
|
||||
# 消息处理的超时时间,单位为秒
|
||||
process_message_timeout = 30
|
||||
process_message_timeout = 120
|
||||
|
||||
# 回复消息时是否显示[GPT]前缀
|
||||
show_prefix = False
|
||||
|
||||
# 回复前的强制延迟时间,降低机器人被腾讯风控概率
|
||||
# *此机制对命令和消息、私聊及群聊均生效
|
||||
# 每次处理时从以下的范围取一个随机秒数,
|
||||
# 当此次消息处理时间低于此秒数时,将会强制延迟至此秒数
|
||||
# 例如:[1.5, 3],则每次处理时会随机取一个1.5-3秒的随机数,若处理时间低于此随机数,则强制延迟至此随机秒数
|
||||
# 若您不需要此功能,请将force_delay_range设置为[0, 0]
|
||||
force_delay_range = [0, 0]
|
||||
|
||||
# 应用长消息处理策略的阈值
|
||||
# 当回复消息长度超过此值时,将使用长消息处理策略
|
||||
blob_message_threshold = 256
|
||||
@@ -219,6 +300,12 @@ blob_message_threshold = 256
|
||||
# - "forward": 将长消息转换为转发消息组件发送
|
||||
blob_message_strategy = "forward"
|
||||
|
||||
# 允许等待
|
||||
# 同一会话内,是否等待上一条消息处理完成后再处理下一条消息
|
||||
# 若设置为False,若上一条未处理完时收到了新消息,将会丢弃新消息
|
||||
# 丢弃消息时的提示信息可以在tips.py中修改
|
||||
wait_last_done = True
|
||||
|
||||
# 文字转图片时使用的字体文件路径
|
||||
# 当策略为"image"时生效
|
||||
# 若在Windows系统下,程序会自动使用Windows自带的微软雅黑字体
|
||||
@@ -233,53 +320,51 @@ retry_times = 3
|
||||
# 设置为False时,向用户及管理员发送错误详细信息
|
||||
hide_exce_info_to_user = False
|
||||
|
||||
# 消息处理出错时向用户发送的提示信息
|
||||
# 仅当hide_exce_info_to_user为True时生效
|
||||
# 设置为空字符串时,不发送提示信息
|
||||
alter_tip_message = '出错了,请稍后再试'
|
||||
|
||||
# 机器人线程池大小
|
||||
# 该参数决定机器人可以同时处理几个人的消息,超出线程池数量的请求会被阻塞,不会被丢弃
|
||||
# 如果你不清楚该参数的意义,请不要更改
|
||||
pool_num = 10
|
||||
|
||||
# 每个会话的过期时间,单位为秒
|
||||
# 默认值20分钟
|
||||
session_expire_time = 60 * 20
|
||||
session_expire_time = 1200
|
||||
|
||||
# 会话限速
|
||||
# 单会话内每分钟可进行的对话次数
|
||||
# 若不需要限速,可以设置为一个很大的值
|
||||
# 默认值60次,基本上不会触发限速
|
||||
rate_limitation = 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 = "wait"
|
||||
|
||||
# drop策略时,超过限速均值时,丢弃的对话的提示信息
|
||||
# 仅当rate_limitation_strategy为"drop"时生效
|
||||
# 若设置为空字符串,则不发送提示信息
|
||||
rate_limit_drop_tip = "本分钟对话次数超过限速次数,此对话被丢弃"
|
||||
rate_limit_strategy = "drop"
|
||||
|
||||
# 是否在启动时进行依赖库更新
|
||||
upgrade_dependencies = True
|
||||
upgrade_dependencies = False
|
||||
|
||||
# 是否上报统计信息
|
||||
# 用于统计机器人的使用情况,不会收集任何用户信息
|
||||
# 仅上报时间、字数使用量、绘图使用量,其他信息不会上报
|
||||
# 用于统计机器人的使用情况,数据不公开,不会收集任何敏感信息。
|
||||
# 仅实例识别UUID、上报时间、字数使用量、绘图使用量、插件使用情况、用户信息,其他信息不会上报
|
||||
report_usage = True
|
||||
|
||||
# 日志级别
|
||||
logging_level = logging.INFO
|
||||
|
||||
# 定制帮助消息
|
||||
help_message = """此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。
|
||||
你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。
|
||||
了解此项目请找QQ 1010553892 联系作者
|
||||
请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题
|
||||
每次会话最后一次交互后{}分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启
|
||||
欢迎到github.com/RockChinQ/QChatGPT 给个star
|
||||
|
||||
指令帮助信息请查看: 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""".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
|
||||
# 根据具体环境配置网络
|
||||
541
main.py
541
main.py
@@ -7,6 +7,57 @@ import time
|
||||
|
||||
import logging
|
||||
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
|
||||
|
||||
|
||||
try:
|
||||
import colorlog
|
||||
@@ -24,17 +75,12 @@ import colorlog
|
||||
import requests
|
||||
import websockets.exceptions
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
import pkg.utils.context
|
||||
|
||||
|
||||
sys.path.append(".")
|
||||
|
||||
log_colors_config = {
|
||||
'DEBUG': 'green', # cyan white
|
||||
'INFO': 'white',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'cyan',
|
||||
}
|
||||
# 是否使用override.json覆盖配置
|
||||
# 仅在启动时提供 --override 或 -r 参数时生效
|
||||
use_override = False
|
||||
|
||||
|
||||
def init_db():
|
||||
@@ -46,77 +92,81 @@ def init_db():
|
||||
|
||||
def ensure_dependencies():
|
||||
import pkg.utils.pkgmgr as pkgmgr
|
||||
pkgmgr.run_pip(["install", "openai", "Pillow", "--upgrade",
|
||||
"-i", "https://pypi.douban.com/simple/",
|
||||
"--trusted-host", "pypi.douban.com"])
|
||||
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
|
||||
|
||||
log_file_name = "qchatgpt.log"
|
||||
|
||||
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 init_runtime_log_file():
|
||||
"""为此次运行生成日志文件
|
||||
格式: qchatgpt-yyyy-MM-dd-HH-mm-ss.log
|
||||
"""
|
||||
global log_file_name
|
||||
def complete_tips():
|
||||
"""根据tips-custom-template模块补全tips模块的属性"""
|
||||
non_exist_keys = []
|
||||
|
||||
# 检查logs目录是否存在
|
||||
if not os.path.exists("logs"):
|
||||
os.mkdir("logs")
|
||||
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
|
||||
|
||||
# 检查本目录是否有qchatgpt.log,若有,移动到logs目录
|
||||
if os.path.exists("qchatgpt.log"):
|
||||
shutil.move("qchatgpt.log", "logs/qchatgpt.legacy.log")
|
||||
|
||||
log_file_name = "logs/qchatgpt-%s.log" % time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
|
||||
|
||||
|
||||
def reset_logging():
|
||||
global log_file_name
|
||||
assert os.path.exists('config.py')
|
||||
|
||||
config = importlib.import_module('config')
|
||||
|
||||
import pkg.utils.context
|
||||
|
||||
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)
|
||||
|
||||
logging.basicConfig(level=config.logging_level, # 设置日志输出格式
|
||||
filename=log_file_name, # log日志输出的文件位置和文件名
|
||||
format="[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s",
|
||||
# 日志输出的格式
|
||||
# -8表示占位符,让输出左对齐,输出长度都为8位
|
||||
datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
|
||||
)
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(config.logging_level)
|
||||
sh.setFormatter(colorlog.ColoredFormatter(
|
||||
fmt="%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : "
|
||||
"%(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
log_colors=log_colors_config
|
||||
))
|
||||
logging.getLogger().addHandler(sh)
|
||||
pkg.utils.context.context['logger_handler'] = sh
|
||||
return sh
|
||||
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)
|
||||
|
||||
|
||||
def main(first_time_init=False):
|
||||
async def start_process(first_time_init=False):
|
||||
"""启动流程,reload之后会被执行"""
|
||||
|
||||
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
|
||||
|
||||
import config
|
||||
# 更新openai库到最新版本
|
||||
if not hasattr(config, 'upgrade_dependencies') or config.upgrade_dependencies:
|
||||
if 'upgrade_dependencies' not in cfg or cfg['upgrade_dependencies']:
|
||||
print("正在更新依赖库,请等待...")
|
||||
if not hasattr(config, 'upgrade_dependencies'):
|
||||
if 'upgrade_dependencies' not in cfg:
|
||||
print("这个操作不是必须的,如果不想更新,请在config.py中添加upgrade_dependencies=False")
|
||||
else:
|
||||
print("这个操作不是必须的,如果不想更新,请在config.py中将upgrade_dependencies设置为False")
|
||||
@@ -127,148 +177,170 @@ def main(first_time_init=False):
|
||||
|
||||
known_exception_caught = False
|
||||
try:
|
||||
# 导入config.py
|
||||
assert os.path.exists('config.py')
|
||||
try:
|
||||
|
||||
config = importlib.import_module('config')
|
||||
sh = reset_logging()
|
||||
pkg.utils.context.context['logger_handler'] = sh
|
||||
|
||||
init_runtime_log_file()
|
||||
# 初始化文字转图片
|
||||
from pkg.utils import text2img
|
||||
text2img.initialize()
|
||||
|
||||
sh = reset_logging()
|
||||
# 检查是否设置了管理员
|
||||
if cfg['admin_qq'] == 0:
|
||||
# logging.warning("未设置管理员QQ,管理员权限命令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
|
||||
while True:
|
||||
try:
|
||||
cfg['admin_qq'] = int(input("未设置管理员QQ,管理员权限命令及运行告警将无法使用,请输入管理员QQ号: "))
|
||||
# 写入到文件
|
||||
|
||||
# 配置完整性校验
|
||||
is_integrity = True
|
||||
config_template = importlib.import_module('config-template')
|
||||
for key in dir(config_template):
|
||||
if not key.startswith("__") and not hasattr(config, key):
|
||||
setattr(config, key, getattr(config_template, key))
|
||||
logging.warning("[{}]不存在".format(key))
|
||||
is_integrity = False
|
||||
# 读取文件
|
||||
config_file_str = ""
|
||||
with open("config.py", "r", encoding="utf-8") as f:
|
||||
config_file_str = f.read()
|
||||
# 替换
|
||||
config_file_str = config_file_str.replace("admin_qq = 0", "admin_qq = " + str(cfg['admin_qq']))
|
||||
# 写入
|
||||
with open("config.py", "w", encoding="utf-8") as f:
|
||||
f.write(config_file_str)
|
||||
|
||||
if not is_integrity:
|
||||
logging.warning("配置文件不完整,请依据config-template.py检查config.py")
|
||||
logging.warning("以上配置已被设为默认值,将在5秒后继续启动... ")
|
||||
print("管理员QQ已设置,如需修改请修改config.py中的admin_qq字段")
|
||||
time.sleep(4)
|
||||
break
|
||||
except ValueError:
|
||||
print("请输入数字")
|
||||
|
||||
# 初始化中央服务器 API 交互实例
|
||||
from pkg.utils.center import apigroup
|
||||
from pkg.utils.center import v2 as center_v2
|
||||
|
||||
# 检查override.json覆盖
|
||||
if os.path.exists("override.json"):
|
||||
override_json = json.load(open("override.json", "r", encoding="utf-8"))
|
||||
for key in override_json:
|
||||
if hasattr(config, key):
|
||||
setattr(config, key, override_json[key])
|
||||
logging.info("覆写配置[{}]为[{}]".format(key, override_json[key]))
|
||||
else:
|
||||
logging.error("无法覆写配置[{}]为[{}],该配置不存在,请检查override.json是否正确".format(key, override_json[key]))
|
||||
center_v2_api = center_v2.V2CenterAPI(
|
||||
basic_info={
|
||||
"host_id": pkg.audit.identifier.identifier['host_id'],
|
||||
"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)
|
||||
|
||||
if not is_integrity:
|
||||
time.sleep(5)
|
||||
import pkg.openai.manager
|
||||
import pkg.database.manager
|
||||
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()
|
||||
|
||||
import pkg.utils.context
|
||||
pkg.utils.context.set_config(config)
|
||||
# 配置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"]
|
||||
}
|
||||
|
||||
# 检查是否设置了管理员
|
||||
if not (hasattr(config, 'admin_qq') and config.admin_qq != 0):
|
||||
# logging.warning("未设置管理员QQ,管理员权限指令及运行告警将无法使用,如需设置请修改config.py中的admin_qq字段")
|
||||
while True:
|
||||
try:
|
||||
config.admin_qq = int(input("未设置管理员QQ,管理员权限指令及运行告警将无法使用,请输入管理员QQ号: "))
|
||||
# 写入到文件
|
||||
# 配置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"]
|
||||
|
||||
# 读取文件
|
||||
config_file_str = ""
|
||||
with open("config.py", "r", encoding="utf-8") as f:
|
||||
config_file_str = f.read()
|
||||
# 替换
|
||||
config_file_str = config_file_str.replace("admin_qq = 0", "admin_qq = " + str(config.admin_qq))
|
||||
# 写入
|
||||
with open("config.py", "w", encoding="utf-8") as f:
|
||||
f.write(config_file_str)
|
||||
# 主启动流程
|
||||
database = pkg.database.manager.DatabaseManager()
|
||||
|
||||
print("管理员QQ已设置,如需修改请修改config.py中的admin_qq字段")
|
||||
time.sleep(4)
|
||||
break
|
||||
except ValueError:
|
||||
print("请输入数字")
|
||||
database.initialize_database()
|
||||
|
||||
import pkg.openai.manager
|
||||
import pkg.database.manager
|
||||
import pkg.openai.session
|
||||
import pkg.qqbot.manager
|
||||
import pkg.openai.dprompt
|
||||
openai_interact = pkg.openai.manager.OpenAIInteract(cfg['openai_config']['api_key'])
|
||||
|
||||
pkg.openai.dprompt.read_prompt_from_file()
|
||||
pkg.openai.dprompt.read_scenario_from_file()
|
||||
# 加载所有未超时的session
|
||||
pkg.openai.session.load_sessions()
|
||||
|
||||
pkg.utils.context.context['logger_handler'] = sh
|
||||
# 主启动流程
|
||||
database = pkg.database.manager.DatabaseManager()
|
||||
# 初始化qq机器人
|
||||
qqbot = pkg.qqbot.manager.QQBotManager(first_time_init=first_time_init)
|
||||
|
||||
database.initialize_database()
|
||||
# 加载插件
|
||||
import pkg.plugin.host
|
||||
pkg.plugin.host.load_plugins()
|
||||
|
||||
openai_interact = pkg.openai.manager.OpenAIInteract(config.openai_config['api_key'])
|
||||
pkg.plugin.host.initialize_plugins()
|
||||
|
||||
# 加载所有未超时的session
|
||||
pkg.openai.session.load_sessions()
|
||||
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
|
||||
|
||||
# 初始化qq机器人
|
||||
qqbot = pkg.qqbot.manager.QQBotManager(mirai_http_api_config=config.mirai_http_api_config,
|
||||
timeout=config.process_message_timeout, retry=config.retry_times,
|
||||
first_time_init=first_time_init, pool_num=config.pool_num)
|
||||
import mirai.exceptions
|
||||
|
||||
# 加载插件
|
||||
import pkg.plugin.host
|
||||
pkg.plugin.host.load_plugins()
|
||||
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
|
||||
|
||||
pkg.plugin.host.initialize_plugins()
|
||||
|
||||
if first_time_init: # 不是热重载之后的启动,则启动新的bot线程
|
||||
|
||||
import mirai.exceptions
|
||||
|
||||
def run_bot_wrapper():
|
||||
global known_exception_caught
|
||||
try:
|
||||
qqbot.bot.run()
|
||||
except TypeError as e:
|
||||
if str(e).__contains__("argument 'debug'"):
|
||||
logging.error(
|
||||
"连接bot失败:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/82".format(e))
|
||||
known_exception_caught = True
|
||||
elif str(e).__contains__("As of 3.10, the *loop*"):
|
||||
logging.error(
|
||||
"Websockets版本过低:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/5".format(e))
|
||||
known_exception_caught = True
|
||||
|
||||
except websockets.exceptions.InvalidStatus as e:
|
||||
logging.error(
|
||||
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||
e))
|
||||
known_exception_caught = True
|
||||
except mirai.exceptions.NetworkError as e:
|
||||
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".format(e))
|
||||
known_exception_caught = True
|
||||
except Exception as e:
|
||||
if str(e).__contains__("404"):
|
||||
except websockets.exceptions.InvalidStatus as e:
|
||||
logging.error(
|
||||
"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))
|
||||
except mirai.exceptions.NetworkError as e:
|
||||
logging.error("连接mirai-api-http失败:{}, 请检查是否已按照文档启动mirai".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:
|
||||
logging.error(
|
||||
"捕捉到未知异常:{}, 请前往 https://github.com/RockChinQ/QChatGPT/issues 查找或提issue".format(e))
|
||||
known_exception_caught = True
|
||||
raise e
|
||||
|
||||
qq_bot_thread = threading.Thread(target=run_bot_wrapper, args=(), daemon=True)
|
||||
qq_bot_thread.start()
|
||||
except Exception as e:
|
||||
if str(e).__contains__("404"):
|
||||
logging.error(
|
||||
"mirai-api-http端口无法使用:{}, 解决方案: https://github.com/RockChinQ/QChatGPT/issues/22".format(
|
||||
e))
|
||||
known_exception_caught = True
|
||||
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:
|
||||
# 判断若是Windows,输出选择模式可能会暂停程序的警告
|
||||
if os.name == 'nt':
|
||||
@@ -276,18 +348,23 @@ def main(first_time_init=False):
|
||||
logging.info("您正在使用Windows系统,若命令行窗口处于“选择”模式,程序可能会被暂停,此时请右键点击窗口空白区域使其取消选择模式。")
|
||||
|
||||
time.sleep(12)
|
||||
|
||||
if first_time_init:
|
||||
if not known_exception_caught:
|
||||
logging.info('程序启动完成,如长时间未显示 ”成功登录到账号xxxxx“ ,并且不回复消息,请查看 '
|
||||
'https://github.com/RockChinQ/QChatGPT/issues/37')
|
||||
if cfg['msg_source_adapter'] == "yirimirai":
|
||||
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:
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info('热重载完成')
|
||||
|
||||
# 发送赞赏码
|
||||
if hasattr(config, 'encourage_sponsor_at_start') \
|
||||
and config.encourage_sponsor_at_start \
|
||||
if cfg['encourage_sponsor_at_start'] \
|
||||
and pkg.utils.context.get_openai_manager().audit_mgr.get_total_text_length() >= 2048:
|
||||
|
||||
logging.info("发送赞赏码")
|
||||
@@ -309,7 +386,8 @@ def main(first_time_init=False):
|
||||
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("当前已是最新版本")
|
||||
# logging.info("当前已是最新版本")
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logging.warning("检查更新失败:{}".format(e))
|
||||
@@ -317,16 +395,21 @@ def main(first_time_init=False):
|
||||
try:
|
||||
import pkg.utils.announcement as announcement
|
||||
new_announcement = announcement.fetch_new()
|
||||
if new_announcement != "":
|
||||
logging.critical("[公告] {}".format(new_announcement))
|
||||
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():
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.manager
|
||||
import pkg.openai.session
|
||||
try:
|
||||
@@ -345,35 +428,27 @@ def stop():
|
||||
raise e
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 检查是否有config.py,如果没有就把config-template.py复制一份,并退出程序
|
||||
if not os.path.exists('config.py'):
|
||||
shutil.copy('config-template.py', 'config.py')
|
||||
print('请先在config.py中填写配置')
|
||||
sys.exit(0)
|
||||
def main():
|
||||
global use_override
|
||||
# 检查是否携带了 --override 或 -r 参数
|
||||
if '--override' in sys.argv or '-r' in sys.argv:
|
||||
use_override = True
|
||||
|
||||
# 检查是否有banlist.py,如果没有就把banlist-template.py复制一份
|
||||
if not os.path.exists('banlist.py'):
|
||||
shutil.copy('banlist-template.py', 'banlist.py')
|
||||
# 初始化logging
|
||||
init_runtime_log_file()
|
||||
pkg.utils.context.context['logger_handler'] = reset_logging()
|
||||
|
||||
# 检查是否有sensitive.json
|
||||
if not os.path.exists("sensitive.json"):
|
||||
shutil.copy("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")
|
||||
|
||||
# 检查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)
|
||||
# 配置线程池
|
||||
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':
|
||||
init_db()
|
||||
sys.exit(0)
|
||||
@@ -384,16 +459,38 @@ if __name__ == '__main__':
|
||||
updater.update_all(cli=True)
|
||||
sys.exit(0)
|
||||
|
||||
# 关闭urllib的http警告
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
qqbot = main(True)
|
||||
def run_wrapper():
|
||||
asyncio.run(start_process(True))
|
||||
|
||||
import pkg.utils.context
|
||||
pkg.utils.context.get_thread_ctl().submit_sys_task(
|
||||
run_wrapper
|
||||
)
|
||||
|
||||
# 主线程循环
|
||||
while True:
|
||||
try:
|
||||
time.sleep(10)
|
||||
except KeyboardInterrupt:
|
||||
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()
|
||||
|
||||
print("程序退出")
|
||||
sys.exit(0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"comment": "这是override.json支持的字段全集, 关于override.json机制, 请查看https://github.com/RockChinQ/QChatGPT/pull/271",
|
||||
"msg_source_adapter": "yirimirai",
|
||||
"mirai_http_api_config": {
|
||||
"adapter": "WebSocketAdapter",
|
||||
"host": "localhost",
|
||||
@@ -7,27 +8,37 @@
|
||||
"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
|
||||
"http_proxy": null,
|
||||
"reverse_proxy": null
|
||||
},
|
||||
"switch_strategy": "active",
|
||||
"admin_qq": 0,
|
||||
"default_prompt": {
|
||||
"default": "如果我之后想获取帮助,请你说“输入!help获取帮助”"
|
||||
"default": "如果用户之后想获取帮助,请你说“输入!help获取帮助”。"
|
||||
},
|
||||
"preset_mode": "default",
|
||||
"preset_mode": "normal",
|
||||
"response_rules": {
|
||||
"at": true,
|
||||
"prefix": [
|
||||
"/ai",
|
||||
"!ai",
|
||||
"!ai",
|
||||
"ai"
|
||||
],
|
||||
"regexp": [],
|
||||
"random_rate": 0.0
|
||||
"default": {
|
||||
"at": true,
|
||||
"prefix": [
|
||||
"/ai",
|
||||
"!ai",
|
||||
"!ai",
|
||||
"ai"
|
||||
],
|
||||
"regexp": [],
|
||||
"random_rate": 0.0
|
||||
}
|
||||
},
|
||||
"ignore_rules": {
|
||||
"prefix": [
|
||||
@@ -42,34 +53,38 @@
|
||||
"baidu_secret_key": "",
|
||||
"inappropriate_message_tips": "[百度云]请珍惜机器人,当前返回内容不合规",
|
||||
"encourage_sponsor_at_start": true,
|
||||
"prompt_submit_length": 1024,
|
||||
"prompt_submit_length": 3072,
|
||||
"auto_reset": true,
|
||||
"completion_api_params": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"temperature": 0.9,
|
||||
"top_p": 1,
|
||||
"frequency_penalty": 0.2,
|
||||
"presence_penalty": 1.0
|
||||
"temperature": 0.9
|
||||
},
|
||||
"image_api_params": {
|
||||
"model": "dall-e-2",
|
||||
"size": "256x256"
|
||||
},
|
||||
"quote_origin": true,
|
||||
"trace_function_calls": false,
|
||||
"quote_origin": false,
|
||||
"at_sender": false,
|
||||
"include_image_description": true,
|
||||
"process_message_timeout": 30,
|
||||
"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,
|
||||
"alter_tip_message": "出错了,请稍后再试",
|
||||
"pool_num": 10,
|
||||
"session_expire_time": 1200,
|
||||
"rate_limitation": 60,
|
||||
"rate_limit_strategy": "wait",
|
||||
"rate_limit_drop_tip": "本分钟对话次数超过限速次数,此对话被丢弃",
|
||||
"upgrade_dependencies": true,
|
||||
"rate_limitation": {
|
||||
"default": 60
|
||||
},
|
||||
"rate_limit_strategy": "drop",
|
||||
"upgrade_dependencies": false,
|
||||
"report_usage": true,
|
||||
"logging_level": 20,
|
||||
"help_message": "此机器人通过调用OpenAI的GPT-3大型语言模型生成回复,不具有情感。\n你可以用自然语言与其交流,回复的消息中[GPT]开头的为模型生成的语言,[bot]开头的为程序提示。\n了解此项目请找QQ 1010553892 联系作者\n请不要用其生成整篇文章或大段代码,因为每次只会向模型提交少部分文字,生成大部分文字会产生偏题、前后矛盾等问题\n每次会话最后一次交互后20分钟后会自动结束,结束后将开启新会话,如需继续前一次会话请发送 !last 重新开启\n欢迎到github.com/RockChinQ/QChatGPT 给个star\n\n指令帮助信息请查看: 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"
|
||||
"logging_level": 20
|
||||
}
|
||||
@@ -5,11 +5,12 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import requests
|
||||
|
||||
import pkg.utils.context
|
||||
import pkg.utils.updater
|
||||
from ..utils import context
|
||||
from ..utils import updater
|
||||
|
||||
|
||||
class DataGatherer:
|
||||
@@ -20,7 +21,7 @@ class DataGatherer:
|
||||
|
||||
以key值md5为key,{
|
||||
"text": {
|
||||
"text-davinci-003": 文字量:int,
|
||||
"gpt-3.5-turbo": 文字量:int,
|
||||
},
|
||||
"image": {
|
||||
"256x256": 图片数量:int,
|
||||
@@ -32,33 +33,17 @@ class DataGatherer:
|
||||
def __init__(self):
|
||||
self.load_from_db()
|
||||
try:
|
||||
self.version_str = pkg.utils.updater.get_current_tag() # 从updater模块获取版本号
|
||||
self.version_str = updater.get_current_tag() # 从updater模块获取版本号
|
||||
except:
|
||||
pass
|
||||
|
||||
def report_to_server(self, subservice_name: str, count: int):
|
||||
"""向中央服务器报告使用量
|
||||
|
||||
只会报告此次请求的使用量,不会报告总量。
|
||||
不包含除版本号、使用类型、使用量以外的任何信息,仅供开发者分析使用情况。
|
||||
"""
|
||||
try:
|
||||
config = pkg.utils.context.get_config()
|
||||
if hasattr(config, "report_usage") and not config.report_usage:
|
||||
return
|
||||
res = requests.get("http://reports.rockchin.top:18989/usage?service_name=qchatgpt.{}&version={}&count={}".format(subservice_name, self.version_str, count))
|
||||
if res.status_code != 200 or res.text != "ok":
|
||||
logging.warning("report to server failed, status_code: {}, text: {}".format(res.status_code, res.text))
|
||||
except:
|
||||
return
|
||||
|
||||
def get_usage(self, 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 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
|
||||
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5() # 以key的md5进行储存
|
||||
|
||||
if key_md5 not in self.usage:
|
||||
self.usage[key_md5] = {}
|
||||
@@ -73,12 +58,10 @@ class DataGatherer:
|
||||
self.usage[key_md5]["text"][model] += length
|
||||
self.dump_to_db()
|
||||
|
||||
self.report_to_server("text", length)
|
||||
|
||||
def report_image_model_usage(self, size):
|
||||
"""调用方报告图片模型请求图片使用量"""
|
||||
|
||||
key_md5 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||
key_md5 = context.get_openai_manager().key_mgr.get_using_key_md5()
|
||||
|
||||
if key_md5 not in self.usage:
|
||||
self.usage[key_md5] = {}
|
||||
@@ -92,8 +75,6 @@ class DataGatherer:
|
||||
self.usage[key_md5]["image"][size] += 1
|
||||
self.dump_to_db()
|
||||
|
||||
self.report_to_server("image", 1)
|
||||
|
||||
def get_text_length_of_key(self, key):
|
||||
"""获取指定api-key (明文) 的文字总使用量(本地记录)"""
|
||||
key_md5 = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||
@@ -125,9 +106,9 @@ class DataGatherer:
|
||||
return total
|
||||
|
||||
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):
|
||||
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:
|
||||
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
|
||||
@@ -5,11 +5,10 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from sqlite3 import Cursor
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pkg.utils.context
|
||||
from ..utils import context
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
@@ -22,7 +21,7 @@ class DatabaseManager:
|
||||
|
||||
self.reconnect()
|
||||
|
||||
pkg.utils.context.set_database_manager(self)
|
||||
context.set_database_manager(self)
|
||||
|
||||
# 连接到数据库文件
|
||||
def reconnect(self):
|
||||
@@ -33,7 +32,7 @@ class DatabaseManager:
|
||||
def close(self):
|
||||
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(args))
|
||||
c = self.cursor.execute(*args, **kwargs)
|
||||
@@ -92,7 +91,7 @@ class DatabaseManager:
|
||||
`json` text not null
|
||||
)
|
||||
""")
|
||||
print('Database initialized.')
|
||||
# print('Database initialized.')
|
||||
|
||||
# session持久化
|
||||
def persistence_session(self, subject_type: str, subject_number: int, create_timestamp: int,
|
||||
@@ -145,11 +144,11 @@ class DatabaseManager:
|
||||
# 从数据库加载还没过期的session数据
|
||||
def load_valid_sessions(self) -> dict:
|
||||
# 从数据库中加载所有还没过期的session
|
||||
config = pkg.utils.context.get_config()
|
||||
config = context.get_config_manager().data
|
||||
self.__execute__("""
|
||||
select `name`, `type`, `number`, `create_timestamp`, `last_interact_timestamp`, `prompt`, `status`, `default_prompt`, `token_counts`
|
||||
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()
|
||||
sessions = {}
|
||||
for result in results:
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
"""OpenAI 接口处理及会话管理相关
|
||||
"""
|
||||
"""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
|
||||
@@ -1,121 +1,134 @@
|
||||
# 多情景预设值管理
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
__current__ = "default"
|
||||
"""当前默认使用的情景预设的名称
|
||||
from ..utils import context
|
||||
|
||||
由管理员使用`!default <名称>`指令切换
|
||||
"""
|
||||
# __current__ = "default"
|
||||
# """当前默认使用的情景预设的名称
|
||||
|
||||
__prompts_from_files__ = {}
|
||||
"""从文件中读取的情景预设值"""
|
||||
# 由管理员使用`!default <名称>`命令切换
|
||||
# """
|
||||
|
||||
__scenario_from_files__ = {}
|
||||
# __prompts_from_files__ = {}
|
||||
# """从文件中读取的情景预设值"""
|
||||
|
||||
# __scenario_from_files__ = {}
|
||||
|
||||
|
||||
def read_prompt_from_file():
|
||||
"""从文件读取预设值"""
|
||||
# 读取prompts/目录下的所有文件,以文件名为键,文件内容为值
|
||||
# 保存在__prompts_from_files__中
|
||||
global __prompts_from_files__
|
||||
import os
|
||||
class ScenarioMode:
|
||||
"""情景预设模式抽象类"""
|
||||
|
||||
__prompts_from_files__ = {}
|
||||
for file in os.listdir("prompts"):
|
||||
with open(os.path.join("prompts", file), encoding="utf-8") as f:
|
||||
__prompts_from_files__[file] = f.read()
|
||||
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
|
||||
|
||||
|
||||
def read_scenario_from_file():
|
||||
"""从JSON文件读取情景预设"""
|
||||
global __scenario_from_files__
|
||||
import os
|
||||
class NormalScenarioMode(ScenarioMode):
|
||||
"""普通情景预设模式"""
|
||||
|
||||
__scenario_from_files__ = {}
|
||||
for file in os.listdir("scenario"):
|
||||
if file == "default-template.json":
|
||||
continue
|
||||
with open(os.path.join("scenario", file), encoding="utf-8") as f:
|
||||
__scenario_from_files__[file] = json.load(f)
|
||||
def __init__(self):
|
||||
config = context.get_config_manager().data
|
||||
|
||||
|
||||
def get_prompt_dict() -> dict:
|
||||
"""获取预设值字典"""
|
||||
import config
|
||||
default_prompt = config.default_prompt
|
||||
if type(default_prompt) == str:
|
||||
default_prompt = {"default": default_prompt}
|
||||
elif type(default_prompt) == dict:
|
||||
pass
|
||||
else:
|
||||
raise TypeError("default_prompt must be str or dict")
|
||||
|
||||
# 将文件中的预设值合并到default_prompt中
|
||||
for key in __prompts_from_files__:
|
||||
default_prompt[key] = __prompts_from_files__[key]
|
||||
|
||||
return default_prompt
|
||||
|
||||
|
||||
def set_current(name):
|
||||
global __current__
|
||||
for key in get_prompt_dict():
|
||||
if key.lower().startswith(name.lower()):
|
||||
__current__ = key
|
||||
return
|
||||
raise KeyError("未找到情景预设: " + name)
|
||||
|
||||
|
||||
def get_current():
|
||||
global __current__
|
||||
return __current__
|
||||
|
||||
|
||||
def set_to_default():
|
||||
global __current__
|
||||
default_dict = get_prompt_dict()
|
||||
|
||||
if "default" in default_dict:
|
||||
__current__ = "default"
|
||||
else:
|
||||
__current__ = list(default_dict.keys())[0]
|
||||
|
||||
|
||||
def get_prompt(name: str = None) -> list:
|
||||
global __scenario_from_files__
|
||||
import config
|
||||
preset_mode = config.preset_mode
|
||||
|
||||
"""获取预设值"""
|
||||
if name is None:
|
||||
name = get_current()
|
||||
|
||||
# JSON预设方式
|
||||
if preset_mode == 'full_scenario':
|
||||
import os
|
||||
|
||||
for key in __scenario_from_files__:
|
||||
if key.lower().startswith(name.lower()):
|
||||
logging.debug('成功加载情景预设从JSON文件: {}'.format(key))
|
||||
return __scenario_from_files__[key]['prompt']
|
||||
# 加载config中的default_prompt值
|
||||
if type(config['default_prompt']) == str:
|
||||
self.using_prompt_name = "default"
|
||||
self.prompts = {"default": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": config['default_prompt']
|
||||
}
|
||||
]}
|
||||
|
||||
# 默认预设方式
|
||||
elif preset_mode == 'default':
|
||||
|
||||
default_dict = get_prompt_dict()
|
||||
|
||||
for key in default_dict:
|
||||
if key.lower().startswith(name.lower()):
|
||||
return [
|
||||
elif type(config['default_prompt']) == dict:
|
||||
for key in config['default_prompt']:
|
||||
self.prompts[key] = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": default_dict[key]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "好的。"
|
||||
"role": "system",
|
||||
"content": config['default_prompt'][key]
|
||||
}
|
||||
]
|
||||
|
||||
raise KeyError("未找到默认情景预设: " + name)
|
||||
# 从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,8 +2,8 @@
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
from ..plugin import host as plugin_host
|
||||
from ..plugin import models as plugin_models
|
||||
|
||||
|
||||
class KeysManager:
|
||||
@@ -11,8 +11,7 @@ class KeysManager:
|
||||
"""所有api-key"""
|
||||
|
||||
using_key = ""
|
||||
"""当前使用的api-key
|
||||
"""
|
||||
"""当前使用的api-key"""
|
||||
|
||||
alerted = []
|
||||
"""已提示过超额的key
|
||||
@@ -33,33 +32,43 @@ class KeysManager:
|
||||
return hashlib.md5(self.using_key.encode('utf-8')).hexdigest()
|
||||
|
||||
def __init__(self, api_key):
|
||||
|
||||
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]
|
||||
|
||||
assert type(api_key) == dict
|
||||
self.api_key = api_key
|
||||
# 从usage中删除未加载的api-key的记录
|
||||
# 不删了,也许会运行时添加曾经有记录的api-key
|
||||
|
||||
self.auto_switch()
|
||||
|
||||
def auto_switch(self) -> (bool, str):
|
||||
def auto_switch(self) -> tuple[bool, str]:
|
||||
"""尝试切换api-key
|
||||
|
||||
Returns:
|
||||
是否切换成功, 切换后的api-key的别名
|
||||
"""
|
||||
|
||||
index = 0
|
||||
|
||||
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:
|
||||
self.using_key = self.api_key[key_name]
|
||||
|
||||
logging.info("使用api-key:" + key_name)
|
||||
logging.debug("使用api-key:" + key_name)
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
@@ -70,17 +79,20 @@ class KeysManager:
|
||||
|
||||
return True, key_name
|
||||
|
||||
self.using_key = list(self.api_key.values())[0]
|
||||
logging.info("使用api-key:" + list(self.api_key.keys())[0])
|
||||
index += 1
|
||||
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):
|
||||
self.api_key[key_name] = key
|
||||
|
||||
def set_current_exceeded(self):
|
||||
"""设置当前使用的api-key使用量超限
|
||||
"""
|
||||
"""设置当前使用的api-key使用量超限"""
|
||||
self.exceeded.append(self.using_key)
|
||||
|
||||
def get_key_name(self, api_key):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
|
||||
import openai
|
||||
from openai.types import images_response
|
||||
|
||||
import pkg.openai.keymgr
|
||||
import pkg.utils.context
|
||||
import pkg.audit.gatherer
|
||||
from pkg.openai.modelmgr import ModelRequest, create_openai_model_request
|
||||
from ..openai import keymgr
|
||||
from ..utils import context
|
||||
from ..audit import gatherer
|
||||
from ..openai import modelmgr
|
||||
from ..openai.api import model as api_model
|
||||
|
||||
|
||||
class OpenAIInteract:
|
||||
@@ -14,66 +16,57 @@ class OpenAIInteract:
|
||||
将文字接口和图片接口封装供调用方使用
|
||||
"""
|
||||
|
||||
key_mgr: pkg.openai.keymgr.KeysManager = None
|
||||
key_mgr: keymgr.KeysManager = None
|
||||
|
||||
audit_mgr: pkg.audit.gatherer.DataGatherer = None
|
||||
audit_mgr: gatherer.DataGatherer = None
|
||||
|
||||
default_image_api_params = {
|
||||
"size": "256x256",
|
||||
}
|
||||
|
||||
client: openai.Client = None
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
|
||||
self.key_mgr = pkg.openai.keymgr.KeysManager(api_key)
|
||||
self.audit_mgr = pkg.audit.gatherer.DataGatherer()
|
||||
self.key_mgr = keymgr.KeysManager(api_key)
|
||||
self.audit_mgr = gatherer.DataGatherer()
|
||||
|
||||
logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length())
|
||||
# logging.info("文字总使用量:%d", self.audit_mgr.get_total_text_length())
|
||||
|
||||
openai.api_key = self.key_mgr.get_using_key()
|
||||
self.client = openai.Client(
|
||||
api_key=self.key_mgr.get_using_key(),
|
||||
base_url=openai.base_url
|
||||
)
|
||||
|
||||
pkg.utils.context.set_openai_manager(self)
|
||||
context.set_openai_manager(self)
|
||||
|
||||
# 请求OpenAI Completion
|
||||
def request_completion(self, prompts) -> tuple[str, int]:
|
||||
"""请求补全接口回复
|
||||
|
||||
Parameters:
|
||||
prompts (str): 提示语
|
||||
|
||||
Returns:
|
||||
str: 回复
|
||||
def request_completion(self, messages: list):
|
||||
"""请求补全接口回复=
|
||||
"""
|
||||
# 选择接口请求类
|
||||
config = context.get_config_manager().data
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
request: api_model.RequestBase
|
||||
|
||||
# 根据模型选择使用的接口
|
||||
ai: ModelRequest = create_openai_model_request(
|
||||
config.completion_api_params['model'],
|
||||
'user',
|
||||
config.openai_config["http_proxy"] if "http_proxy" in config.openai_config else None
|
||||
)
|
||||
ai.request(
|
||||
prompts,
|
||||
**config.completion_api_params
|
||||
)
|
||||
response = ai.get_response()
|
||||
model: str = config['completion_api_params']['model']
|
||||
|
||||
logging.debug("OpenAI response: %s", response)
|
||||
cp_parmas = config['completion_api_params'].copy()
|
||||
del cp_parmas['model']
|
||||
|
||||
# 记录使用量
|
||||
current_round_token = 0
|
||||
if 'model' in config.completion_api_params:
|
||||
self.audit_mgr.report_text_model_usage(config.completion_api_params['model'],
|
||||
ai.get_total_tokens())
|
||||
current_round_token = ai.get_total_tokens()
|
||||
elif 'engine' in config.completion_api_params:
|
||||
self.audit_mgr.report_text_model_usage(config.completion_api_params['engine'],
|
||||
response['usage']['total_tokens'])
|
||||
current_round_token = response['usage']['total_tokens']
|
||||
request = modelmgr.select_request_cls(self.client, model, messages, cp_parmas)
|
||||
|
||||
return ai.get_message(), current_round_token
|
||||
# 请求接口
|
||||
for resp in request:
|
||||
|
||||
def request_image(self, prompt) -> dict:
|
||||
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:
|
||||
@@ -82,10 +75,10 @@ class OpenAIInteract:
|
||||
Returns:
|
||||
dict: 响应
|
||||
"""
|
||||
config = pkg.utils.context.get_config()
|
||||
params = config.image_api_params if hasattr(config, "image_api_params") else self.default_image_api_params
|
||||
config = context.get_config_manager().data
|
||||
params = config['image_api_params']
|
||||
|
||||
response = openai.Image.create(
|
||||
response = self.client.images.generate(
|
||||
prompt=prompt,
|
||||
n=1,
|
||||
**params
|
||||
|
||||
@@ -5,26 +5,44 @@ ChatCompletion - gpt-3.5-turbo 等模型
|
||||
Completion - text-davinci-003 等模型
|
||||
此模块封装此两个接口的请求实现,为上层提供统一的调用方式
|
||||
"""
|
||||
import openai, logging, threading, asyncio
|
||||
import openai.error as aiE
|
||||
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 = {
|
||||
'text-davinci-003',
|
||||
'text-davinci-002',
|
||||
'code-davinci-002',
|
||||
'code-cushman-001',
|
||||
'text-curie-001',
|
||||
'text-babbage-001',
|
||||
'text-ada-001',
|
||||
"gpt-3.5-turbo-instruct",
|
||||
}
|
||||
|
||||
CHAT_COMPLETION_MODELS = {
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-0301',
|
||||
'gpt-4',
|
||||
'gpt-4-0314',
|
||||
'gpt-4-32k',
|
||||
'gpt-4-32k-0314'
|
||||
# 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 = {
|
||||
@@ -36,153 +54,86 @@ IMAGE_MODELS = {
|
||||
}
|
||||
|
||||
|
||||
class ModelRequest:
|
||||
"""模型接口请求父类"""
|
||||
|
||||
can_chat = False
|
||||
runtime: threading.Thread = None
|
||||
ret = {}
|
||||
proxy: str = None
|
||||
request_ready = True
|
||||
error_info: str = "若在没有任何错误的情况下看到这句话,请带着配置文件上报Issues"
|
||||
|
||||
def __init__(self, model_name, user_name, request_fun, http_proxy:str = None, time_out = None):
|
||||
self.model_name = model_name
|
||||
self.user_name = user_name
|
||||
self.request_fun = request_fun
|
||||
self.time_out = time_out
|
||||
if http_proxy != None:
|
||||
self.proxy = http_proxy
|
||||
openai.proxy = self.proxy
|
||||
self.request_ready = False
|
||||
|
||||
async def __a_request__(self, **kwargs):
|
||||
"""异步请求"""
|
||||
|
||||
try:
|
||||
self.ret:dict = await self.request_fun(**kwargs)
|
||||
self.request_ready = True
|
||||
except aiE.APIConnectionError as e:
|
||||
self.error_info = "{}\n请检查网络连接或代理是否正常".format(e)
|
||||
raise ConnectionError(self.error_info)
|
||||
except ValueError as e:
|
||||
self.error_info = "{}\n该错误可能是由于http_proxy格式设置错误引起的"
|
||||
except Exception as e:
|
||||
self.error_info = "{}\n由于请求异常产生的未知错误,请查看日志".format(e)
|
||||
raise Exception(self.error_info)
|
||||
|
||||
def request(self, **kwargs):
|
||||
"""向接口发起请求"""
|
||||
|
||||
if self.proxy != None: #异步请求
|
||||
self.request_ready = False
|
||||
loop = asyncio.new_event_loop()
|
||||
self.runtime = threading.Thread(
|
||||
target=loop.run_until_complete,
|
||||
args=(self.__a_request__(**kwargs),)
|
||||
)
|
||||
self.runtime.start()
|
||||
else: #同步请求
|
||||
self.ret = self.request_fun(**kwargs)
|
||||
|
||||
def __msg_handle__(self, msg):
|
||||
"""将prompt dict转换成接口需要的格式"""
|
||||
return msg
|
||||
|
||||
def ret_handle(self):
|
||||
'''
|
||||
API消息返回处理函数
|
||||
若重写该方法,应检查异步线程状态,或在需要检查处super该方法
|
||||
'''
|
||||
if self.runtime != None and isinstance(self.runtime, threading.Thread):
|
||||
self.runtime.join(self.time_out)
|
||||
if self.request_ready:
|
||||
return
|
||||
raise Exception(self.error_info)
|
||||
|
||||
def get_total_tokens(self):
|
||||
try:
|
||||
return self.ret['usage']['total_tokens']
|
||||
except:
|
||||
return 0
|
||||
|
||||
def get_message(self):
|
||||
return self.message
|
||||
|
||||
def get_response(self):
|
||||
return self.ret
|
||||
|
||||
|
||||
class ChatCompletionModel(ModelRequest):
|
||||
"""ChatCompletion接口的请求实现"""
|
||||
|
||||
Chat_role = ['system', 'user', 'assistant']
|
||||
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
|
||||
if http_proxy == None:
|
||||
request_fun = openai.ChatCompletion.create
|
||||
else:
|
||||
request_fun = openai.ChatCompletion.acreate
|
||||
self.can_chat = True
|
||||
super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
|
||||
|
||||
def request(self, prompts, **kwargs):
|
||||
prompts = self.__msg_handle__(prompts)
|
||||
kwargs['messages'] = prompts
|
||||
super().request(**kwargs)
|
||||
self.ret_handle()
|
||||
|
||||
def __msg_handle__(self, msgs):
|
||||
temp_msgs = []
|
||||
# 把msgs拷贝进temp_msgs
|
||||
for msg in msgs:
|
||||
temp_msgs.append(msg.copy())
|
||||
return temp_msgs
|
||||
|
||||
def get_message(self):
|
||||
return self.ret["choices"][0]["message"]['content'] #需要时直接加载加快请求速度,降低内存消耗
|
||||
|
||||
|
||||
class CompletionModel(ModelRequest):
|
||||
"""Completion接口的请求实现"""
|
||||
|
||||
def __init__(self, model_name, user_name, http_proxy:str = None, **kwargs):
|
||||
if http_proxy == None:
|
||||
request_fun = openai.Completion.create
|
||||
else:
|
||||
request_fun = openai.Completion.acreate
|
||||
super().__init__(model_name, user_name, request_fun, http_proxy, **kwargs)
|
||||
|
||||
def request(self, prompts, **kwargs):
|
||||
prompts = self.__msg_handle__(prompts)
|
||||
kwargs['prompt'] = prompts
|
||||
super().request(**kwargs)
|
||||
self.ret_handle()
|
||||
|
||||
def __msg_handle__(self, msgs):
|
||||
prompt = ''
|
||||
for msg in msgs:
|
||||
prompt = prompt + "{}: {}\n".format(msg['role'], msg['content'])
|
||||
# for msg in msgs:
|
||||
# if msg['role'] == 'assistant':
|
||||
# prompt = prompt + "{}\n".format(msg['content'])
|
||||
# else:
|
||||
# prompt = prompt + "{}:{}\n".format(msg['role'] , msg['content'])
|
||||
prompt = prompt + "assistant: "
|
||||
return prompt
|
||||
|
||||
def get_message(self):
|
||||
return self.ret["choices"][0]["text"]
|
||||
|
||||
|
||||
def create_openai_model_request(model_name: str, user_name: str = 'user', http_proxy:str = None) -> ModelRequest:
|
||||
"""使用给定的模型名称创建模型请求对象"""
|
||||
def select_request_cls(client: openai.Client, model_name: str, messages: list, args: dict) -> api_model.RequestBase:
|
||||
if model_name in CHAT_COMPLETION_MODELS:
|
||||
model = ChatCompletionModel(model_name, user_name, http_proxy)
|
||||
return api_chat_completion.ChatCompletionRequest(client, model_name, messages, **args)
|
||||
elif model_name in COMPLETION_MODELS:
|
||||
model = CompletionModel(model_name, user_name, http_proxy)
|
||||
else :
|
||||
log = "找不到模型[{}],请检查配置文件".format(model_name)
|
||||
logging.error(log)
|
||||
raise IndexError(log)
|
||||
logging.debug("使用接口[{}]创建模型请求[{}]".format(model.__class__.__name__, model_name))
|
||||
return model
|
||||
return api_completion.CompletionRequest(client, model_name, messages, **args)
|
||||
raise ValueError("不支持模型[{}],请检查配置文件".format(model_name))
|
||||
|
||||
|
||||
def count_chat_completion_tokens(messages: list, model: str) -> int:
|
||||
"""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 count_completion_tokens(messages: list, model: str) -> int:
|
||||
|
||||
try:
|
||||
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,28 +0,0 @@
|
||||
# 计费模块
|
||||
# 已弃用 https://github.com/RockChinQ/QChatGPT/issues/81
|
||||
|
||||
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]
|
||||
@@ -8,13 +8,13 @@ import threading
|
||||
import time
|
||||
import json
|
||||
|
||||
import pkg.openai.manager
|
||||
import pkg.openai.modelmgr
|
||||
import pkg.database.manager
|
||||
import pkg.utils.context
|
||||
from ..openai import manager as openai_manager
|
||||
from ..openai import modelmgr as openai_modelmgr
|
||||
from ..database import manager as database_manager
|
||||
from ..utils import context as context
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
from ..plugin import host as plugin_host
|
||||
from ..plugin import models as plugin_models
|
||||
|
||||
# 运行时保存的所有session
|
||||
sessions = {}
|
||||
@@ -25,57 +25,27 @@ class SessionOfflineStatus:
|
||||
EXPLICITLY_CLOSED = 'explicitly_closed'
|
||||
|
||||
|
||||
# 重置session.prompt
|
||||
def reset_session_prompt(session_name, prompt):
|
||||
# 备份原始数据
|
||||
bak_path = 'logs/{}-{}.bak'.format(
|
||||
session_name,
|
||||
time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
|
||||
)
|
||||
f = open(bak_path, 'w+')
|
||||
f.write(prompt)
|
||||
f.close()
|
||||
# 生成新数据
|
||||
config = pkg.utils.context.get_config()
|
||||
prompt = [
|
||||
{
|
||||
'role': 'system',
|
||||
'content': config.default_prompt['default'] if type(config.default_prompt) == dict else config.default_prompt
|
||||
}
|
||||
]
|
||||
# 警告
|
||||
logging.warning(
|
||||
"""
|
||||
用户[{}]的数据已被重置,有可能是因为数据版本过旧或存储错误
|
||||
原始数据将备份在:
|
||||
{}""".format(session_name, bak_path)
|
||||
) # 为保证多行文本格式正确故无缩进
|
||||
return prompt
|
||||
|
||||
|
||||
# 从数据加载session
|
||||
def load_sessions():
|
||||
"""从数据库加载sessions"""
|
||||
|
||||
global sessions
|
||||
|
||||
db_inst = pkg.utils.context.get_database_manager()
|
||||
db_inst = context.get_database_manager()
|
||||
|
||||
session_data = db_inst.load_valid_sessions()
|
||||
|
||||
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.name = session_name
|
||||
temp_session.create_timestamp = session_data[session_name]['create_timestamp']
|
||||
temp_session.last_interact_timestamp = session_data[session_name]['last_interact_timestamp']
|
||||
try:
|
||||
temp_session.prompt = json.loads(session_data[session_name]['prompt'])
|
||||
temp_session.token_counts = json.loads(session_data[session_name]['token_counts'])
|
||||
except Exception:
|
||||
temp_session.prompt = reset_session_prompt(session_name, session_data[session_name]['prompt'])
|
||||
temp_session.persistence()
|
||||
|
||||
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 []
|
||||
|
||||
@@ -83,7 +53,7 @@ def load_sessions():
|
||||
|
||||
|
||||
# 获取指定名称的session,如果不存在则创建一个新的
|
||||
def get_session(session_name: str):
|
||||
def get_session(session_name: str) -> 'Session':
|
||||
global sessions
|
||||
if session_name not in sessions:
|
||||
sessions[session_name] = Session(session_name)
|
||||
@@ -107,9 +77,6 @@ class Session:
|
||||
prompt = []
|
||||
"""使用list来保存会话中的回合"""
|
||||
|
||||
token_counts = []
|
||||
"""每个回合的token数量"""
|
||||
|
||||
default_prompt = []
|
||||
"""本session的默认prompt"""
|
||||
|
||||
@@ -141,9 +108,9 @@ class Session:
|
||||
import pkg.openai.dprompt as dprompt
|
||||
|
||||
if use_default is None:
|
||||
use_default = dprompt.get_current()
|
||||
use_default = dprompt.mode_inst().get_using_name()
|
||||
|
||||
current_default_prompt = dprompt.get_prompt(use_default)
|
||||
current_default_prompt, _ = dprompt.mode_inst().get_prompt(use_default)
|
||||
return current_default_prompt
|
||||
|
||||
def __init__(self, name: str):
|
||||
@@ -173,17 +140,17 @@ class Session:
|
||||
if self.create_timestamp != create_timestamp or self not in sessions.values():
|
||||
return
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if int(time.time()) - self.last_interact_timestamp > config.session_expire_time:
|
||||
config = context.get_config_manager().data
|
||||
if int(time.time()) - self.last_interact_timestamp > config['session_expire_time']:
|
||||
logging.info('session {} 已过期'.format(self.name))
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'session_name': self.name,
|
||||
'session': self,
|
||||
'session_expire_time': config.session_expire_time
|
||||
'session_expire_time': config['session_expire_time']
|
||||
}
|
||||
event = pkg.plugin.host.emit(plugin_models.SessionExpired, **args)
|
||||
event = plugin_host.emit(plugin_models.SessionExpired, **args)
|
||||
if event.is_prevented_default():
|
||||
return
|
||||
|
||||
@@ -195,8 +162,15 @@ class Session:
|
||||
|
||||
# 请求回复
|
||||
# 这个函数是阻塞的
|
||||
def append(self, text: str) -> str:
|
||||
"""向session中添加一条消息,返回接口回复"""
|
||||
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())
|
||||
|
||||
@@ -208,48 +182,166 @@ class Session:
|
||||
'default_prompt': self.default_prompt,
|
||||
}
|
||||
|
||||
event = pkg.plugin.host.emit(plugin_models.SessionFirstMessageReceived, **args)
|
||||
event = plugin_host.emit(plugin_models.SessionFirstMessageReceived, **args)
|
||||
if event.is_prevented_default():
|
||||
return None
|
||||
return None, None, None
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
max_length = config.prompt_submit_length if hasattr(config, "prompt_submit_length") else 1024
|
||||
config = context.get_config_manager().data
|
||||
max_length = config['prompt_submit_length']
|
||||
|
||||
prompts, counts = self.cut_out(text, max_length)
|
||||
local_default_prompt = self.default_prompt.copy()
|
||||
local_prompt = self.prompt.copy()
|
||||
|
||||
# 计算请求前的prompt数量
|
||||
total_token_before_query = 0
|
||||
for token_count in counts:
|
||||
total_token_before_query += token_count
|
||||
# 触发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请求补全
|
||||
message, total_token = pkg.utils.context.get_openai_manager().request_completion(
|
||||
prompts,
|
||||
)
|
||||
# message, total_token = pkg.utils.context.get_openai_manager().request_completion(
|
||||
# prompts,
|
||||
# )
|
||||
|
||||
# 成功获取,处理回复
|
||||
res_test = message
|
||||
res_ans = res_test
|
||||
|
||||
# 去除开头可能的提示
|
||||
res_ans_spt = res_test.split("\n\n")
|
||||
if len(res_ans_spt) > 1:
|
||||
del (res_ans_spt[0])
|
||||
res_ans = '\n\n'.join(res_ans_spt)
|
||||
# res_test = message
|
||||
res_ans = res_text.strip()
|
||||
|
||||
# 将此次对话的双方内容加入到prompt中
|
||||
self.prompt.append({'role': 'user', 'content': text})
|
||||
self.prompt.append({'role': 'assistant', 'content': res_ans})
|
||||
# self.prompt.append({'role': 'user', 'content': text})
|
||||
# self.prompt.append({'role': 'assistant', 'content': res_ans})
|
||||
if text:
|
||||
self.prompt.append({'role': 'user', 'content': text})
|
||||
# 添加pending_msgs
|
||||
self.prompt += pending_msgs
|
||||
|
||||
# 向token_counts中添加本回合的token数量
|
||||
self.token_counts.append(total_token-total_token_before_query)
|
||||
logging.debug("本回合使用token: {}, session counts: {}".format(total_token-total_token_before_query, self.token_counts))
|
||||
# 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:
|
||||
self.just_switched_to_exist_session = False
|
||||
self.set_ongoing()
|
||||
|
||||
return res_ans if res_ans[0] != '\n' else res_ans[1:]
|
||||
# 上报使用量数据
|
||||
session_type = session_name_spt[0]
|
||||
session_id = session_name_spt[1]
|
||||
|
||||
ability_provider = "QChatGPT.Text"
|
||||
usage = total_tokens
|
||||
model_name = context.get_config_manager().data['completion_api_params']['model']
|
||||
response_seconds = int(time.time() - start_time)
|
||||
retry_times = -1 # 暂不记录
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
return res_ans if res_ans[0] != '\n' else res_ans[1:], finish_reason, funcs
|
||||
|
||||
# 删除上一回合并返回上一回合的问题
|
||||
def undo(self) -> str:
|
||||
@@ -267,7 +359,7 @@ class Session:
|
||||
return question
|
||||
|
||||
# 构建对话体
|
||||
def cut_out(self, msg: str, max_tokens: int) -> tuple[list, list]:
|
||||
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)
|
||||
@@ -280,49 +372,42 @@ class Session:
|
||||
|
||||
# 包装目前的对话回合内容
|
||||
changable_prompts = []
|
||||
changable_counts = []
|
||||
# 倒着来, 遍历prompt的步长为2, 遍历tokens_counts的步长为1
|
||||
changable_index = len(self.prompt) - 1
|
||||
token_count_index = len(self.token_counts) - 1
|
||||
|
||||
packed_tokens = 0
|
||||
use_model = context.get_config_manager().data['completion_api_params']['model']
|
||||
|
||||
while changable_index >= 0 and token_count_index >= 0:
|
||||
if packed_tokens + self.token_counts[token_count_index] > max_tokens:
|
||||
ptr = len(prompt) - 1
|
||||
|
||||
# 直接从后向前扫描拼接,不管是否是整回合
|
||||
while ptr >= 0:
|
||||
if openai_modelmgr.count_tokens(prompt[ptr:ptr+1]+changable_prompts, use_model) > max_tokens:
|
||||
break
|
||||
|
||||
changable_prompts.insert(0, self.prompt[changable_index])
|
||||
changable_prompts.insert(0, self.prompt[changable_index - 1])
|
||||
changable_counts.insert(0, self.token_counts[token_count_index])
|
||||
packed_tokens += self.token_counts[token_count_index]
|
||||
changable_prompts.insert(0, prompt[ptr])
|
||||
|
||||
changable_index -= 2
|
||||
token_count_index -= 1
|
||||
ptr -= 1
|
||||
|
||||
# 将default_prompt和changable_prompts合并
|
||||
result_prompt = self.default_prompt + changable_prompts
|
||||
result_prompt = default_prompt + changable_prompts
|
||||
|
||||
# 添加当前问题
|
||||
result_prompt.append(
|
||||
{
|
||||
'role': 'user',
|
||||
'content': msg
|
||||
}
|
||||
)
|
||||
if msg:
|
||||
result_prompt.append(
|
||||
{
|
||||
'role': 'user',
|
||||
'content': msg
|
||||
}
|
||||
)
|
||||
|
||||
logging.debug('cut_out: {}\nchangable section tokens: {}\npacked counts: {}\nsession counts: {}'.format(json.dumps(result_prompt, ensure_ascii=False, indent=4),
|
||||
packed_tokens,
|
||||
changable_counts,
|
||||
self.token_counts))
|
||||
logging.debug("cut_out: {}".format(json.dumps(result_prompt, ensure_ascii=False, indent=4)))
|
||||
|
||||
return result_prompt, changable_counts
|
||||
return result_prompt, openai_modelmgr.count_tokens(changable_prompts, use_model)
|
||||
|
||||
# 持久化session
|
||||
def persistence(self):
|
||||
if self.prompt == self.get_default_prompt():
|
||||
return
|
||||
|
||||
db_inst = pkg.utils.context.get_database_manager()
|
||||
db_inst = context.get_database_manager()
|
||||
|
||||
name_spt = self.name.split('_')
|
||||
|
||||
@@ -333,7 +418,7 @@ class Session:
|
||||
json.dumps(self.prompt), json.dumps(self.default_prompt), json.dumps(self.token_counts))
|
||||
|
||||
# 重置session
|
||||
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None):
|
||||
def reset(self, explicit: bool = False, expired: bool = False, schedule_new: bool = True, use_prompt: str = None, persist: bool = False):
|
||||
if self.prompt:
|
||||
self.persistence()
|
||||
if explicit:
|
||||
@@ -344,14 +429,15 @@ class Session:
|
||||
}
|
||||
|
||||
# 此事件不支持阻止默认行为
|
||||
_ = pkg.plugin.host.emit(plugin_models.SessionExplicitReset, **args)
|
||||
_ = plugin_host.emit(plugin_models.SessionExplicitReset, **args)
|
||||
|
||||
pkg.utils.context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
||||
context.get_database_manager().explicit_close_session(self.name, self.create_timestamp)
|
||||
|
||||
if expired:
|
||||
pkg.utils.context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
|
||||
context.get_database_manager().set_session_expired(self.name, self.create_timestamp)
|
||||
|
||||
self.default_prompt = self.get_default_prompt(use_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())
|
||||
@@ -365,11 +451,11 @@ class Session:
|
||||
|
||||
# 将本session的数据库状态设置为on_going
|
||||
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
|
||||
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:
|
||||
return None
|
||||
else:
|
||||
@@ -377,12 +463,10 @@ class Session:
|
||||
|
||||
self.create_timestamp = last_one['create_timestamp']
|
||||
self.last_interact_timestamp = last_one['last_interact_timestamp']
|
||||
try:
|
||||
self.prompt = json.loads(last_one['prompt'])
|
||||
self.token_counts = json.loads(last_one['token_counts'])
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.prompt = reset_session_prompt(self.name, last_one['prompt'])
|
||||
self.persistence()
|
||||
|
||||
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
|
||||
@@ -390,7 +474,7 @@ class Session:
|
||||
|
||||
# 切换到下一个session
|
||||
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:
|
||||
return None
|
||||
else:
|
||||
@@ -398,25 +482,23 @@ class Session:
|
||||
|
||||
self.create_timestamp = next_one['create_timestamp']
|
||||
self.last_interact_timestamp = next_one['last_interact_timestamp']
|
||||
try:
|
||||
self.prompt = json.loads(next_one['prompt'])
|
||||
self.token_counts = json.loads(next_one['token_counts'])
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.prompt = reset_session_prompt(self.name, next_one['prompt'])
|
||||
self.persistence()
|
||||
|
||||
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
|
||||
return self
|
||||
|
||||
def list_history(self, capacity: int = 10, page: int = 0):
|
||||
return pkg.utils.context.get_database_manager().list_history(self.name, capacity, page)
|
||||
return context.get_database_manager().list_history(self.name, capacity, page)
|
||||
|
||||
def delete_history(self, index: int) -> bool:
|
||||
return pkg.utils.context.get_database_manager().delete_history(self.name, index)
|
||||
return context.get_database_manager().delete_history(self.name, index)
|
||||
|
||||
def delete_all_history(self) -> bool:
|
||||
return pkg.utils.context.get_database_manager().delete_all_history(self.name)
|
||||
return context.get_database_manager().delete_all_history(self.name)
|
||||
|
||||
def draw_image(self, prompt: str):
|
||||
return pkg.utils.context.get_openai_manager().request_image(prompt)
|
||||
return context.get_openai_manager().request_image(prompt)
|
||||
|
||||
@@ -7,16 +7,24 @@ import pkgutil
|
||||
import sys
|
||||
import shutil
|
||||
import traceback
|
||||
import time
|
||||
import re
|
||||
|
||||
import pkg.utils.context as context
|
||||
import pkg.plugin.switch as switch
|
||||
import pkg.plugin.settings as settings
|
||||
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__ = {}
|
||||
"""
|
||||
插件列表
|
||||
"""插件列表
|
||||
|
||||
示例:
|
||||
{
|
||||
@@ -35,14 +43,24 @@ __plugins__ = {}
|
||||
},
|
||||
"instance": None
|
||||
}
|
||||
}"""
|
||||
}
|
||||
"""
|
||||
|
||||
__plugins_order__ = []
|
||||
"""插件顺序"""
|
||||
|
||||
__enable_content_functions__ = True
|
||||
"""是否启用内容函数"""
|
||||
|
||||
__callable_functions__ = []
|
||||
"""供GPT调用的函数结构"""
|
||||
|
||||
__function_inst_map__: dict[str, callable] = {}
|
||||
"""函数名:实例 映射"""
|
||||
|
||||
|
||||
def generate_plugin_order():
|
||||
""" 根据__plugin__生成插件初始顺序,无视是否启用 """
|
||||
"""根据__plugin__生成插件初始顺序,无视是否启用"""
|
||||
global __plugins_order__
|
||||
__plugins_order__ = []
|
||||
for plugin_name in __plugins__:
|
||||
@@ -50,13 +68,15 @@ def generate_plugin_order():
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -64,31 +84,42 @@ def iter_plugins_name():
|
||||
__current_module_path__ = ""
|
||||
|
||||
|
||||
def walk_plugin_path(module, prefix='', path_prefix=''):
|
||||
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 + '/')
|
||||
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'
|
||||
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.info('加载模块: plugins/{} 成功'.format(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()))
|
||||
logging.error(
|
||||
"加载模块: plugins/{} 失败: {}".format(
|
||||
path_prefix + item.name + ".py", sys.exc_info()
|
||||
)
|
||||
)
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def load_plugins():
|
||||
""" 加载插件 """
|
||||
logging.info("加载插件")
|
||||
"""加载插件"""
|
||||
logging.debug("加载插件")
|
||||
PluginHost()
|
||||
walk_plugin_path(__import__('plugins'))
|
||||
walk_plugin_path(__import__("plugins"))
|
||||
|
||||
logging.debug(__plugins__)
|
||||
|
||||
@@ -100,25 +131,40 @@ def load_plugins():
|
||||
# 加载插件顺序
|
||||
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.info("初始化插件")
|
||||
"""初始化插件"""
|
||||
logging.debug("初始化插件")
|
||||
import pkg.plugin.models as models
|
||||
|
||||
successfully_initialized_plugins = []
|
||||
|
||||
for plugin in iter_plugins():
|
||||
if not plugin['enabled']:
|
||||
continue
|
||||
# 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']))
|
||||
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.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:
|
||||
@@ -133,62 +179,226 @@ def unload_plugins():
|
||||
# logging.error("插件{}卸载时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||
|
||||
|
||||
def install_plugin(repo_url: str):
|
||||
""" 安装插件,从git储存库获取并解决依赖 """
|
||||
try:
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.ensure_dulwich()
|
||||
except:
|
||||
pass
|
||||
def get_github_plugin_repo_label(repo_url: str) -> list[str]:
|
||||
"""获取username, repo"""
|
||||
|
||||
try:
|
||||
import dulwich
|
||||
except ModuleNotFoundError:
|
||||
raise Exception("dulwich模块未安装,请查看 https://github.com/RockChinQ/QChatGPT/issues/77")
|
||||
# 提取 username/repo , 正则表达式
|
||||
repo = re.findall(
|
||||
r"(?:https?://github\.com/|git@github\.com:)([^/]+/[^/]+?)(?:\.git|/|$)",
|
||||
repo_url,
|
||||
)
|
||||
|
||||
from dulwich import porcelain
|
||||
if len(repo) > 0: # github
|
||||
return repo[0].split("/")
|
||||
else:
|
||||
return None
|
||||
|
||||
logging.info("克隆插件储存库: {}".format(repo_url))
|
||||
repo = porcelain.clone(repo_url, "plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/", checkout=True)
|
||||
|
||||
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("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt"):
|
||||
if os.path.exists(path + "/requirements.txt"):
|
||||
logging.info("检测到requirements.txt,正在安装依赖")
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.install_requirements("plugins/"+repo_url.split(".git")[0].split("/")[-1]+"/requirements.txt")
|
||||
|
||||
import main
|
||||
main.reset_logging()
|
||||
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("\\", "/")
|
||||
plugin_path = __plugins__[plugin_name]["path"].replace("\\", "/")
|
||||
|
||||
# 剪切路径为plugins/插件名
|
||||
plugin_path = plugin_path.split("plugins/")[1].split("/")[0]
|
||||
|
||||
# 删除文件夹
|
||||
shutil.rmtree("plugins/"+plugin_path)
|
||||
return "plugins/"+plugin_path
|
||||
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__ = {}
|
||||
""" 返回值
|
||||
@@ -213,7 +423,7 @@ class EventContext:
|
||||
self.__return_value__[key] = []
|
||||
self.__return_value__[key].append(ret)
|
||||
|
||||
def get_return(self, key: str):
|
||||
def get_return(self, key: str) -> list:
|
||||
"""获取key的所有返回值"""
|
||||
if key in self.__return_value__:
|
||||
return self.__return_value__[key]
|
||||
@@ -251,8 +461,9 @@ class EventContext:
|
||||
|
||||
|
||||
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)
|
||||
@@ -262,7 +473,9 @@ class PluginHost:
|
||||
"""插件宿主"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化插件宿主"""
|
||||
context.set_plugin_host(self)
|
||||
self.calling_gpt_session = Session([])
|
||||
|
||||
def get_runtime_context(self) -> context:
|
||||
"""获取运行时上下文(pkg.utils.context模块的对象)
|
||||
@@ -277,27 +490,32 @@ class PluginHost:
|
||||
"""获取机器人对象"""
|
||||
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):
|
||||
"""发送私聊消息"""
|
||||
asyncio.run(self.get_bot().send_friend_message(person, message))
|
||||
self.get_bot_adapter().send_message("person", person, message)
|
||||
|
||||
def send_group_message(self, group, message):
|
||||
"""发送群消息"""
|
||||
asyncio.run(self.get_bot().send_group_message(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']:
|
||||
if not plugin["enabled"]:
|
||||
continue
|
||||
|
||||
# if plugin['instance'] is None:
|
||||
@@ -309,9 +527,11 @@ class PluginHost:
|
||||
# logging.error("插件 {} 初始化时发生错误: {}".format(plugin['name'], sys.exc_info()))
|
||||
# continue
|
||||
|
||||
if 'hooks' not in plugin or event_name not in plugin['hooks']:
|
||||
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]
|
||||
@@ -319,24 +539,40 @@ class PluginHost:
|
||||
try:
|
||||
already_prevented_default = event_context.is_prevented_default()
|
||||
|
||||
kwargs['host'] = context.get_plugin_host()
|
||||
kwargs['event'] = event_context
|
||||
kwargs["host"] = context.get_plugin_host()
|
||||
kwargs["event"] = event_context
|
||||
|
||||
hook(plugin['instance'], **kwargs)
|
||||
hook(plugin["instance"], **kwargs)
|
||||
|
||||
if event_context.is_prevented_default() and not already_prevented_default:
|
||||
logging.debug("插件 {} 已要求阻止事件 {} 的默认行为".format(plugin['name'], event_name))
|
||||
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("插件{}响应事件{}时发生错误".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']))
|
||||
logging.debug("插件 {} 阻止了后序插件的执行".format(plugin["name"]))
|
||||
break
|
||||
|
||||
logging.debug("事件 {} ({}) 处理完毕,返回值: {}".format(event_name, event_context.eid,
|
||||
event_context.__return_value__))
|
||||
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 {}
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pkg.plugin.host as host
|
||||
import pkg.utils.context
|
||||
from ..plugin import host
|
||||
from ..utils import context
|
||||
|
||||
PersonMessageReceived = "person_message_received"
|
||||
"""收到私聊消息时,在判断是否应该响应前触发
|
||||
@@ -35,18 +35,18 @@ PersonNormalMessageReceived = "person_normal_message_received"
|
||||
"""
|
||||
|
||||
PersonCommandSent = "person_command_sent"
|
||||
"""判断为应该处理的私聊指令时触发
|
||||
"""判断为应该处理的私聊命令时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
command: str 指令
|
||||
command: str 命令
|
||||
params: list[str] 参数列表
|
||||
text_message: str 完整指令文本
|
||||
text_message: str 完整命令文本
|
||||
is_admin: bool 是否为管理员
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的完整指令文本
|
||||
alter: str 修改后的完整命令文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
@@ -64,18 +64,18 @@ GroupNormalMessageReceived = "group_normal_message_received"
|
||||
"""
|
||||
|
||||
GroupCommandSent = "group_command_sent"
|
||||
"""判断为应该处理的群聊指令时触发
|
||||
"""判断为应该处理的群聊命令时触发
|
||||
kwargs:
|
||||
launcher_type: str 发起对象类型(group/person)
|
||||
launcher_id: int 发起对象ID(群号/QQ号)
|
||||
sender_id: int 发送者ID(QQ号)
|
||||
command: str 指令
|
||||
command: str 命令
|
||||
params: list[str] 参数列表
|
||||
text_message: str 完整指令文本
|
||||
text_message: str 完整命令文本
|
||||
is_admin: bool 是否为管理员
|
||||
|
||||
returns (optional):
|
||||
alter: str 修改后的完整指令文本
|
||||
alter: str 修改后的完整命令文本
|
||||
reply: list 回复消息组件列表
|
||||
"""
|
||||
|
||||
@@ -88,6 +88,8 @@ NormalMessageResponded = "normal_message_responded"
|
||||
session: pkg.openai.session.Session 会话对象
|
||||
prefix: str 回复文字消息的前缀
|
||||
response_text: str 响应文本
|
||||
finish_reason: str 响应结束原因
|
||||
funcs_called: list[str] 此次响应中调用的函数列表
|
||||
|
||||
returns (optional):
|
||||
prefix: str 修改后的回复文字消息的前缀
|
||||
@@ -132,18 +134,64 @@ KeySwitched = "key_switched"
|
||||
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(event: str):
|
||||
|
||||
def on(*args, **kwargs):
|
||||
"""注册事件监听器
|
||||
:param
|
||||
event: str 事件名称
|
||||
"""
|
||||
return Plugin.on(event)
|
||||
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:
|
||||
"""插件基类"""
|
||||
|
||||
@@ -176,6 +224,34 @@ class Plugin:
|
||||
|
||||
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):
|
||||
"""注册插件, 此函数作为装饰器使用
|
||||
@@ -209,7 +285,7 @@ def register(name: str, description: str, version: str, author: str):
|
||||
cls.description = description
|
||||
cls.version = version
|
||||
cls.author = author
|
||||
cls.host = pkg.utils.context.get_plugin_host()
|
||||
cls.host = context.get_plugin_host()
|
||||
cls.enabled = True
|
||||
cls.path = host.__current_module_path__
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import pkg.plugin.host as host
|
||||
import logging
|
||||
|
||||
from ..plugin import host
|
||||
|
||||
def wrapper_dict_from_runtime_context() -> dict:
|
||||
"""从变量中包装settings.json的数据字典"""
|
||||
settings = {
|
||||
"order": []
|
||||
"order": [],
|
||||
"functions": {
|
||||
"enabled": host.__enable_content_functions__
|
||||
}
|
||||
}
|
||||
|
||||
for plugin_name in host.__plugins_order__:
|
||||
@@ -22,6 +25,11 @@ def apply_settings(settings: dict):
|
||||
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数据"""
|
||||
@@ -78,6 +86,17 @@ def load_settings():
|
||||
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:
|
||||
|
||||
@@ -3,11 +3,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pkg.plugin.host as host
|
||||
from ..plugin import host
|
||||
|
||||
|
||||
def wrapper_dict_from_plugin_list() -> dict:
|
||||
""" 将插件列表转换为开关json """
|
||||
"""将插件列表转换为开关json"""
|
||||
switch = {}
|
||||
|
||||
for plugin_name in host.__plugins__:
|
||||
@@ -28,9 +28,14 @@ def apply_switch(switch: dict):
|
||||
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
|
||||
|
||||
@@ -41,7 +46,7 @@ def dump_switch():
|
||||
|
||||
|
||||
def load_switch():
|
||||
""" 加载开关数据 """
|
||||
"""加载开关数据"""
|
||||
logging.debug("加载开关数据")
|
||||
# 读取plugins/switch.json
|
||||
|
||||
|
||||
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
|
||||
@@ -1,18 +1,18 @@
|
||||
import pkg.utils.context
|
||||
from ..utils import context
|
||||
|
||||
|
||||
def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool:
|
||||
if not pkg.utils.context.get_qqbot_manager().enable_banlist:
|
||||
if not context.get_qqbot_manager().enable_banlist:
|
||||
return False
|
||||
|
||||
result = False
|
||||
|
||||
if launcher_type == 'group':
|
||||
# 检查是否显式声明发起人QQ要被person忽略
|
||||
if sender_id in pkg.utils.context.get_qqbot_manager().ban_person:
|
||||
if sender_id in context.get_qqbot_manager().ban_person:
|
||||
result = True
|
||||
else:
|
||||
for group_rule in pkg.utils.context.get_qqbot_manager().ban_group:
|
||||
for group_rule in context.get_qqbot_manager().ban_group:
|
||||
if type(group_rule) == int:
|
||||
if group_rule == launcher_id: # 此群群号被禁用
|
||||
result = True
|
||||
@@ -32,7 +32,7 @@ def is_banned(launcher_type: str, launcher_id: int, sender_id: int) -> bool:
|
||||
|
||||
else:
|
||||
# ban_person, 与群规则相同
|
||||
for person_rule in pkg.utils.context.get_qqbot_manager().ban_person:
|
||||
for person_rule in context.get_qqbot_manager().ban_person:
|
||||
if type(person_rule) == int:
|
||||
if person_rule == launcher_id:
|
||||
result = True
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
import os
|
||||
import time
|
||||
import base64
|
||||
import typing
|
||||
|
||||
import config
|
||||
from mirai.models.message import MessageComponent, MessageChain, Image
|
||||
from mirai.models.message import ForwardMessageNode
|
||||
from mirai.models.base import MiraiBaseModel
|
||||
from typing import List
|
||||
import pkg.utils.context as context
|
||||
import pkg.utils.text2img as text2img
|
||||
|
||||
from ..utils import text2img
|
||||
from ..utils import context
|
||||
|
||||
|
||||
class ForwardMessageDiaplay(MiraiBaseModel):
|
||||
title: str = "群聊的聊天记录"
|
||||
brief: str = "[聊天记录]"
|
||||
source: str = "聊天记录"
|
||||
preview: List[str] = []
|
||||
preview: typing.List[str] = []
|
||||
summary: str = "查看x条转发消息"
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class Forward(MessageComponent):
|
||||
"""消息组件类型。"""
|
||||
display: ForwardMessageDiaplay
|
||||
"""显示信息"""
|
||||
node_list: List[ForwardMessageNode]
|
||||
node_list: typing.List[ForwardMessageNode]
|
||||
"""转发消息节点列表。"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
if len(args) == 1:
|
||||
@@ -64,18 +64,16 @@ def text_to_image(text: str) -> MessageComponent:
|
||||
|
||||
def check_text(text: str) -> list:
|
||||
"""检查文本是否为长消息,并转换成该使用的消息链组件"""
|
||||
if not hasattr(config, 'blob_message_threshold'):
|
||||
return [text]
|
||||
|
||||
if len(text) > config.blob_message_threshold:
|
||||
if not hasattr(config, 'blob_message_strategy'):
|
||||
raise AttributeError('未定义长消息处理策略')
|
||||
config = context.get_config_manager().data
|
||||
|
||||
if len(text) > config['blob_message_threshold']:
|
||||
|
||||
# logging.info("长消息: {}".format(text))
|
||||
if config.blob_message_strategy == 'image':
|
||||
if config['blob_message_strategy'] == 'image':
|
||||
# 转换成图片
|
||||
return [text_to_image(text)]
|
||||
elif config.blob_message_strategy == 'forward':
|
||||
elif config['blob_message_strategy'] == 'forward':
|
||||
|
||||
# 包装转发消息
|
||||
display = ForwardMessageDiaplay(
|
||||
@@ -87,7 +85,7 @@ def check_text(text: str) -> list:
|
||||
)
|
||||
|
||||
node = ForwardMessageNode(
|
||||
sender_id=config.mirai_http_api_config['qq'],
|
||||
sender_id=config['mirai_http_api_config']['qq'],
|
||||
sender_name='bot',
|
||||
message_chain=MessageChain([text])
|
||||
)
|
||||
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
from pkg.qqbot.cmds.model import command
|
||||
|
||||
import logging
|
||||
|
||||
from mirai import Image
|
||||
|
||||
import config
|
||||
import pkg.openai.session
|
||||
|
||||
@command(
|
||||
"draw",
|
||||
"使用DALL·E模型作画",
|
||||
"!draw <图片提示语>",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_draw(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""使用DALL·E模型作画"""
|
||||
reply = []
|
||||
|
||||
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))
|
||||
|
||||
return reply
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
# 指令模型
|
||||
import logging
|
||||
|
||||
commands = []
|
||||
"""已注册的指令类
|
||||
{
|
||||
"name": "指令名",
|
||||
"description": "指令描述",
|
||||
"usage": "指令用法",
|
||||
"aliases": ["别名1", "别名2"],
|
||||
"admin_only": "是否仅管理员可用",
|
||||
"func": "指令执行函数"
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def command(name: str, description: str, usage: str, aliases: list = None, admin_only: bool = False):
|
||||
"""指令装饰器"""
|
||||
|
||||
def wrapper(fun):
|
||||
commands.append({
|
||||
"name": name,
|
||||
"description": description,
|
||||
"usage": usage,
|
||||
"aliases": aliases,
|
||||
"admin_only": admin_only,
|
||||
"func": fun
|
||||
})
|
||||
return fun
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def search(cmd: str) -> dict:
|
||||
"""查找指令"""
|
||||
for command in commands:
|
||||
if (command["name"] == cmd) or (cmd in command["aliases"]):
|
||||
return command
|
||||
return None
|
||||
|
||||
|
||||
import pkg.qqbot.cmds.func
|
||||
import pkg.qqbot.cmds.system
|
||||
import pkg.qqbot.cmds.session
|
||||
import pkg.qqbot.cmds.plugin
|
||||
@@ -1,129 +0,0 @@
|
||||
from pkg.qqbot.cmds.model import command
|
||||
import pkg.utils.context
|
||||
import pkg.plugin.switch as plugin_switch
|
||||
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
|
||||
|
||||
def plugin_operation(cmd, params, is_admin):
|
||||
reply = []
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.utils.updater as updater
|
||||
|
||||
plugin_list = plugin_host.__plugins__
|
||||
|
||||
if len(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]
|
||||
elif params[0] == 'update':
|
||||
# 更新所有插件
|
||||
if is_admin:
|
||||
def closure():
|
||||
import pkg.utils.context
|
||||
updated = []
|
||||
for key in plugin_list:
|
||||
plugin = plugin_list[key]
|
||||
if updater.is_repo("/".join(plugin['path'].split('/')[:-1])):
|
||||
success = updater.pull_latest("/".join(plugin['path'].split('/')[:-1]))
|
||||
if success:
|
||||
updated.append(plugin['name'])
|
||||
|
||||
# 检查是否有requirements.txt
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("正在安装依赖...")
|
||||
for key in plugin_list:
|
||||
plugin = plugin_list[key]
|
||||
if os.path.exists("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt"):
|
||||
logging.info("{}检测到requirements.txt,安装依赖".format(plugin['name']))
|
||||
import pkg.utils.pkgmgr
|
||||
pkg.utils.pkgmgr.install_requirements("/".join(plugin['path'].split('/')[:-1])+"/requirements.txt")
|
||||
|
||||
import main
|
||||
main.reset_logging()
|
||||
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin("已更新插件: {}".format(", ".join(updated)))
|
||||
|
||||
threading.Thread(target=closure).start()
|
||||
reply = ["[bot]正在更新所有插件,请勿重复发起..."]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足"]
|
||||
elif params[0] == 'del' or params[0] == 'delete':
|
||||
if is_admin:
|
||||
if len(params) < 2:
|
||||
reply = ["[bot]err:未指定插件名"]
|
||||
else:
|
||||
plugin_name = params[1]
|
||||
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)]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
|
||||
elif params[0] == 'on' or params[0] == 'off' :
|
||||
new_status = params[0] == 'on'
|
||||
if is_admin:
|
||||
if len(params) < 2:
|
||||
reply = ["[bot]err:未指定插件名"]
|
||||
else:
|
||||
plugin_name = params[1]
|
||||
if plugin_name in plugin_list:
|
||||
plugin_list[plugin_name]['enabled'] = new_status
|
||||
plugin_switch.dump_switch()
|
||||
reply = ["[bot]已{}插件: {}".format("启用" if new_status else "禁用", plugin_name)]
|
||||
else:
|
||||
reply = ["[bot]err:未找到插件: {}, 请使用!plugin指令查看插件列表".format(plugin_name)]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
|
||||
elif params[0].startswith("http"):
|
||||
if is_admin:
|
||||
|
||||
def closure():
|
||||
try:
|
||||
plugin_host.install_plugin(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]正在安装插件..."]
|
||||
else:
|
||||
reply = ["[bot]err:权限不足,请使用管理员账号私聊发起"]
|
||||
else:
|
||||
reply = ["[bot]err:未知参数: {}".format(params)]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"plugin",
|
||||
"插件相关操作",
|
||||
"!plugin\n!plugin <插件仓库地址>\!plugin update\n!plugin del <插件名>\n!plugin on <插件名>\n!plugin off <插件名>",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_plugin(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""插件相关操作"""
|
||||
reply = plugin_operation(cmd, params, is_admin)
|
||||
return 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
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
# 会话管理相关指令
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from pkg.qqbot.cmds.model import command
|
||||
import pkg.openai.session
|
||||
import pkg.utils.context
|
||||
import config
|
||||
|
||||
@command(
|
||||
"reset",
|
||||
"重置当前会话",
|
||||
"!reset\n!reset [使用情景预设名称]",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_reset(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""重置会话"""
|
||||
reply = []
|
||||
|
||||
if len(params) == 0:
|
||||
pkg.openai.session.get_session(session_name).reset(explicit=True)
|
||||
reply = ["[bot]会话已重置"]
|
||||
else:
|
||||
pkg.openai.session.get_session(session_name).reset(explicit=True, use_prompt=params[0])
|
||||
reply = ["[bot]会话已重置,使用场景预设:{}".format(params[0])]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"last",
|
||||
"切换到前一次会话",
|
||||
"!last",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_last(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""切换到前一次会话"""
|
||||
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 reply
|
||||
|
||||
@command(
|
||||
"next",
|
||||
"切换到后一次会话",
|
||||
"!next",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_next(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: int, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""切换到后一次会话"""
|
||||
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 reply
|
||||
|
||||
|
||||
@command(
|
||||
"prompt",
|
||||
"获取当前会话的前文",
|
||||
"!prompt",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_prompt(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""获取当前会话的前文"""
|
||||
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 reply
|
||||
|
||||
|
||||
@command(
|
||||
"list",
|
||||
"列出当前会话的所有历史记录",
|
||||
"!list\n!list [页数]",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_list(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""列出当前会话的所有历史记录"""
|
||||
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 = ["[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 = ""
|
||||
try:
|
||||
msg = json.loads(results[i]['prompt'])
|
||||
except json.decoder.JSONDecodeError:
|
||||
msg = pkg.openai.session.reset_session_prompt(session_name, results[i]['prompt'])
|
||||
# 持久化
|
||||
pkg.openai.session.get_session(session_name).persistence()
|
||||
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 reply
|
||||
|
||||
|
||||
@command(
|
||||
"resend",
|
||||
"重新获取上一次问题的回复",
|
||||
"!resend",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_resend(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""重新获取上一次问题的回复"""
|
||||
reply = []
|
||||
|
||||
session = pkg.openai.session.get_session(session_name)
|
||||
to_send = session.undo()
|
||||
|
||||
mgr = pkg.utils.context.get_qqbot_manager()
|
||||
|
||||
reply = pkg.qqbot.message.process_normal_message(to_send, mgr, config,
|
||||
launcher_type, launcher_id, sender_id)
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"del",
|
||||
"删除当前会话的历史记录",
|
||||
"!del <序号>\n!del all",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_del(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""删除当前会话的历史记录"""
|
||||
reply = []
|
||||
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]参数不足, 格式: !del <序号>\n可以通过!list查看序号"]
|
||||
else:
|
||||
if params[0] == 'all':
|
||||
pkg.openai.session.get_session(session_name).delete_all_history()
|
||||
reply = ["[bot]已删除所有历史会话"]
|
||||
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 reply
|
||||
|
||||
|
||||
@command(
|
||||
"default",
|
||||
"操作情景预设",
|
||||
"!default\n!default [指定情景预设为默认]",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_default(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""操作情景预设"""
|
||||
reply = []
|
||||
|
||||
if len(params) == 0:
|
||||
# 输出目前所有情景预设
|
||||
import pkg.openai.dprompt as dprompt
|
||||
reply_str = "[bot]当前所有情景预设:\n\n"
|
||||
for key,value in dprompt.get_prompt_dict().items():
|
||||
reply_str += " - {}: {}\n".format(key,value)
|
||||
|
||||
reply_str += "\n当前默认情景预设:{}\n".format(dprompt.get_current())
|
||||
reply_str += "请使用!default <情景预设>来设置默认情景预设"
|
||||
reply = [reply_str]
|
||||
elif len(params) >0 and is_admin:
|
||||
# 设置默认情景
|
||||
import pkg.openai.dprompt as dprompt
|
||||
try:
|
||||
dprompt.set_current(params[0])
|
||||
reply = ["[bot]已设置默认情景预设为:{}".format(dprompt.get_current())]
|
||||
except KeyError:
|
||||
reply = ["[bot]err: 未找到情景预设:{}".format(params[0])]
|
||||
else:
|
||||
reply = ["[bot]err: 仅管理员可设置默认情景预设"]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"delhst",
|
||||
"删除指定会话的所有历史记录",
|
||||
"!delhst <会话名称>\n!delhst all",
|
||||
[],
|
||||
True
|
||||
)
|
||||
def cmd_delhst(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""删除指定会话的所有历史记录"""
|
||||
reply = []
|
||||
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入要删除的会话名: group_<群号> 或者 person_<QQ号>, 或使用 !delhst all 删除所有会话的历史记录"]
|
||||
else:
|
||||
if params[0] == "all":
|
||||
pkg.utils.context.get_database_manager().delete_all_session_history()
|
||||
reply = ["[bot]已删除所有会话的历史记录"]
|
||||
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 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
|
||||
@@ -1,216 +0,0 @@
|
||||
from pkg.qqbot.cmds.model import command
|
||||
import pkg.utils.context
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.credit as credit
|
||||
import config
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
import json
|
||||
|
||||
@command(
|
||||
"help",
|
||||
"获取帮助信息",
|
||||
"!help",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_help(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""获取帮助信息"""
|
||||
return ["[bot]" + config.help_message]
|
||||
|
||||
|
||||
@command(
|
||||
"usage",
|
||||
"获取使用情况",
|
||||
"!usage",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_usage(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""获取使用情况"""
|
||||
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))
|
||||
# 获取此key的额度
|
||||
try:
|
||||
http_proxy = config.openai_config["http_proxy"] if "http_proxy" in config.openai_config else None
|
||||
credit_data = credit.fetch_credit_data(api_keys[key_name], http_proxy)
|
||||
reply_str += " - 使用额度:{:.2f}/{:.2f}\n".format(credit_data['total_used'],credit_data['total_granted'])
|
||||
except Exception as e:
|
||||
logging.warning("获取额度失败:{}".format(e))
|
||||
|
||||
reply = [reply_str]
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"version",
|
||||
"查看版本信息",
|
||||
"!version",
|
||||
[],
|
||||
False
|
||||
)
|
||||
def cmd_version(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""查看版本信息"""
|
||||
reply = []
|
||||
|
||||
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 reply
|
||||
|
||||
|
||||
@command(
|
||||
"reload",
|
||||
"执行热重载",
|
||||
"!reload",
|
||||
[],
|
||||
True
|
||||
)
|
||||
def cmd_reload(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""执行热重载"""
|
||||
import pkg.utils.reloader
|
||||
def reload_task():
|
||||
pkg.utils.reloader.reload_all()
|
||||
|
||||
threading.Thread(target=reload_task, daemon=True).start()
|
||||
|
||||
|
||||
@command(
|
||||
"update",
|
||||
"更新程序",
|
||||
"!update",
|
||||
[],
|
||||
True
|
||||
)
|
||||
def cmd_update(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: 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.reloader.reload_all(notify=False)
|
||||
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]正在更新,请耐心等待,请勿重复发起更新..."]
|
||||
|
||||
|
||||
def config_operation(cmd, params):
|
||||
reply = []
|
||||
config = pkg.utils.context.get_config()
|
||||
reply_str = ""
|
||||
if len(params) == 0:
|
||||
reply = ["[bot]err:请输入配置项"]
|
||||
else:
|
||||
cfg_name = params[0]
|
||||
if cfg_name == 'all':
|
||||
reply_str = "[bot]所有配置项:\n\n"
|
||||
for cfg in dir(config):
|
||||
if not cfg.startswith('__') and not cfg == 'logging':
|
||||
# 根据配置项类型进行格式化,如果是字典则转换为json并格式化
|
||||
if isinstance(getattr(config, cfg), str):
|
||||
reply_str += "{}: \"{}\"\n".format(cfg, getattr(config, cfg))
|
||||
elif isinstance(getattr(config, cfg), dict):
|
||||
# 不进行unicode转义,并格式化
|
||||
reply_str += "{}: {}\n".format(cfg,
|
||||
json.dumps(getattr(config, cfg),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str += "{}: {}\n".format(cfg, getattr(config, cfg))
|
||||
reply = [reply_str]
|
||||
elif cfg_name in dir(config):
|
||||
if len(params) == 1:
|
||||
# 按照配置项类型进行格式化
|
||||
if isinstance(getattr(config, cfg_name), str):
|
||||
reply_str = "[bot]配置项{}: \"{}\"\n".format(cfg_name, getattr(config, cfg_name))
|
||||
elif isinstance(getattr(config, cfg_name), dict):
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name,
|
||||
json.dumps(getattr(config, cfg_name),
|
||||
ensure_ascii=False, indent=4))
|
||||
else:
|
||||
reply_str = "[bot]配置项{}: {}\n".format(cfg_name, getattr(config, cfg_name))
|
||||
reply = [reply_str]
|
||||
else:
|
||||
cfg_value = " ".join(params[1:])
|
||||
# 类型转换,如果是json则转换为字典
|
||||
if cfg_value == 'true':
|
||||
cfg_value = True
|
||||
elif cfg_value == 'false':
|
||||
cfg_value = False
|
||||
elif cfg_value.isdigit():
|
||||
cfg_value = int(cfg_value)
|
||||
elif cfg_value.startswith('{') and cfg_value.endswith('}'):
|
||||
cfg_value = json.loads(cfg_value)
|
||||
else:
|
||||
try:
|
||||
cfg_value = float(cfg_value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 检查类型是否匹配
|
||||
if isinstance(getattr(config, cfg_name), type(cfg_value)):
|
||||
setattr(config, cfg_name, cfg_value)
|
||||
pkg.utils.context.set_config(config)
|
||||
reply = ["[bot]配置项{}修改成功".format(cfg_name)]
|
||||
else:
|
||||
reply = ["[bot]err:配置项{}类型不匹配".format(cfg_name)]
|
||||
|
||||
else:
|
||||
reply = ["[bot]err:未找到配置项 {}".format(cfg_name)]
|
||||
|
||||
return reply
|
||||
|
||||
|
||||
@command(
|
||||
"cfg",
|
||||
"配置文件相关操作",
|
||||
"!cfg all\n!cfg <配置项名称>\n!cfg <配置项名称> <配置项新值>",
|
||||
[],
|
||||
True
|
||||
)
|
||||
def cmd_cfg(cmd: str, params: list, session_name: str,
|
||||
text_message: str, launcher_type: str, launcher_id: int,
|
||||
sender_id: int, is_admin: bool) -> list:
|
||||
"""配置文件相关操作"""
|
||||
reply = config_operation(cmd, params)
|
||||
return 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
|
||||
@@ -1,61 +1,48 @@
|
||||
# 指令处理模块
|
||||
# 命令处理模块
|
||||
import logging
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
import pkg.utils.reloader
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.message
|
||||
import pkg.utils.credit as credit
|
||||
import pkg.qqbot.cmds.model as cmdmodel
|
||||
|
||||
from mirai import Image
|
||||
from ..qqbot.cmds import aamgr as cmdmgr
|
||||
|
||||
|
||||
|
||||
def process_command(session_name: str, text_message: str, mgr, config,
|
||||
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))] + (
|
||||
"[{}]发起命令:{}".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
|
||||
# 把!~开头的转换成!cfg
|
||||
if cmd.startswith('~'):
|
||||
params = [cmd[1:]] + params
|
||||
cmd = 'cfg'
|
||||
|
||||
# 选择指令处理函数
|
||||
cmd_obj = cmdmodel.search(cmd)
|
||||
if cmd_obj is not None and (cmd_obj['admin_only'] is False or is_admin):
|
||||
cmd_func = cmd_obj['func']
|
||||
reply = cmd_func(
|
||||
cmd=cmd,
|
||||
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,
|
||||
)
|
||||
else:
|
||||
reply = ["[bot]err:未知的指令或权限不足: " + cmd]
|
||||
# 包装参数
|
||||
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))
|
||||
mgr.notify_admin("{}命令执行失败:{}".format(session_name, e))
|
||||
logging.exception(e)
|
||||
reply = ["[bot]err:{}".format(e)]
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ..utils import context
|
||||
|
||||
|
||||
class ReplyFilter:
|
||||
sensitive_words = []
|
||||
@@ -20,12 +22,13 @@ class ReplyFilter:
|
||||
self.sensitive_words = sensitive_words
|
||||
self.mask = mask
|
||||
self.mask_word = mask_word
|
||||
import config
|
||||
if hasattr(config, 'baidu_check') and hasattr(config, 'baidu_api_key') and hasattr(config, 'baidu_secret_key'):
|
||||
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
|
||||
|
||||
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)
|
||||
@@ -50,8 +53,8 @@ class ReplyFilter:
|
||||
|
||||
# 百度云审核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",
|
||||
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"))
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import re
|
||||
|
||||
from ..utils import context
|
||||
|
||||
|
||||
def ignore(msg: str) -> bool:
|
||||
"""检查消息是否应该被忽略"""
|
||||
import config
|
||||
config = context.get_config_manager().data
|
||||
|
||||
if not hasattr(config, 'ignore_rules'):
|
||||
return False
|
||||
|
||||
if 'prefix' in config.ignore_rules:
|
||||
for rule in config.ignore_rules['prefix']:
|
||||
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 'regexp' in config['ignore_rules']:
|
||||
for rule in config['ignore_rules']['regexp']:
|
||||
if re.search(rule, msg):
|
||||
return True
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import mirai.models.bus
|
||||
from mirai import At, GroupMessage, MessageEvent, Mirai, StrangerMessage, WebSocketAdapter, HTTPAdapter, \
|
||||
FriendMessage, Image
|
||||
from func_timeout import func_set_timeout
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
from func_timeout import FunctionTimedOut
|
||||
import logging
|
||||
|
||||
import pkg.qqbot.filter
|
||||
import pkg.qqbot.process as processor
|
||||
import pkg.utils.context
|
||||
from mirai import At, GroupMessage, MessageEvent, StrangerMessage, \
|
||||
FriendMessage, Image, MessageChain, Plain
|
||||
import func_timeout
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
from ..openai import session as openai_session
|
||||
|
||||
from ..qqbot import filter as qqbot_filter
|
||||
from ..qqbot import process as processor
|
||||
from ..utils import context
|
||||
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):
|
||||
config = pkg.utils.context.get_config()
|
||||
if not hasattr(config, 'response_rules'):
|
||||
return False, ''
|
||||
def check_response_rule(group_id:int, text: str):
|
||||
config = context.get_config_manager().data
|
||||
|
||||
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:
|
||||
for rule in rules['prefix']:
|
||||
@@ -46,19 +47,39 @@ def check_response_rule(text: str):
|
||||
return False, ""
|
||||
|
||||
|
||||
def response_at():
|
||||
config = pkg.utils.context.get_config()
|
||||
if 'at' not in config.response_rules:
|
||||
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 config.response_rules['at']
|
||||
return use_response_rule['at']
|
||||
|
||||
|
||||
def random_responding():
|
||||
config = pkg.utils.context.get_config()
|
||||
if 'random_rate' in config.response_rules:
|
||||
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() < config.response_rules['random_rate']
|
||||
return random.random() < use_response_rule['random_rate']
|
||||
return False
|
||||
|
||||
|
||||
@@ -66,64 +87,56 @@ def random_responding():
|
||||
class QQBotManager:
|
||||
retry = 3
|
||||
|
||||
#线程池控制
|
||||
pool = None
|
||||
adapter: msadapter.MessageSourceAdapter = None
|
||||
|
||||
bot: Mirai = None
|
||||
bot_account_id: int = 0
|
||||
|
||||
reply_filter = None
|
||||
|
||||
enable_banlist = False
|
||||
|
||||
enable_private = True
|
||||
enable_group = True
|
||||
|
||||
ban_person = []
|
||||
ban_group = []
|
||||
|
||||
def __init__(self, mirai_http_api_config: dict, timeout: int = 60, retry: int = 3, pool_num: int = 10, first_time_init=True):
|
||||
self.timeout = timeout
|
||||
self.retry = retry
|
||||
def __init__(self, first_time_init=True):
|
||||
config = context.get_config_manager().data
|
||||
|
||||
self.pool_num = pool_num
|
||||
self.pool = ThreadPoolExecutor(max_workers=self.pool_num)
|
||||
logging.debug("Registered thread pool Size:{}".format(pool_num))
|
||||
|
||||
# 加载禁用列表
|
||||
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))
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
if os.path.exists("sensitive.json") \
|
||||
and config.sensitive_word_filter is not None \
|
||||
and config.sensitive_word_filter:
|
||||
with open("sensitive.json", "r", encoding="utf-8") as f:
|
||||
sensitive_json = json.load(f)
|
||||
self.reply_filter = pkg.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:
|
||||
self.reply_filter = pkg.qqbot.filter.ReplyFilter([])
|
||||
self.timeout = config['process_message_timeout']
|
||||
self.retry = config['retry_times']
|
||||
|
||||
# 由于YiriMirai的bot对象是单例的,且shutdown方法暂时无法使用
|
||||
# 故只在第一次初始化时创建bot对象,重载之后使用原bot对象
|
||||
# 因此,bot的配置不支持热重载
|
||||
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:
|
||||
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中编写相应的取消订阅代码
|
||||
@self.bot.on(FriendMessage)
|
||||
async def on_friend_message(event: FriendMessage):
|
||||
|
||||
def friend_message_handler(event: FriendMessage):
|
||||
def on_friend_message(event: FriendMessage):
|
||||
|
||||
def friend_message_handler():
|
||||
# 触发事件
|
||||
args = {
|
||||
"launcher_type": "person",
|
||||
@@ -138,12 +151,17 @@ class QQBotManager:
|
||||
|
||||
self.on_person_message(event)
|
||||
|
||||
self.go(friend_message_handler, event)
|
||||
context.get_thread_ctl().submit_user_task(
|
||||
friend_message_handler,
|
||||
)
|
||||
self.adapter.register_listener(
|
||||
FriendMessage,
|
||||
on_friend_message
|
||||
)
|
||||
|
||||
@self.bot.on(StrangerMessage)
|
||||
async def on_stranger_message(event: StrangerMessage):
|
||||
def on_stranger_message(event: StrangerMessage):
|
||||
|
||||
def stranger_message_handler(event: StrangerMessage):
|
||||
def stranger_message_handler():
|
||||
# 触发事件
|
||||
args = {
|
||||
"launcher_type": "person",
|
||||
@@ -158,10 +176,17 @@ class QQBotManager:
|
||||
|
||||
self.on_person_message(event)
|
||||
|
||||
self.go(stranger_message_handler, 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
|
||||
)
|
||||
|
||||
@self.bot.on(GroupMessage)
|
||||
async def on_group_message(event: GroupMessage):
|
||||
def on_group_message(event: GroupMessage):
|
||||
|
||||
def group_message_handler(event: GroupMessage):
|
||||
# 触发事件
|
||||
@@ -178,65 +203,96 @@ class QQBotManager:
|
||||
|
||||
self.on_group_message(event)
|
||||
|
||||
self.go(group_message_handler, event)
|
||||
context.get_thread_ctl().submit_user_task(
|
||||
group_message_handler,
|
||||
event
|
||||
)
|
||||
self.adapter.register_listener(
|
||||
GroupMessage,
|
||||
on_group_message
|
||||
)
|
||||
|
||||
def unsubscribe_all():
|
||||
"""取消所有订阅
|
||||
|
||||
用于在热重载流程中卸载所有事件处理器
|
||||
"""
|
||||
assert isinstance(self.bot, Mirai)
|
||||
bus = self.bot.bus
|
||||
assert isinstance(bus, mirai.models.bus.ModelEventBus)
|
||||
|
||||
bus.unsubscribe(FriendMessage, on_friend_message)
|
||||
bus.unsubscribe(StrangerMessage, on_stranger_message)
|
||||
bus.unsubscribe(GroupMessage, on_group_message)
|
||||
self.adapter.unregister_listener(
|
||||
FriendMessage,
|
||||
on_friend_message
|
||||
)
|
||||
if config['msg_source_adapter'] == 'yirimirai':
|
||||
self.adapter.unregister_listener(
|
||||
StrangerMessage,
|
||||
on_stranger_message
|
||||
)
|
||||
self.adapter.unregister_listener(
|
||||
GroupMessage,
|
||||
on_group_message
|
||||
)
|
||||
|
||||
self.unsubscribe_all = unsubscribe_all
|
||||
|
||||
def go(self, func, *args, **kwargs):
|
||||
self.pool.submit(func, *args, **kwargs)
|
||||
# 加载禁用列表
|
||||
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))
|
||||
|
||||
def first_time_init(self, mirai_http_api_config: dict):
|
||||
"""热重载后不再运行此函数"""
|
||||
if hasattr(banlist, "enable_private"):
|
||||
self.enable_private = banlist.enable_private
|
||||
if hasattr(banlist, "enable_group"):
|
||||
self.enable_group = banlist.enable_group
|
||||
|
||||
if 'adapter' not in mirai_http_api_config or mirai_http_api_config['adapter'] == "WebSocketAdapter":
|
||||
bot = Mirai(
|
||||
qq=mirai_http_api_config['qq'],
|
||||
adapter=WebSocketAdapter(
|
||||
verify_key=mirai_http_api_config['verifyKey'],
|
||||
host=mirai_http_api_config['host'],
|
||||
port=mirai_http_api_config['port']
|
||||
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 ''
|
||||
)
|
||||
)
|
||||
elif mirai_http_api_config['adapter'] == "HTTPAdapter":
|
||||
bot = Mirai(
|
||||
qq=mirai_http_api_config['qq'],
|
||||
adapter=HTTPAdapter(
|
||||
verify_key=mirai_http_api_config['verifyKey'],
|
||||
host=mirai_http_api_config['host'],
|
||||
port=mirai_http_api_config['port']
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
raise Exception("未知的适配器类型")
|
||||
self.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):
|
||||
config = pkg.utils.context.get_config()
|
||||
asyncio.run(
|
||||
self.bot.send(event, msg, quote=True if hasattr(config,
|
||||
"quote_origin") and config.quote_origin and check_quote else False))
|
||||
# 当回复的正文中包含换行时,quote可能会自带at,此时就不再单独添加at,只添加换行
|
||||
if "\n" not in str(msg[1]) or config['msg_source_adapter'] == 'nakuru':
|
||||
msg.insert(
|
||||
0,
|
||||
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):
|
||||
import config
|
||||
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
|
||||
else:
|
||||
if Image in event.message_chain:
|
||||
@@ -247,7 +303,7 @@ class QQBotManager:
|
||||
for i in range(self.retry):
|
||||
try:
|
||||
|
||||
@func_set_timeout(config.process_message_timeout)
|
||||
@func_timeout.func_set_timeout(config['process_message_timeout'])
|
||||
def time_ctrl_wrapper():
|
||||
reply = processor.process_message('person', event.sender.id, str(event.message_chain),
|
||||
event.message_chain,
|
||||
@@ -256,37 +312,38 @@ class QQBotManager:
|
||||
|
||||
reply = time_ctrl_wrapper()
|
||||
break
|
||||
except FunctionTimedOut:
|
||||
except func_timeout.FunctionTimedOut:
|
||||
logging.warning("person_{}: 超时,重试中({})".format(event.sender.id, i))
|
||||
pkg.openai.session.get_session('person_{}'.format(event.sender.id)).release_response_lock()
|
||||
if "person_{}".format(event.sender.id) in pkg.qqbot.process.processing:
|
||||
pkg.qqbot.process.processing.remove('person_{}'.format(event.sender.id))
|
||||
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
|
||||
continue
|
||||
|
||||
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)))
|
||||
reply = ["[bot]err:请求超时"]
|
||||
reply = [tips_custom.reply_message]
|
||||
|
||||
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):
|
||||
import config
|
||||
reply = ''
|
||||
|
||||
config = context.get_config_manager().data
|
||||
|
||||
def process(text=None) -> str:
|
||||
replys = ""
|
||||
if At(self.bot.qq) in event.message_chain:
|
||||
event.message_chain.remove(At(self.bot.qq))
|
||||
if At(self.bot_account_id) in event.message_chain:
|
||||
event.message_chain.remove(At(self.bot_account_id))
|
||||
|
||||
# 超时则重试,重试超过次数则放弃
|
||||
failed = 0
|
||||
for i in range(self.retry):
|
||||
try:
|
||||
@func_set_timeout(config.process_message_timeout)
|
||||
@func_timeout.func_set_timeout(config['process_message_timeout'])
|
||||
def time_ctrl_wrapper():
|
||||
replys = processor.process_message('group', event.group.id,
|
||||
str(event.message_chain).strip() if text is None else text,
|
||||
@@ -296,34 +353,36 @@ class QQBotManager:
|
||||
|
||||
replys = time_ctrl_wrapper()
|
||||
break
|
||||
except FunctionTimedOut:
|
||||
except func_timeout.FunctionTimedOut:
|
||||
logging.warning("group_{}: 超时,重试中({})".format(event.group.id, i))
|
||||
pkg.openai.session.get_session('group_{}'.format(event.group.id)).release_response_lock()
|
||||
if "group_{}".format(event.group.id) in pkg.qqbot.process.processing:
|
||||
pkg.qqbot.process.processing.remove('group_{}'.format(event.group.id))
|
||||
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
|
||||
continue
|
||||
|
||||
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)))
|
||||
replys = ["[bot]err:请求超时"]
|
||||
replys = [tips_custom.replys_message]
|
||||
|
||||
return replys
|
||||
|
||||
if Image in event.message_chain:
|
||||
|
||||
if not self.enable_group:
|
||||
logging.debug("已在banlist.py中禁用所有群聊")
|
||||
elif Image in event.message_chain:
|
||||
pass
|
||||
else:
|
||||
if At(self.bot.qq) in event.message_chain and response_at():
|
||||
if At(self.bot_account_id) in event.message_chain and response_at(event.group.id):
|
||||
# 直接调用
|
||||
reply = process()
|
||||
else:
|
||||
check, result = check_response_rule(str(event.message_chain).strip())
|
||||
check, result = check_response_rule(event.group.id, str(event.message_chain).strip())
|
||||
|
||||
if check:
|
||||
reply = process(result.strip())
|
||||
# 检查是否随机响应
|
||||
elif random_responding():
|
||||
elif random_responding(event.group.id):
|
||||
logging.info("随机响应group_{}消息".format(event.group.id))
|
||||
reply = process()
|
||||
|
||||
@@ -332,26 +391,37 @@ class QQBotManager:
|
||||
|
||||
# 通知系统管理员
|
||||
def notify_admin(self, message: str):
|
||||
config = pkg.utils.context.get_config()
|
||||
if hasattr(config, "admin_qq") and config.admin_qq != 0 and config.admin_qq != []:
|
||||
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:
|
||||
send_task = self.bot.send_friend_message(config.admin_qq, "[bot]{}".format(message))
|
||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
||||
if type(config['admin_qq']) == int:
|
||||
self.adapter.send_message(
|
||||
"person",
|
||||
config['admin_qq'],
|
||||
MessageChain([Plain("[bot]{}".format(message))])
|
||||
)
|
||||
else:
|
||||
for adm in config.admin_qq:
|
||||
send_task = self.bot.send_friend_message(adm, "[bot]{}".format(message))
|
||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
||||
|
||||
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 = pkg.utils.context.get_config()
|
||||
if hasattr(config, "admin_qq") and config.admin_qq != 0 and config.admin_qq != []:
|
||||
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:
|
||||
send_task = self.bot.send_friend_message(config.admin_qq, message)
|
||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
||||
if type(config['admin_qq']) == int:
|
||||
self.adapter.send_message(
|
||||
"person",
|
||||
config['admin_qq'],
|
||||
message
|
||||
)
|
||||
else:
|
||||
for adm in config.admin_qq:
|
||||
send_task = self.bot.send_friend_message(adm, message)
|
||||
threading.Thread(target=asyncio.run, args=(send_task,)).start()
|
||||
for adm in config['admin_qq']:
|
||||
self.adapter.send_message(
|
||||
"person",
|
||||
adm,
|
||||
message
|
||||
)
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
# 普通消息处理模块
|
||||
import logging
|
||||
import openai
|
||||
import pkg.utils.context
|
||||
import pkg.openai.session
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
import pkg.qqbot.blob as blob
|
||||
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不为空时,会通知管理员,返回通知用户的消息"""
|
||||
import config
|
||||
pkg.utils.context.get_qqbot_manager().notify_admin(notify_admin)
|
||||
if hasattr(config, 'hide_exce_info_to_user') and config.hide_exce_info_to_user:
|
||||
if hasattr(config, 'alter_tip_message'):
|
||||
return [config.alter_tip_message] if config.alter_tip_message else []
|
||||
else:
|
||||
return ["[bot]出错了,请重试或联系管理员"]
|
||||
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, launcher_type: str,
|
||||
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 = pkg.openai.session.get_session(session_name)
|
||||
session = openai_session.get_session(session_name)
|
||||
|
||||
unexpected_exception_times = 0
|
||||
|
||||
@@ -40,9 +39,9 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
|
||||
reply = handle_exception(notify_admin=f"{session_name},多次尝试失败。", set_reply=f"[bot]多次尝试失败,请重试或联系管理员")
|
||||
break
|
||||
try:
|
||||
prefix = "[GPT]" if hasattr(config, "show_prefix") and config.show_prefix else ""
|
||||
prefix = "[GPT]" if config['show_prefix'] else ""
|
||||
|
||||
text = session.append(text_message)
|
||||
text, finish_reason, funcs = session.query(text_message)
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
@@ -51,10 +50,12 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
|
||||
"sender_id": sender_id,
|
||||
"session": session,
|
||||
"prefix": prefix,
|
||||
"response_text": text
|
||||
"response_text": text,
|
||||
"finish_reason": finish_reason,
|
||||
"funcs_called": funcs,
|
||||
}
|
||||
|
||||
event = pkg.plugin.host.emit(plugin_models.NormalMessageResponded, **args)
|
||||
event = plugin_host.emit(plugin_models.NormalMessageResponded, **args)
|
||||
|
||||
if event.get_return_value("prefix") is not None:
|
||||
prefix = event.get_return_value("prefix")
|
||||
@@ -64,42 +65,43 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
|
||||
|
||||
if not event.is_prevented_default():
|
||||
reply = [prefix + text]
|
||||
except openai.error.APIConnectionError as e:
|
||||
|
||||
except openai.APIConnectionError as e:
|
||||
err_msg = str(e)
|
||||
if err_msg.__contains__('Error communicating with OpenAI'):
|
||||
reply = handle_exception("{}会话调用API失败:{}\n请尝试关闭网络代理来解决此问题。".format(session_name, e),
|
||||
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.error.RateLimitError as e:
|
||||
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 = pkg.utils.context.get_openai_manager().key_mgr.get_key_name(
|
||||
pkg.utils.context.get_openai_manager().key_mgr.using_key
|
||||
current_key_name = context.get_openai_manager().key_mgr.get_key_name(
|
||||
context.get_openai_manager().key_mgr.using_key
|
||||
)
|
||||
pkg.utils.context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||
context.get_openai_manager().key_mgr.set_current_exceeded()
|
||||
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'key_name': current_key_name,
|
||||
'usage': pkg.utils.context.get_openai_manager().audit_mgr
|
||||
.get_usage(pkg.utils.context.get_openai_manager().key_mgr.get_using_key_md5()),
|
||||
'exceeded_keys': pkg.utils.context.get_openai_manager().key_mgr.exceeded,
|
||||
'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 = pkg.utils.context.get_openai_manager().key_mgr.auto_switch()
|
||||
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 = pkg.utils.context.get_openai_manager().key_mgr.get_using_key()
|
||||
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
|
||||
@@ -115,10 +117,14 @@ def process_normal_message(text_message: str, mgr, config, launcher_type: str,
|
||||
else:
|
||||
reply = handle_exception("{}会话调用API失败:{}".format(session_name, e),
|
||||
"[bot]err:RateLimitError,请重试或联系作者,或等待修复")
|
||||
except openai.error.InvalidRequestError as e:
|
||||
reply = handle_exception("{}API调用参数错误:{}\n".format(
|
||||
session_name, e), "[bot]err:API调用参数错误,请联系管理员,或等待修复")
|
||||
except openai.error.ServiceUnavailableError as e:
|
||||
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)
|
||||
|
||||
@@ -1,50 +1,46 @@
|
||||
# 此模块提供了消息处理的具体逻辑的接口
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import mirai
|
||||
import logging
|
||||
|
||||
from mirai import MessageChain, Plain
|
||||
|
||||
# 这里不使用动态引入config
|
||||
# 因为在这里动态引入会卡死程序
|
||||
# 而此模块静态引用config与动态引入的表现一致
|
||||
# 已弃用,由于超时时间现已动态使用
|
||||
# import config as config_init_import
|
||||
|
||||
import pkg.openai.session
|
||||
import pkg.openai.manager
|
||||
import pkg.utils.reloader
|
||||
import pkg.utils.updater
|
||||
import pkg.utils.context
|
||||
import pkg.qqbot.message
|
||||
import pkg.qqbot.command
|
||||
import pkg.qqbot.ratelimit as ratelimit
|
||||
from ..qqbot import ratelimit
|
||||
from ..qqbot import command, message
|
||||
from ..openai import session as openai_session
|
||||
from ..utils import context
|
||||
|
||||
import pkg.plugin.host as plugin_host
|
||||
import pkg.plugin.models as plugin_models
|
||||
import pkg.qqbot.ignore as ignore
|
||||
import pkg.qqbot.banlist as banlist
|
||||
import pkg.qqbot.blob as blob
|
||||
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 = []
|
||||
|
||||
|
||||
def is_admin(qq: int) -> bool:
|
||||
"""兼容list和int类型的管理员判断"""
|
||||
import config
|
||||
if type(config.admin_qq) == list:
|
||||
return qq in config.admin_qq
|
||||
config = context.get_config_manager().data
|
||||
if type(config['admin_qq']) == list:
|
||||
return qq in config['admin_qq']
|
||||
else:
|
||||
return qq == config.admin_qq
|
||||
return qq == config['admin_qq']
|
||||
|
||||
|
||||
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: MessageChain,
|
||||
sender_id: int) -> MessageChain:
|
||||
def process_message(launcher_type: str, launcher_id: int, text_message: str, message_chain: mirai.MessageChain,
|
||||
sender_id: int) -> mirai.MessageChain:
|
||||
global processing
|
||||
|
||||
mgr = pkg.utils.context.get_qqbot_manager()
|
||||
mgr = context.get_qqbot_manager()
|
||||
|
||||
reply = []
|
||||
session_name = "{}_{}".format(launcher_type, launcher_id)
|
||||
@@ -58,35 +54,38 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
||||
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':
|
||||
result = mgr.bot.member_info(target=launcher_id, member_id=mgr.bot.qq).get()
|
||||
result = asyncio.run(result)
|
||||
if result.mute_time_remaining > 0:
|
||||
logging.info("机器人被禁言,跳过消息处理(group_{},剩余{}s)".format(launcher_id,
|
||||
result.mute_time_remaining))
|
||||
is_muted = mgr.adapter.is_muted(launcher_id)
|
||||
if is_muted:
|
||||
logging.info("机器人被禁言,跳过消息处理(group_{})".format(launcher_id))
|
||||
return reply
|
||||
|
||||
import config
|
||||
if hasattr(config, 'income_msg_check') and config.income_msg_check:
|
||||
if config['income_msg_check']:
|
||||
if mgr.reply_filter.is_illegal(text_message):
|
||||
return MessageChain(Plain("[bot] 你的提问中有不合适的内容, 请更换措辞~"))
|
||||
return mirai.MessageChain(mirai.Plain("[bot] 消息中存在不合适的内容, 请更换措辞"))
|
||||
|
||||
pkg.openai.session.get_session(session_name).acquire_response_lock()
|
||||
openai_session.get_session(session_name).acquire_response_lock()
|
||||
|
||||
text_message = text_message.strip()
|
||||
|
||||
|
||||
# 为强制消息延迟计时
|
||||
start_time = time.time()
|
||||
|
||||
# 处理消息
|
||||
try:
|
||||
if session_name in processing:
|
||||
pkg.openai.session.get_session(session_name).release_response_lock()
|
||||
return MessageChain([Plain("[bot]err:正在处理中,请稍后再试")])
|
||||
|
||||
config = pkg.utils.context.get_config()
|
||||
|
||||
processing.append(session_name)
|
||||
try:
|
||||
if text_message.startswith('!') or text_message.startswith("!"): # 指令
|
||||
msg_type = ''
|
||||
if text_message.startswith('!') or text_message.startswith("!"): # 命令
|
||||
msg_type = 'command'
|
||||
# 触发插件事件
|
||||
args = {
|
||||
'launcher_type': launcher_type,
|
||||
@@ -109,16 +108,18 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
||||
reply = event.get_return_value("reply")
|
||||
|
||||
if not event.is_prevented_default():
|
||||
reply = pkg.qqbot.command.process_command(session_name, text_message,
|
||||
reply = command.process_command(session_name, text_message,
|
||||
mgr, config, launcher_type, launcher_id, sender_id, is_admin(sender_id))
|
||||
|
||||
else: # 消息
|
||||
msg_type = 'message'
|
||||
# 限速丢弃检查
|
||||
# print(ratelimit.__crt_minute_usage__[session_name])
|
||||
if hasattr(config, "rate_limitation") and config.rate_limit_strategy == "drop":
|
||||
if config['rate_limit_strategy'] == "drop":
|
||||
if ratelimit.is_reach_limit(session_name):
|
||||
logging.info("根据限速策略丢弃[{}]消息: {}".format(session_name, text_message))
|
||||
return MessageChain(["[bot]"+config.rate_limit_drop_tip]) if hasattr(config, "rate_limit_drop_tip") and config.rate_limit_drop_tip != "" else []
|
||||
|
||||
return mirai.MessageChain(["[bot]"+tips_custom.rate_limit_drop_tip]) if tips_custom.rate_limit_drop_tip != "" else []
|
||||
|
||||
before = time.time()
|
||||
# 触发插件事件
|
||||
@@ -140,15 +141,14 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
||||
reply = event.get_return_value("reply")
|
||||
|
||||
if not event.is_prevented_default():
|
||||
reply = pkg.qqbot.message.process_normal_message(text_message,
|
||||
reply = message.process_normal_message(text_message,
|
||||
mgr, config, launcher_type, launcher_id, sender_id)
|
||||
|
||||
# 限速等待时间
|
||||
if hasattr(config, "rate_limitation") and config.rate_limit_strategy == "wait":
|
||||
if config['rate_limit_strategy'] == "wait":
|
||||
time.sleep(ratelimit.get_rest_wait_time(session_name, time.time() - before))
|
||||
|
||||
if hasattr(config, "rate_limitation"):
|
||||
ratelimit.add_usage(session_name)
|
||||
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:
|
||||
@@ -157,7 +157,9 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
||||
"回复[{}]文字消息:{}".format(session_name,
|
||||
reply[0][:min(100, len(reply[0]))] + (
|
||||
"..." 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:
|
||||
logging.info("回复[{}]消息".format(session_name))
|
||||
@@ -165,6 +167,25 @@ def process_message(launcher_type: str, launcher_id: int, text_message: str, mes
|
||||
finally:
|
||||
processing.remove(session_name)
|
||||
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)
|
||||
|
||||
@@ -3,6 +3,9 @@ import time
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from ..utils import context
|
||||
|
||||
|
||||
__crt_minute_usage__ = {}
|
||||
"""当前分钟每个会话的对话次数"""
|
||||
|
||||
@@ -10,6 +13,16 @@ __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__
|
||||
@@ -56,12 +69,7 @@ def get_rest_wait_time(session_name: str, spent: float) -> float:
|
||||
"""获取会话此回合的剩余等待时间"""
|
||||
global __crt_minute_usage__
|
||||
|
||||
import config
|
||||
|
||||
if not hasattr(config, 'rate_limitation'):
|
||||
return 0
|
||||
|
||||
min_seconds_per_round = 60.0 / config.rate_limitation
|
||||
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)
|
||||
@@ -73,13 +81,8 @@ def is_reach_limit(session_name: str) -> bool:
|
||||
"""判断会话是否超过限制"""
|
||||
global __crt_minute_usage__
|
||||
|
||||
import config
|
||||
|
||||
if not hasattr(config, 'rate_limitation'):
|
||||
return False
|
||||
|
||||
if session_name in __crt_minute_usage__:
|
||||
return __crt_minute_usage__[session_name] >= config.rate_limitation
|
||||
return __crt_minute_usage__[session_name] >= get_limitation(session_name)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
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
|
||||
@@ -1,47 +1,68 @@
|
||||
import base64
|
||||
import os
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
import pkg.utils.network as network
|
||||
|
||||
|
||||
def read_latest() -> str:
|
||||
def read_latest() -> list:
|
||||
import pkg.utils.network as network
|
||||
resp = requests.get(
|
||||
url="https://api.github.com/repos/RockChinQ/QChatGPT/contents/res/announcement",
|
||||
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 content
|
||||
return json.loads(content)
|
||||
|
||||
|
||||
def read_saved() -> str:
|
||||
def read_saved() -> list:
|
||||
# 已保存的在res/announcement_saved
|
||||
# 检查是否存在
|
||||
if not os.path.exists("res/announcement_saved"):
|
||||
with open("res/announcement_saved", "w") as f:
|
||||
f.write("")
|
||||
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", "r") as f:
|
||||
with open("res/announcement_saved.json", "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
return content
|
||||
return json.loads(content)
|
||||
|
||||
|
||||
def write_saved(content: str):
|
||||
def write_saved(content: list):
|
||||
# 已保存的在res/announcement_saved
|
||||
with open("res/announcement_saved", "w") as f:
|
||||
f.write(content)
|
||||
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() -> str:
|
||||
def fetch_new() -> list:
|
||||
latest = read_latest()
|
||||
saved = read_saved()
|
||||
if latest.replace(saved, "").strip() == "":
|
||||
return ""
|
||||
else:
|
||||
write_saved(latest)
|
||||
return latest.replace(saved, "").strip()
|
||||
|
||||
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)
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user