mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-07 05:20:23 +00:00
Compare commits
3198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb12e3f45 | ||
|
|
1dc2f7e5a2 | ||
|
|
2531b08538 | ||
|
|
9fcfb5493c | ||
|
|
4576354c51 | ||
|
|
1dcf2ef0c6 | ||
|
|
3642c65e8c | ||
|
|
40e105994a | ||
|
|
f2ee973882 | ||
|
|
3aa30792bf | ||
|
|
6e336fa78e | ||
|
|
900027a6b7 | ||
|
|
38bdca2409 | ||
|
|
7196e476bf | ||
|
|
e0fd3785d9 | ||
|
|
b53ebb6c2a | ||
|
|
1ea80f4447 | ||
|
|
627d3c0a7a | ||
|
|
182cccfc71 | ||
|
|
6a3713e86c | ||
|
|
788da4e4f1 | ||
|
|
fd26d34e19 | ||
|
|
e9fcdc7d2e | ||
|
|
0fe4911d01 | ||
|
|
d4fb09fa80 | ||
|
|
e6d5a37236 | ||
|
|
79fd10ac10 | ||
|
|
a2e6095e44 | ||
|
|
64530471a0 | ||
|
|
e31e831309 | ||
|
|
cf6871df9b | ||
|
|
482e7f1c75 | ||
|
|
aab501e31e | ||
|
|
ceec9e5e1b | ||
|
|
aadebb3cc5 | ||
|
|
657ddd3341 | ||
|
|
62127b6d48 | ||
|
|
f5f405796f | ||
|
|
39873947a3 | ||
|
|
a1079dd948 | ||
|
|
4eeabcc9e0 | ||
|
|
c3568d07e8 | ||
|
|
1adb4a4ba8 | ||
|
|
6d0020533c | ||
|
|
4e6af0a655 | ||
|
|
00f726b515 | ||
|
|
035aa32305 | ||
|
|
62ea4b98e1 | ||
|
|
4be821137d | ||
|
|
7fba9960bf | ||
|
|
876bfbd3cb | ||
|
|
edde2c210b | ||
|
|
f956d96d94 | ||
|
|
c2296fd900 | ||
|
|
0feed5b640 | ||
|
|
93904dcb1b | ||
|
|
86cbdf793a | ||
|
|
56b1b9b598 | ||
|
|
f7ec3ae131 | ||
|
|
01d11d6213 | ||
|
|
74a316e758 | ||
|
|
d20c5185a4 | ||
|
|
da965e7b39 | ||
|
|
3fbed815a5 | ||
|
|
152be29739 | ||
|
|
e521740a44 | ||
|
|
ee047e8bc1 | ||
|
|
5eaa9ca347 | ||
|
|
40f79ee816 | ||
|
|
f0dcef7981 | ||
|
|
3c09ff13d0 | ||
|
|
7158f25f37 | ||
|
|
54f805b6e4 | ||
|
|
70c4651fbf | ||
|
|
962d3c064f | ||
|
|
c6a459a111 | ||
|
|
b0242ccb62 | ||
|
|
53f5277b08 | ||
|
|
90b54435b5 | ||
|
|
12a1681b42 | ||
|
|
4277cb3f3c | ||
|
|
8353d53589 | ||
|
|
9e94d98cfb | ||
|
|
b6ec1aaa9b | ||
|
|
e7e8763f1c | ||
|
|
515c1af676 | ||
|
|
6fa7a973ba | ||
|
|
3e63f509bc | ||
|
|
b3b02e781a | ||
|
|
6d83921e20 | ||
|
|
30bd372d45 | ||
|
|
63254b7e55 | ||
|
|
f4c08d93f4 | ||
|
|
6ca1ac21e4 | ||
|
|
381ee1c30e | ||
|
|
902fe907bd | ||
|
|
bbb4ad7d95 | ||
|
|
24bc9f35b2 | ||
|
|
52c68a3bfb | ||
|
|
d982bcdad5 | ||
|
|
b8165242f0 | ||
|
|
7ce95bca04 | ||
|
|
cd212abd5f | ||
|
|
e5b063accb | ||
|
|
eeef5409dc | ||
|
|
2bf8d8f791 | ||
|
|
56e62392a6 | ||
|
|
2ecf04c78c | ||
|
|
a19358da5b | ||
|
|
a5d4998933 | ||
|
|
8edbe54456 | ||
|
|
e898915d01 | ||
|
|
b2075130d9 | ||
|
|
02e39b5714 | ||
|
|
de64b03054 | ||
|
|
fa70eec3d8 | ||
|
|
583ec10c7c | ||
|
|
38a098c77d | ||
|
|
d17674d06e | ||
|
|
0b839258aa | ||
|
|
50e207cf6f | ||
|
|
5d2d8c7123 | ||
|
|
23702f412c | ||
|
|
31e94792c4 | ||
|
|
249afdce81 | ||
|
|
ee8f381341 | ||
|
|
83f3df76cd | ||
|
|
16195ca52b | ||
|
|
d5f492775e | ||
|
|
1f273a8799 | ||
|
|
f44f6fd1e9 | ||
|
|
21ca13789e | ||
|
|
648faedca6 | ||
|
|
3a6748ae37 | ||
|
|
4d4b1ad26c | ||
|
|
e42fbea918 | ||
|
|
48b648b0fb | ||
|
|
68e86b07c7 | ||
|
|
12cb500818 | ||
|
|
9ffaab178a | ||
|
|
d4fbbd6711 | ||
|
|
ded53cd348 | ||
|
|
be9e80c87b | ||
|
|
e9fe6f28cc | ||
|
|
0b8bf739e9 | ||
|
|
0222664db8 | ||
|
|
a88792e452 | ||
|
|
ad45400742 | ||
|
|
53e5ba03be | ||
|
|
b587d6b91d | ||
|
|
5e750d4ee9 | ||
|
|
50fb32f81c | ||
|
|
6c46cdd947 | ||
|
|
372452fbee | ||
|
|
417ef5d335 | ||
|
|
9c534f8afd | ||
|
|
ecd426bb80 | ||
|
|
f74ef273de | ||
|
|
f913e0b027 | ||
|
|
f7268c30ca | ||
|
|
0f5ef03d63 | ||
|
|
745276d0f0 | ||
|
|
2e108a4bd6 | ||
|
|
666da80ef5 | ||
|
|
cc73104d62 | ||
|
|
3c10b82bab | ||
|
|
9a65dae6a2 | ||
|
|
f26cd8cdc9 | ||
|
|
eeec905df0 | ||
|
|
0c6aac7f66 | ||
|
|
86d22db141 | ||
|
|
48a5d0eef3 | ||
|
|
bda174bed4 | ||
|
|
caf98b8655 | ||
|
|
c9833c5988 | ||
|
|
55ef7e529e | ||
|
|
9b04ddcefd | ||
|
|
6dc4f38581 | ||
|
|
93ce8bfb85 | ||
|
|
e7d138448a | ||
|
|
02c4a468cb | ||
|
|
d392e653e1 | ||
|
|
e8faa09f1d | ||
|
|
e80ed3b33e | ||
|
|
41a346e1cf | ||
|
|
5e19fc112a | ||
|
|
2f7aff2b56 | ||
|
|
ccb0e1fb4f | ||
|
|
d4163c913a | ||
|
|
8087ba0e4a | ||
|
|
6700523b61 | ||
|
|
49f1c3f9ba | ||
|
|
575ab4f1d1 | ||
|
|
3658547731 | ||
|
|
eb6590e9e2 | ||
|
|
83f28795f2 | ||
|
|
e98bfaac11 | ||
|
|
4f4bd3c6e0 | ||
|
|
bd1faccaa8 | ||
|
|
25751b8149 | ||
|
|
e34b60315c | ||
|
|
046afc0c23 | ||
|
|
2f61ba7f25 | ||
|
|
8981f12b1a | ||
|
|
34e96b1089 | ||
|
|
41db435ef5 | ||
|
|
b525fa81bb | ||
|
|
6382b29da8 | ||
|
|
8bc0403139 | ||
|
|
9f261e78c3 | ||
|
|
15d9390ee4 | ||
|
|
572b8809a5 | ||
|
|
623799c049 | ||
|
|
4271acc6ab | ||
|
|
609e83a824 | ||
|
|
e98910c9ff | ||
|
|
c432799580 | ||
|
|
fa87f7c8c3 | ||
|
|
4a44062814 | ||
|
|
fe0bda11d3 | ||
|
|
1ec1040e43 | ||
|
|
e44595334a | ||
|
|
f40de023b0 | ||
|
|
9799d02ad2 | ||
|
|
bec88fee04 | ||
|
|
1a94e20691 | ||
|
|
3690307d0b | ||
|
|
2d5b4bc90a | ||
|
|
cc93ed3567 | ||
|
|
dce4988767 | ||
|
|
5c81b60b58 | ||
|
|
a668bfbc13 | ||
|
|
bc0fc96b9b | ||
|
|
ae14692d5b | ||
|
|
d445dc6644 | ||
|
|
db3d435402 | ||
|
|
7ee48f1443 | ||
|
|
a54f30acc1 | ||
|
|
75e7bc7275 | ||
|
|
f1b2c8b1cf | ||
|
|
50079e7a96 | ||
|
|
6d37868ae8 | ||
|
|
543961e980 | ||
|
|
1e2c76bb47 | ||
|
|
ddc0ed066d | ||
|
|
6708903c65 | ||
|
|
5ee0afb604 | ||
|
|
9b20e9db29 | ||
|
|
74b4d9bf49 | ||
|
|
89f7892681 | ||
|
|
f83bf197d2 | ||
|
|
5bcc130dd7 | ||
|
|
4be6d8ec01 | ||
|
|
aad5ed55d2 | ||
|
|
86da417c17 | ||
|
|
ae57ab78f3 | ||
|
|
4487db4e0a | ||
|
|
a0a50755d3 | ||
|
|
621e41cc96 | ||
|
|
96b1f71437 | ||
|
|
5e0b3b2f35 | ||
|
|
6829fad5bd | ||
|
|
7af0d9e87b | ||
|
|
c089ebea99 | ||
|
|
d2a2c1c39c | ||
|
|
ce9b09e8d1 | ||
|
|
2f6dfe51f5 | ||
|
|
bd227cd0b8 | ||
|
|
96003724ab | ||
|
|
6a08b15095 | ||
|
|
dab0f9ab45 | ||
|
|
e733a6b69a | ||
|
|
9aca98bf13 | ||
|
|
b7c95e53dc | ||
|
|
f762c450ca | ||
|
|
d58bbe53da | ||
|
|
f32edd8af7 | ||
|
|
c747a86e5b | ||
|
|
abfda0dd58 | ||
|
|
f66d7b11a8 | ||
|
|
f425c9478e | ||
|
|
756dea71fc | ||
|
|
71a6c4ccc5 | ||
|
|
ae2f4777ec | ||
|
|
dcd9b8168a | ||
|
|
4bb03ae5ba | ||
|
|
8bd6f8397b | ||
|
|
096e52d93e | ||
|
|
037065291d | ||
|
|
4cf52e1b13 | ||
|
|
21b228552d | ||
|
|
76b404cdd8 | ||
|
|
937c594ff7 | ||
|
|
b463140de7 | ||
|
|
f518fb9214 | ||
|
|
1092831718 | ||
|
|
6b377416da | ||
|
|
8f5baa47ec | ||
|
|
5494ff0553 | ||
|
|
7a4805b464 | ||
|
|
8435375810 | ||
|
|
c893ec6030 | ||
|
|
e8bf6fa0a6 | ||
|
|
f228129c19 | ||
|
|
cbf98ffb89 | ||
|
|
f6067b002f | ||
|
|
636d1103e3 | ||
|
|
bede517f7e | ||
|
|
16e4891b7d | ||
|
|
3bcd79fbb7 | ||
|
|
aacf6c2917 | ||
|
|
92d720cd57 | ||
|
|
2ea025047f | ||
|
|
f7f7e09cab | ||
|
|
75866b435e | ||
|
|
f07941685b | ||
|
|
60a0539216 | ||
|
|
3dd4b6549f | ||
|
|
0802c35dc1 | ||
|
|
7d9d7226ec | ||
|
|
b5ef6ce6b0 | ||
|
|
49ec6181b0 | ||
|
|
783a534768 | ||
|
|
704ac11cbb | ||
|
|
aa9663d85e | ||
|
|
05291f34fb | ||
|
|
2260fe32a1 | ||
|
|
2c398a6832 | ||
|
|
3e1f566699 | ||
|
|
4f89f184b8 | ||
|
|
787685c937 | ||
|
|
ed9cd2fe38 | ||
|
|
740d80e851 | ||
|
|
4520a20bd4 | ||
|
|
98c65c4923 | ||
|
|
e287906a9d | ||
|
|
8bae789020 | ||
|
|
ce57b7b725 | ||
|
|
1d9872195d | ||
|
|
98d1f8e29f | ||
|
|
221b3fb730 | ||
|
|
90a834495a | ||
|
|
8bfd102232 | ||
|
|
65e784f169 | ||
|
|
0fc81c672f | ||
|
|
62ae0f4321 | ||
|
|
a01a0a1a18 | ||
|
|
4c30cc69ad | ||
|
|
1d43b75df4 | ||
|
|
d02afdfc3e | ||
|
|
5d6dee9fd0 | ||
|
|
60c67ef41c | ||
|
|
917d7c1f19 | ||
|
|
ad19f2c99e | ||
|
|
8a61f5a03f | ||
|
|
8c164910f6 | ||
|
|
a560d3d266 | ||
|
|
532f739272 | ||
|
|
a120727f2d | ||
|
|
a9bcb830a8 | ||
|
|
56e5f0033f | ||
|
|
101106996a | ||
|
|
41a81534dc | ||
|
|
1425e8f229 | ||
|
|
75bb1d2193 | ||
|
|
2a23820f9b | ||
|
|
2ee0fed047 | ||
|
|
40be6b9c43 | ||
|
|
a06b3f0246 | ||
|
|
4787fa53b4 | ||
|
|
a06158bf01 | ||
|
|
314e7485b8 | ||
|
|
aed5d2d9f0 | ||
|
|
f44e48a28b | ||
|
|
38be90450c | ||
|
|
2dd57d7676 | ||
|
|
6b3b163fa8 | ||
|
|
9792ebafdc | ||
|
|
d10e7c37cb | ||
|
|
d38f1853a4 | ||
|
|
bdec16266e | ||
|
|
49ca698ab9 | ||
|
|
3efd8163c9 | ||
|
|
cc2d11449c | ||
|
|
7e9c19ca5b | ||
|
|
3b01b6827f | ||
|
|
8d9ef851ba | ||
|
|
b070bc59bc | ||
|
|
8d663946e1 | ||
|
|
2a2328b029 | ||
|
|
efc9064abb | ||
|
|
dd70adf071 | ||
|
|
0f427375cb | ||
|
|
4001270b93 | ||
|
|
e7f5ed3bcc | ||
|
|
05cdc37d0a | ||
|
|
27920e0bee | ||
|
|
ae409b7249 | ||
|
|
8276258348 | ||
|
|
1bf96a97a5 | ||
|
|
d672680c4c | ||
|
|
b89f2805e7 | ||
|
|
78b4aa9295 | ||
|
|
0a06637e78 | ||
|
|
13afa2c7ab | ||
|
|
51d34d17cc | ||
|
|
18a99341d5 | ||
|
|
f01c8f0110 | ||
|
|
d8070eee2a | ||
|
|
8519b7f4df | ||
|
|
591ab1b1df | ||
|
|
393815b11e | ||
|
|
341a397bc4 | ||
|
|
e46d274a75 | ||
|
|
ad6f21980c | ||
|
|
017b8b7f15 | ||
|
|
9b448b17e6 | ||
|
|
f9996a9987 | ||
|
|
000ef55273 | ||
|
|
e1ac0f02b4 | ||
|
|
b9297e3f1d | ||
|
|
34d0669ca8 | ||
|
|
25e42720cf | ||
|
|
f7c1951191 | ||
|
|
479b971b0c | ||
|
|
347ba5f354 | ||
|
|
81dbb9d980 | ||
|
|
c4e1a3ab04 | ||
|
|
90ec774a21 | ||
|
|
db7a27e624 | ||
|
|
f7d965eda2 | ||
|
|
74ca2e2e16 | ||
|
|
8ab550f2f5 | ||
|
|
018aca4db2 | ||
|
|
d4327166c1 | ||
|
|
fa25d2e779 | ||
|
|
3ce1c3f0ec | ||
|
|
96dff5141e | ||
|
|
78d85d9965 | ||
|
|
37ec455b02 | ||
|
|
6ab82739a6 | ||
|
|
a36917e7c0 | ||
|
|
21f3428b36 | ||
|
|
f8a487db25 | ||
|
|
73a859be04 | ||
|
|
63bcee01a1 | ||
|
|
85b4966ba8 | ||
|
|
36c2c567b7 | ||
|
|
7b1ac224f6 | ||
|
|
34d9f04f15 | ||
|
|
be5da7cc6f | ||
|
|
8d32ccb5d4 | ||
|
|
6acceb884c | ||
|
|
4c834fd640 | ||
|
|
301278c7a9 | ||
|
|
42ee83c54f | ||
|
|
e631f69621 | ||
|
|
ce8760a39a | ||
|
|
ff952956de | ||
|
|
28f3ff4971 | ||
|
|
19e728c3cb | ||
|
|
269773ed6b | ||
|
|
e0d32417e1 | ||
|
|
9fa6083bed | ||
|
|
4d2fccdfb4 | ||
|
|
c1c4bdfe94 | ||
|
|
8a0e9e8b61 | ||
|
|
1190e14171 | ||
|
|
00292b177a | ||
|
|
88de57f984 | ||
|
|
61ddf38892 | ||
|
|
52b3540ec3 | ||
|
|
5f831958c3 | ||
|
|
c3d4698af3 | ||
|
|
bd6e83217d | ||
|
|
50ec49d9a2 | ||
|
|
dc3a089070 | ||
|
|
530e380178 | ||
|
|
10e4387add | ||
|
|
e925bc3aa8 | ||
|
|
427b3a7560 | ||
|
|
c8da950725 | ||
|
|
743c5b8196 | ||
|
|
5e62abea57 | ||
|
|
6bfc545582 | ||
|
|
411108a2d2 | ||
|
|
308a6fa9e4 | ||
|
|
2dc7b785d0 | ||
|
|
0e69e9e839 | ||
|
|
b83229b5da | ||
|
|
6f053f5f7d | ||
|
|
c3dc53eaaf | ||
|
|
ffdc34cfe2 | ||
|
|
4825a0e341 | ||
|
|
95a00d7f35 | ||
|
|
d885bab426 | ||
|
|
e2a6a0bc02 | ||
|
|
ff7d8609ce | ||
|
|
7507b90e03 | ||
|
|
2b226a4b27 | ||
|
|
8b0232c4fe | ||
|
|
0728ee9ad6 | ||
|
|
8c6f04d0bc | ||
|
|
c67fad789e | ||
|
|
4072339d70 | ||
|
|
3a244f5804 | ||
|
|
f12cf59137 | ||
|
|
c76f556a11 | ||
|
|
e0f3d07b98 | ||
|
|
378d85dc67 | ||
|
|
875e91fc0e | ||
|
|
15f7cd9814 | ||
|
|
1eb5cd6237 | ||
|
|
ad2f843c8f | ||
|
|
8e550e216e | ||
|
|
9f07b07c82 | ||
|
|
0be6effc32 | ||
|
|
7ab6a10fc9 | ||
|
|
fb09af0e64 | ||
|
|
0d99d30b2d | ||
|
|
0000ec8b5b | ||
|
|
0085bd8a1f | ||
|
|
617139dfa4 | ||
|
|
4eb4a612d0 | ||
|
|
cda5e784f6 | ||
|
|
d93a280ab3 | ||
|
|
f7e2b3a4a7 | ||
|
|
39d9c8fa74 | ||
|
|
8823895a03 | ||
|
|
b44a9e696c | ||
|
|
cf28a3dc17 | ||
|
|
7416e6caf6 | ||
|
|
90f6896f3c | ||
|
|
eebcd0700d | ||
|
|
133eee0c66 | ||
|
|
640fb75f74 | ||
|
|
51dcc1add6 | ||
|
|
730c928f91 | ||
|
|
c3b7e111b9 | ||
|
|
1874e48925 | ||
|
|
e7a082c91c | ||
|
|
5d4f45407e | ||
|
|
17c37ec32f | ||
|
|
b5f8140c79 | ||
|
|
63f746c237 | ||
|
|
dac6709f27 | ||
|
|
470c8d0b29 | ||
|
|
b0d35e803b | ||
|
|
a71475be8b | ||
|
|
b9f2cc5142 | ||
|
|
2d46e55b9b | ||
|
|
684e254996 | ||
|
|
a2f7903960 | ||
|
|
c0c757d6bd | ||
|
|
da0fad743d | ||
|
|
80b10d6025 | ||
|
|
a27c2a69c4 | ||
|
|
9ed2a2fd19 | ||
|
|
aa9d96718c | ||
|
|
aa67a2b71c | ||
|
|
d3405edd42 | ||
|
|
3612098d62 | ||
|
|
2f08b72d69 | ||
|
|
ab66904c1a | ||
|
|
55542a3dbe | ||
|
|
8569a45114 | ||
|
|
c790311fc3 | ||
|
|
3c45c8bd80 | ||
|
|
d5b7b3ae31 | ||
|
|
43e73a5f24 | ||
|
|
698947ed97 | ||
|
|
f3d967ae07 | ||
|
|
dbe72fa07e | ||
|
|
801a97d85b | ||
|
|
9f8f938c47 | ||
|
|
8fe37d1c1e | ||
|
|
5cca8457e7 | ||
|
|
e9332e7646 | ||
|
|
31365505d8 | ||
|
|
b3fbe9e34a | ||
|
|
4082b651c5 | ||
|
|
0081000ef0 | ||
|
|
ad4d6a1070 | ||
|
|
5190b26399 | ||
|
|
29a8db96f4 | ||
|
|
1a4c2cabfd | ||
|
|
ef9189055c | ||
|
|
5cc3719125 | ||
|
|
5d46f41348 | ||
|
|
3c2c1963f4 | ||
|
|
4896ca9279 | ||
|
|
f0afba6cd9 | ||
|
|
bd717c298a | ||
|
|
baaa8a70dc | ||
|
|
6d561c6e6f | ||
|
|
e6b6947d49 | ||
|
|
52e99a2175 | ||
|
|
052d17a46f | ||
|
|
1aa1f4c212 | ||
|
|
c3a48e3344 | ||
|
|
1d5483dc28 | ||
|
|
54277fa0df | ||
|
|
ab04bd262f | ||
|
|
fb23087b65 | ||
|
|
846fee7ac8 | ||
|
|
977eacc679 | ||
|
|
dacfefe644 | ||
|
|
345e941e11 | ||
|
|
6cb7d45464 | ||
|
|
e7222653fa | ||
|
|
014f0758f5 | ||
|
|
0e8b416f6d | ||
|
|
09a60a2204 | ||
|
|
b0eae307c2 | ||
|
|
f5d2b54cca | ||
|
|
3eefec3899 | ||
|
|
b6a8094554 | ||
|
|
4083b35436 | ||
|
|
bb72d70baf | ||
|
|
95d1a77f52 | ||
|
|
051729886e | ||
|
|
0f00123dc7 | ||
|
|
0b0a089d86 | ||
|
|
c711a7d99a | ||
|
|
43f1d8c88c | ||
|
|
e818e79d20 | ||
|
|
cbad3ff1de | ||
|
|
16a2e5e996 | ||
|
|
331c6a50d0 | ||
|
|
31c4540ec6 | ||
|
|
1e6116554f | ||
|
|
a12ea0e761 | ||
|
|
c9e3bbcd9f | ||
|
|
9c17dc1b8f | ||
|
|
69d1cae686 | ||
|
|
1c2404b6af | ||
|
|
b33b33739d | ||
|
|
2b7886c682 | ||
|
|
106d1f6374 | ||
|
|
e601786bd7 | ||
|
|
fda2a98b40 | ||
|
|
c01d70b8fc | ||
|
|
eccbcc3e28 | ||
|
|
7a4a255a89 | ||
|
|
83bced82b1 | ||
|
|
f3033ce732 | ||
|
|
5c21a1727c | ||
|
|
93aab437b7 | ||
|
|
34e797270f | ||
|
|
0f337a8d8c | ||
|
|
cc9b83089e | ||
|
|
a565929686 | ||
|
|
6adacea774 | ||
|
|
47ab5421ed | ||
|
|
10c404d455 | ||
|
|
dfdca11155 | ||
|
|
698e095364 | ||
|
|
524fd258d8 | ||
|
|
17e70a4360 | ||
|
|
e4a533e7b7 | ||
|
|
0cb68d3737 | ||
|
|
9faeadbebe | ||
|
|
35d201cfb8 | ||
|
|
205174255f | ||
|
|
8873a030ab | ||
|
|
0ab61bac12 | ||
|
|
b1157f60f5 | ||
|
|
bb93df06b2 | ||
|
|
82e807fd80 | ||
|
|
29da539467 | ||
|
|
659aa005b0 | ||
|
|
3f20733e7e | ||
|
|
b15e1174d6 | ||
|
|
05b05fd74e | ||
|
|
d30d467a21 | ||
|
|
cd62e8ca37 | ||
|
|
f9e44820c1 | ||
|
|
169ae6a4d0 | ||
|
|
030ba15952 | ||
|
|
964874bdad | ||
|
|
7affa081ac | ||
|
|
10e281ed35 | ||
|
|
27081ae599 | ||
|
|
61cbcdffe8 | ||
|
|
eeb15ea564 | ||
|
|
565c820925 | ||
|
|
325dff5735 | ||
|
|
397c2cf5f0 | ||
|
|
1fbc339a42 | ||
|
|
f2c719c60d | ||
|
|
08505fcc9a | ||
|
|
a79c933693 | ||
|
|
b4cb3ddf1c | ||
|
|
aa188a6e89 | ||
|
|
a04b6b8a70 | ||
|
|
11149d2743 | ||
|
|
86bfd990db | ||
|
|
9304430889 | ||
|
|
095f1c270b | ||
|
|
d3f91a832b | ||
|
|
4790a1170f | ||
|
|
501c392028 | ||
|
|
9200520f70 | ||
|
|
8122561337 | ||
|
|
c6dc86ef8d | ||
|
|
bea3b8485f | ||
|
|
b807b89cdc | ||
|
|
daac2f7fd9 | ||
|
|
f0a5523174 | ||
|
|
eda8fbb178 | ||
|
|
67ca6184e9 | ||
|
|
d79e91fc1e | ||
|
|
1cdb93baa2 | ||
|
|
f91991e25c | ||
|
|
d21da47a7d | ||
|
|
b4e22a345d | ||
|
|
30e594ae5f | ||
|
|
ffba3573ba | ||
|
|
9df5bee8d3 | ||
|
|
71c0728622 | ||
|
|
476d8ba14d | ||
|
|
274c956f16 | ||
|
|
3068f9ee3d | ||
|
|
a0c49d5f7f | ||
|
|
a8534974fe | ||
|
|
c517790391 | ||
|
|
b7e875c77f | ||
|
|
befd9c0624 | ||
|
|
7a46f11089 | ||
|
|
dc168bf8b9 | ||
|
|
eef5293ca0 | ||
|
|
a2c4498694 | ||
|
|
938a84a460 | ||
|
|
978d2c24ee | ||
|
|
cdd00d665d | ||
|
|
bb8b06c044 | ||
|
|
604c5dcdc1 | ||
|
|
6bc2ecdbf0 | ||
|
|
e91c81def7 | ||
|
|
bedd2fa15a | ||
|
|
50465eef54 | ||
|
|
07689adfcd | ||
|
|
8f4f898675 | ||
|
|
968bd7a437 | ||
|
|
eba5900ba8 | ||
|
|
69c477b104 | ||
|
|
c8df8f4f54 | ||
|
|
d35a19b4fd | ||
|
|
a97437a6e5 | ||
|
|
39c4473367 | ||
|
|
b882bc721d | ||
|
|
405cace489 | ||
|
|
402a7b7fc9 | ||
|
|
8ad805e654 | ||
|
|
b23c357f73 | ||
|
|
f561c2b0fa | ||
|
|
5a8eea668f | ||
|
|
777143e502 | ||
|
|
0d8c9a82fe | ||
|
|
d10ab1cce3 | ||
|
|
ec25e09d73 | ||
|
|
cba9c78ab1 | ||
|
|
c32db4a881 | ||
|
|
871add3071 | ||
|
|
e661c617a3 | ||
|
|
d4bf721540 | ||
|
|
d91b55faed | ||
|
|
9687832d4d | ||
|
|
fc3e436744 | ||
|
|
da90245f7b | ||
|
|
410d6a85d7 | ||
|
|
b693342e4f | ||
|
|
acca361f2e | ||
|
|
b663f47713 | ||
|
|
d332b199b5 | ||
|
|
78bac1dbd1 | ||
|
|
724ff215f9 | ||
|
|
68ea146469 | ||
|
|
82583e616f | ||
|
|
bfc339c58d | ||
|
|
fe4427c076 | ||
|
|
5745f388a9 | ||
|
|
377e3c253f | ||
|
|
3007a0c00e | ||
|
|
f51ffc091d | ||
|
|
c37c364a08 | ||
|
|
331a106e9a | ||
|
|
cd74687b7b | ||
|
|
b3e145c1e6 | ||
|
|
d8e1547736 | ||
|
|
8617f01924 | ||
|
|
55f9e75e6a | ||
|
|
b93e7b7ed1 | ||
|
|
89cc79ad60 | ||
|
|
8dd0e60eea | ||
|
|
df6113fdf6 | ||
|
|
3a3095d15a | ||
|
|
fb4d07391e | ||
|
|
9bef9c85cf | ||
|
|
b77b3f227f | ||
|
|
6a065f0a34 | ||
|
|
4e1e190797 | ||
|
|
1ce8cd2100 | ||
|
|
c03af6b9ad | ||
|
|
adca850075 | ||
|
|
e3616b484e | ||
|
|
cfd7808169 | ||
|
|
addcedc588 | ||
|
|
bfea786088 | ||
|
|
50e84c3c9e | ||
|
|
dc92ace85e | ||
|
|
1a543928b1 | ||
|
|
652fe8d21e | ||
|
|
199690f45f | ||
|
|
37a4dd4b00 | ||
|
|
34d4358bfc | ||
|
|
90906b9019 | ||
|
|
1c212ff2b4 | ||
|
|
7d709f44a8 | ||
|
|
ea9e88a18a | ||
|
|
0be8a9c805 | ||
|
|
fcf8139afe | ||
|
|
62f969b50b | ||
|
|
6726062500 | ||
|
|
cf1f4bdcaf | ||
|
|
b09a14ad4e | ||
|
|
1dc62c9ca3 | ||
|
|
beaa89a2dc | ||
|
|
f39a000b49 | ||
|
|
013a74fb14 | ||
|
|
7c4964753b | ||
|
|
8353533d60 | ||
|
|
c06df27424 | ||
|
|
ad82919ddf | ||
|
|
44dbba17e1 | ||
|
|
5ba110e1da | ||
|
|
b6e392fdb2 | ||
|
|
2280e83aa2 | ||
|
|
f49b94edb9 | ||
|
|
2428a12221 | ||
|
|
9c353f3760 | ||
|
|
5b86d25d7f | ||
|
|
2b168e8bbc | ||
|
|
537db32847 | ||
|
|
498b7f9f2b | ||
|
|
9935568597 | ||
|
|
467003af8c | ||
|
|
4c9edcc47b | ||
|
|
24bf9cf121 | ||
|
|
e06f6f39a9 | ||
|
|
98ee0c307b | ||
|
|
5e53ea0bc3 | ||
|
|
847d88ea77 | ||
|
|
d5046cc2b3 | ||
|
|
3ad64b7cbb | ||
|
|
0dbfe8ca55 | ||
|
|
91b794d66d | ||
|
|
0d65e1e314 | ||
|
|
2d8f58c6d8 | ||
|
|
65888fa816 | ||
|
|
857e882c6e | ||
|
|
add2931834 | ||
|
|
cdda5f45ee | ||
|
|
5f73d6a913 | ||
|
|
0637882fbc | ||
|
|
3f785bab20 | ||
|
|
a4ca89bdd6 | ||
|
|
1a64e796bd | ||
|
|
a8b85a34f7 | ||
|
|
e7bec7d6b0 | ||
|
|
a582026037 | ||
|
|
1a67a001c5 | ||
|
|
406deac592 | ||
|
|
e719ae0676 | ||
|
|
d8b7726440 | ||
|
|
49f642e712 | ||
|
|
70117016ce | ||
|
|
a4738f6281 | ||
|
|
b1fc72d696 | ||
|
|
457c2c2b50 | ||
|
|
48848d7d1a | ||
|
|
55b07ca3ab | ||
|
|
a1d4882e18 | ||
|
|
3843795d8f | ||
|
|
f2bf8d42da | ||
|
|
a3b244e114 | ||
|
|
3093bdbc68 | ||
|
|
9ab0799283 | ||
|
|
236bec11ed | ||
|
|
de48b0f940 | ||
|
|
4885d4db86 | ||
|
|
0c7bbda936 | ||
|
|
fa07c2c1fb | ||
|
|
5d17a191f6 | ||
|
|
67fb74d3c2 | ||
|
|
dc04cfc1b3 | ||
|
|
d61d481965 | ||
|
|
6b346ee1de | ||
|
|
d0f248aaf9 | ||
|
|
85c9227515 | ||
|
|
73b6d3be84 | ||
|
|
1ff6ce2343 | ||
|
|
c145935d46 | ||
|
|
e9ede6924e | ||
|
|
515a21761d | ||
|
|
8d6397028b | ||
|
|
eb4828d81f | ||
|
|
7e74578312 | ||
|
|
640e3516d4 | ||
|
|
bd295a4632 | ||
|
|
166c30fe2c | ||
|
|
66c1bab629 | ||
|
|
66656304f9 | ||
|
|
07f66e379d | ||
|
|
7ae8fd60c4 | ||
|
|
7275066994 | ||
|
|
385adec186 | ||
|
|
96b5bec5ab | ||
|
|
6a9ec4e5f0 | ||
|
|
d9851493df | ||
|
|
efdb520414 | ||
|
|
5548644aeb | ||
|
|
e3fcd91b2d | ||
|
|
2cae30ba88 | ||
|
|
58cd38c4a8 | ||
|
|
3300304feb | ||
|
|
f0e376d06b | ||
|
|
16f7bb48f2 | ||
|
|
7f383dd29b | ||
|
|
3dc529edf4 | ||
|
|
45dedb4872 | ||
|
|
afcdd01c0d | ||
|
|
1164877e9a | ||
|
|
fe92a449ba | ||
|
|
401b0e2bd0 | ||
|
|
cf9c71fcc1 | ||
|
|
15a2400069 | ||
|
|
d68a39b49e | ||
|
|
066ca22e24 | ||
|
|
0418b926fe | ||
|
|
be40bbdf40 | ||
|
|
df4f42e79e | ||
|
|
5f80058f70 | ||
|
|
0cbe59052d | ||
|
|
af28a26e37 | ||
|
|
70c596df93 | ||
|
|
748b51428c | ||
|
|
8ad746397c | ||
|
|
45baed2f9a | ||
|
|
74185f2d33 | ||
|
|
90a91e4105 | ||
|
|
11aa3a0315 | ||
|
|
0c2e39214f | ||
|
|
d89620d7a6 | ||
|
|
edf80775b7 | ||
|
|
46e56ac726 | ||
|
|
40b2f6bfd6 | ||
|
|
911e4921e2 | ||
|
|
1db9bb419d | ||
|
|
c6241a94e3 | ||
|
|
1cbf75ca36 | ||
|
|
8f85c897c8 | ||
|
|
29c31b7aba | ||
|
|
402919d6f2 | ||
|
|
82608dd5ff | ||
|
|
f312368df2 | ||
|
|
374fc64427 | ||
|
|
95bd74bb0d | ||
|
|
a9f5069649 | ||
|
|
957f7ffd8d | ||
|
|
336dd3ce10 | ||
|
|
47a7295477 | ||
|
|
341a0e1c2a | ||
|
|
c4f73d0eb8 | ||
|
|
bd9258bae4 | ||
|
|
e3b3260aa0 | ||
|
|
676766c99e | ||
|
|
1025a07593 | ||
|
|
00c3fcd033 | ||
|
|
b8457d4aff | ||
|
|
a2ecf10d19 | ||
|
|
1e63a2a7e7 | ||
|
|
964014fc5c | ||
|
|
fc2bb6d8c3 | ||
|
|
1b10252d76 | ||
|
|
ad8af12a10 | ||
|
|
b040c9b118 | ||
|
|
f6da7da90b | ||
|
|
a745185408 | ||
|
|
d3336f9027 | ||
|
|
daf42c8203 | ||
|
|
0a18bae3b5 | ||
|
|
919705966c | ||
|
|
2c54aee63e | ||
|
|
3f80bdf2a3 | ||
|
|
1c429b8dd3 | ||
|
|
5669e2b0b7 | ||
|
|
1a6a43babf | ||
|
|
2650db5ddc | ||
|
|
255491a107 | ||
|
|
5c64147dfa | ||
|
|
39f4118577 | ||
|
|
f7f6e4736a | ||
|
|
c635da7ebb | ||
|
|
58124b006a | ||
|
|
563aeccd0f | ||
|
|
bd1a95a7f5 | ||
|
|
cdb25828f2 | ||
|
|
45803b3b23 | ||
|
|
0e5e3d3383 | ||
|
|
4672930037 | ||
|
|
09be7131c3 | ||
|
|
a804f90b9c | ||
|
|
264cb6bbd2 | ||
|
|
b7772e867b | ||
|
|
cc0e77abfb | ||
|
|
537d1c6f4f | ||
|
|
80facadd67 | ||
|
|
ba097dad23 | ||
|
|
c13c15d046 | ||
|
|
4f52128a06 | ||
|
|
500b2d0e6d | ||
|
|
e59d094feb | ||
|
|
a8372f14f8 | ||
|
|
5174ff422d | ||
|
|
5c06751c3b | ||
|
|
ac2b0118a6 | ||
|
|
3eb8fd4abe | ||
|
|
48b389ebe3 | ||
|
|
065adeb2cd | ||
|
|
269d0a06fe | ||
|
|
8eca26b1a5 | ||
|
|
3019ef7de4 | ||
|
|
522311b547 | ||
|
|
21061561ec | ||
|
|
b83c41ad56 | ||
|
|
e80a1cc64a | ||
|
|
a01e4ca89f | ||
|
|
c20362e9b6 | ||
|
|
c90cfb99bd | ||
|
|
7bcea14799 | ||
|
|
b415c1a6d1 | ||
|
|
452c72d280 | ||
|
|
48350be625 | ||
|
|
ab824fb219 | ||
|
|
043d8a1861 | ||
|
|
074ac15d0f | ||
|
|
d36a28fa81 | ||
|
|
ba12bc6c91 | ||
|
|
87332778e5 | ||
|
|
453feb8473 | ||
|
|
8ff469974c | ||
|
|
994ec5ac0f | ||
|
|
43f7f9a363 | ||
|
|
4a11ebc9b9 | ||
|
|
d76a1305e7 | ||
|
|
6a0d592491 | ||
|
|
9898c2196d | ||
|
|
41a8dc840f | ||
|
|
c3eaae9d88 | ||
|
|
3ca959b7a6 | ||
|
|
1d2e2b6e5c | ||
|
|
31d963c4d1 | ||
|
|
7e96118cdc | ||
|
|
709a0744bd | ||
|
|
f59248cc5a | ||
|
|
8647c5c607 | ||
|
|
6699ff38a1 | ||
|
|
d79b98bd55 | ||
|
|
5065a052fb | ||
|
|
45603bb78c | ||
|
|
40948995b4 | ||
|
|
4ccdd8d1d3 | ||
|
|
30d0174f47 | ||
|
|
5a986ba25c | ||
|
|
fe63c24ac3 | ||
|
|
c384bd6875 | ||
|
|
dcbff3f569 | ||
|
|
7d91e05a69 | ||
|
|
a5ce424a40 | ||
|
|
47c36ca062 | ||
|
|
c4c5b3bf8b | ||
|
|
b1a81b0d12 | ||
|
|
ad9fe64850 | ||
|
|
f236349dc6 | ||
|
|
5f56c8a7d4 | ||
|
|
309d8a9f18 | ||
|
|
2981799803 | ||
|
|
00f8e1c0da | ||
|
|
e9482e2ec4 | ||
|
|
9bff327377 | ||
|
|
ae009f98c1 | ||
|
|
77505a6f5b | ||
|
|
19c729aa23 | ||
|
|
595888128a | ||
|
|
51589d0eae | ||
|
|
f1643ac549 | ||
|
|
3f24461612 | ||
|
|
b5deb198de | ||
|
|
78452cf6a9 | ||
|
|
4b4a784f56 | ||
|
|
3e53cbcf8f | ||
|
|
f34740f1f0 | ||
|
|
b406bdfc37 | ||
|
|
03c056702c | ||
|
|
9c5f3f1946 | ||
|
|
b50d7c24e7 | ||
|
|
f05cf68945 | ||
|
|
efc1875e35 | ||
|
|
df063e6762 | ||
|
|
e5c55b4339 | ||
|
|
bee9095d6f | ||
|
|
92f8eaaac9 | ||
|
|
f5e7288fe5 | ||
|
|
214aa7b6e4 | ||
|
|
5b5d5b41f5 | ||
|
|
23d613321e | ||
|
|
0b6be0923f | ||
|
|
aba748ea13 | ||
|
|
f1f1ac582d | ||
|
|
54a7cbc3f4 | ||
|
|
2f4dbaec4c | ||
|
|
578f518aaf | ||
|
|
077ba74b22 | ||
|
|
e0efe635c7 | ||
|
|
1a06841de0 | ||
|
|
3987e0ee0b | ||
|
|
9f53bea02f | ||
|
|
737709f9e7 | ||
|
|
39477aa6a0 | ||
|
|
f097050b56 | ||
|
|
f14726ed1a | ||
|
|
e1e4d038d9 | ||
|
|
d2db4cf887 | ||
|
|
2f3ece9ca3 | ||
|
|
9f82007116 | ||
|
|
f79198a472 | ||
|
|
ce3d35d7ec | ||
|
|
f4d40f0466 | ||
|
|
a2fa085d5f | ||
|
|
a598266a6e | ||
|
|
f5fe33cee7 | ||
|
|
200c7226ef | ||
|
|
53475a6a0e | ||
|
|
b4ec1ad6c0 | ||
|
|
ef511a729d | ||
|
|
275c4ce226 | ||
|
|
45f9c029c8 | ||
|
|
db5e4ad5d9 | ||
|
|
f05d0a9727 | ||
|
|
04593e9d9a | ||
|
|
b1ecf13f8e | ||
|
|
e91e054f20 | ||
|
|
130ff7517e | ||
|
|
c7042d9684 | ||
|
|
5752e45dd1 | ||
|
|
1a034ecb53 | ||
|
|
025da8fb76 | ||
|
|
2027da1db5 | ||
|
|
7732f28ca8 | ||
|
|
7f9da8cc2d | ||
|
|
c6342b80a7 | ||
|
|
f99c82de4b | ||
|
|
56fa57ea02 | ||
|
|
cc85985d08 | ||
|
|
bd1751903e | ||
|
|
03a298a70f | ||
|
|
2722ca2b0e | ||
|
|
179c4b800e | ||
|
|
6bdf14223d | ||
|
|
1b8252aa4f | ||
|
|
8219889154 | ||
|
|
df4ac5dcce | ||
|
|
738eaf9de9 | ||
|
|
c483ccbbbc | ||
|
|
0d65f846ae | ||
|
|
f47e75c423 | ||
|
|
c008e58fb8 | ||
|
|
26e0f17bc5 | ||
|
|
6543f28bdb | ||
|
|
a86851b338 | ||
|
|
3a03e455c6 | ||
|
|
3d39fd1580 | ||
|
|
601b0add26 | ||
|
|
4f974cc913 | ||
|
|
f691320453 | ||
|
|
be39fc3a21 | ||
|
|
d2fafaf33a | ||
|
|
27ae331352 | ||
|
|
3f2dcfbacc | ||
|
|
8565aee8b6 | ||
|
|
f983add599 | ||
|
|
030192afeb | ||
|
|
c8b6a158f1 | ||
|
|
e71f7849a7 | ||
|
|
b64d1ff4ff | ||
|
|
5a0028be26 | ||
|
|
926d7deb43 | ||
|
|
6384b50bae | ||
|
|
9feb0f4b53 | ||
|
|
43ec1b7cfd | ||
|
|
05b7a59f8d | ||
|
|
17e680f7af | ||
|
|
035d256d4e | ||
|
|
8939adf886 | ||
|
|
027ffbffa6 | ||
|
|
3cca06712b | ||
|
|
2b9359dbf4 | ||
|
|
c0f5d3bd2e | ||
|
|
2a2d5382e1 | ||
|
|
2e4986024c | ||
|
|
8a9c605dae | ||
|
|
44f51a93c8 | ||
|
|
66c8537b41 | ||
|
|
86ae6dd332 | ||
|
|
69380c9c73 | ||
|
|
3d3759137c | ||
|
|
9b9b8f6f6f | ||
|
|
8ff87a8245 | ||
|
|
d1896da171 | ||
|
|
0bc4f6fd96 | ||
|
|
b16a429686 | ||
|
|
fa1d266696 | ||
|
|
d5dd2e9551 | ||
|
|
be57c312c4 | ||
|
|
f180687ba3 | ||
|
|
3f3d9cc6f1 | ||
|
|
4f98c0d045 | ||
|
|
c254441d40 | ||
|
|
17cbe74fa3 | ||
|
|
7aa0bd9b79 | ||
|
|
2553cf6b72 | ||
|
|
fe9050aeda | ||
|
|
7092894d22 | ||
|
|
af6ac26664 | ||
|
|
a22ef67486 | ||
|
|
7bb57cd78a | ||
|
|
89b69bbdf8 | ||
|
|
e21c779d06 | ||
|
|
dfa3553b71 | ||
|
|
19097388d0 | ||
|
|
a71eddbed2 | ||
|
|
65bbed0c26 | ||
|
|
871cc61dfc | ||
|
|
bc62feb71b | ||
|
|
0bba329999 | ||
|
|
b1a1fdbeee | ||
|
|
542c5beb1b | ||
|
|
7b87b0919b | ||
|
|
9c34f558d3 | ||
|
|
3e2da3b490 | ||
|
|
fb4a4f50be | ||
|
|
6596e9cab6 | ||
|
|
f1b137f2e1 | ||
|
|
535720d0fe | ||
|
|
f063cf4a16 | ||
|
|
90bbdbf2fe | ||
|
|
5f1d8fb99d | ||
|
|
5486ffcdcc | ||
|
|
adfd123970 | ||
|
|
f1a364bfa2 | ||
|
|
9da714bf15 | ||
|
|
fc73295520 | ||
|
|
ab955e41fb | ||
|
|
c64367335c | ||
|
|
edc787eb3e | ||
|
|
f2c69fc68b | ||
|
|
d947fe743b | ||
|
|
5b37ae9026 | ||
|
|
ec9e042b29 | ||
|
|
337ac0eab9 | ||
|
|
f6a1b784c4 | ||
|
|
332fcecb78 | ||
|
|
18590be1e7 | ||
|
|
b76edcaf1d | ||
|
|
6024cabb69 | ||
|
|
08446e648e | ||
|
|
14af7a3572 | ||
|
|
cdc4275f81 | ||
|
|
a9ade98315 | ||
|
|
f3ae6fa70f | ||
|
|
96457bbec3 | ||
|
|
8f465e376e | ||
|
|
adc366a959 | ||
|
|
a20a6bc8bb | ||
|
|
b176fa66d4 | ||
|
|
f81b1926fb | ||
|
|
7b7609a068 | ||
|
|
670d4108e6 | ||
|
|
a5c7b88a40 | ||
|
|
e52b2e6d69 | ||
|
|
e4066fb8df | ||
|
|
f7a0fb22b4 | ||
|
|
cad2ae723c | ||
|
|
889a8c6093 | ||
|
|
573418914f | ||
|
|
d7fb6f9c05 | ||
|
|
136e27d655 | ||
|
|
d5ff2d7099 | ||
|
|
2a7f8d0c99 | ||
|
|
e3ca5df713 | ||
|
|
bda32f3e8f | ||
|
|
a7c6e45a92 | ||
|
|
7c20ca9b64 | ||
|
|
a201461eff | ||
|
|
e5d9df37c5 | ||
|
|
106fbaf086 | ||
|
|
a0024c98d5 | ||
|
|
684a702638 | ||
|
|
aec4a009d1 | ||
|
|
822af575c9 | ||
|
|
485efa7d44 | ||
|
|
3d09d45423 | ||
|
|
4c69c6d9fd | ||
|
|
920a41acef | ||
|
|
0cf13a284c | ||
|
|
a89cdef436 | ||
|
|
881d88f4ad | ||
|
|
a72c96f56d | ||
|
|
bc8235b209 | ||
|
|
0087495749 | ||
|
|
9560afd4a7 | ||
|
|
56ec8559a0 | ||
|
|
99ca79ac7d | ||
|
|
24564f4c74 | ||
|
|
212c802a1e | ||
|
|
984b5d6c40 | ||
|
|
0e3a4191a9 | ||
|
|
570a34bca5 | ||
|
|
c9a0c29286 | ||
|
|
b5f804ec22 | ||
|
|
dadbb83271 | ||
|
|
848aacdbbf | ||
|
|
da3665a167 | ||
|
|
dcf0a06217 | ||
|
|
3b5e6553cd | ||
|
|
509390af20 | ||
|
|
9ad511a9c0 | ||
|
|
89c102513d | ||
|
|
5e65ae76ad | ||
|
|
b6c364cd78 | ||
|
|
e086b8707f | ||
|
|
90dddd10a9 | ||
|
|
2dd0907565 | ||
|
|
f31b0d0c71 | ||
|
|
a0825b75f7 | ||
|
|
a3bd4c0f73 | ||
|
|
a3e8c9b28a | ||
|
|
d7fb850b4a | ||
|
|
d084778a6e | ||
|
|
8ca30de760 | ||
|
|
8a10b81bd9 | ||
|
|
4a93c4e584 | ||
|
|
50177cd6bd | ||
|
|
71a2e52739 | ||
|
|
4fac6d5aa3 | ||
|
|
37d061b602 | ||
|
|
40193e4edc | ||
|
|
b4e9d61871 | ||
|
|
d44b589e55 | ||
|
|
68216415b6 | ||
|
|
ba53da18d1 | ||
|
|
9b76fa3582 | ||
|
|
13d8d10a7f | ||
|
|
5c6c1bb09d | ||
|
|
12105d96ea | ||
|
|
4054756035 | ||
|
|
16769c7838 | ||
|
|
cd076c5959 | ||
|
|
f52e1aa131 | ||
|
|
fdc1ef7e9a | ||
|
|
9cccf2d47b | ||
|
|
0796f27f2a | ||
|
|
6c84014e0d | ||
|
|
cd496a22bf | ||
|
|
0200343780 | ||
|
|
47fb629d26 | ||
|
|
71ae08706b | ||
|
|
50dd798757 | ||
|
|
0c8cf73746 | ||
|
|
1bee811312 | ||
|
|
b4c0068637 | ||
|
|
f484c6e5fe | ||
|
|
7a08187c5f | ||
|
|
c4d7d5a0d4 | ||
|
|
5b75e753a7 | ||
|
|
326e9b86ce | ||
|
|
d22f5d369c | ||
|
|
d76503995c | ||
|
|
eab930c083 | ||
|
|
e430cc54f2 | ||
|
|
fd26a9c698 | ||
|
|
e79b608f77 | ||
|
|
42b23a6c9c | ||
|
|
8d94f24c71 | ||
|
|
6ac74c39d9 | ||
|
|
836eb7b708 | ||
|
|
698624b4dc | ||
|
|
5c1df82076 | ||
|
|
5d649b3687 | ||
|
|
a6a3d71155 | ||
|
|
1cc9d501ab | ||
|
|
7a98025df8 | ||
|
|
44d6ed5e80 | ||
|
|
b5f2226bef | ||
|
|
ddbffe55d2 | ||
|
|
9676b1d0e9 | ||
|
|
8142d3bfeb | ||
|
|
755ad27a0a | ||
|
|
5afa2dcdf1 | ||
|
|
03098ee024 | ||
|
|
a2bfdd003c | ||
|
|
7eb80646ba | ||
|
|
6fd24e57d3 | ||
|
|
22c90adb47 | ||
|
|
df0c6fafbe | ||
|
|
dc30321b04 | ||
|
|
63dd98d2df | ||
|
|
caaa6ed506 | ||
|
|
caf23792cb | ||
|
|
e430db20aa | ||
|
|
6fc5da9b67 | ||
|
|
f428e57724 | ||
|
|
14ab21fe9a | ||
|
|
85626e19da | ||
|
|
8712160fd7 | ||
|
|
75b33f5cb1 | ||
|
|
f5e8ede847 | ||
|
|
3b3f684a8c | ||
|
|
a78b60d40e | ||
|
|
9ff06a3c44 | ||
|
|
8532dc486c | ||
|
|
861340f4bf | ||
|
|
cdcb51ebe4 | ||
|
|
0b11786d7d | ||
|
|
1742247a9a | ||
|
|
42bad123b2 | ||
|
|
2d1e87defc | ||
|
|
1c6f783a07 | ||
|
|
6aafc097d5 | ||
|
|
4010f233dd | ||
|
|
75f67caa1b | ||
|
|
d760ce54b7 | ||
|
|
956976ebd5 | ||
|
|
f9c2d4ca6c | ||
|
|
dd5cc3c38c | ||
|
|
daed4cc13e | ||
|
|
6ff614dd18 | ||
|
|
eb70ac4266 | ||
|
|
a3a431adb7 | ||
|
|
e12c72ab98 | ||
|
|
9f8549b831 | ||
|
|
b2de256f87 | ||
|
|
7f32a5cf9e | ||
|
|
56f8314d29 | ||
|
|
4ceb2a8669 | ||
|
|
c778d3b699 | ||
|
|
47eda9cdf2 | ||
|
|
dcaec4d356 | ||
|
|
aee4f349c6 | ||
|
|
daa2c39902 | ||
|
|
5770fc02a1 | ||
|
|
47cafd295b | ||
|
|
3296f2daf8 | ||
|
|
962616545c | ||
|
|
11ea92c078 | ||
|
|
1d64fa4817 | ||
|
|
c46f2956c2 | ||
|
|
8f6d4298be | ||
|
|
3bce81326e | ||
|
|
2ae9f6d0fe | ||
|
|
9266828278 | ||
|
|
a8a2ffc33e | ||
|
|
27c4543471 | ||
|
|
50a02cb59e | ||
|
|
50579bb9e6 | ||
|
|
50512ca63c | ||
|
|
8b3577b216 | ||
|
|
7553aab932 | ||
|
|
5dacdcfe5e | ||
|
|
8645a412b7 | ||
|
|
acad07a588 | ||
|
|
51bbb480bb | ||
|
|
f0306cd10a | ||
|
|
25253ad9e7 | ||
|
|
51f2fb8e8b | ||
|
|
9e8d650cbd | ||
|
|
d222ccfa58 | ||
|
|
9a05aaa906 | ||
|
|
00fdce8876 | ||
|
|
29b51adf7d | ||
|
|
8e14b39969 | ||
|
|
d9fb4d6c4d | ||
|
|
fcf2f4c5f2 | ||
|
|
4e97501690 | ||
|
|
b0402391fb | ||
|
|
f05a862cf9 | ||
|
|
3ea92d57c2 | ||
|
|
254b85fbd8 | ||
|
|
16371c0cc4 | ||
|
|
2256d67e2b | ||
|
|
0af0bdede6 | ||
|
|
379f31b9de | ||
|
|
771a524734 | ||
|
|
560e18a610 | ||
|
|
147bdfab95 | ||
|
|
36b1b0f663 | ||
|
|
91511e4c3f | ||
|
|
6a72056b25 | ||
|
|
62e0c57a50 | ||
|
|
9d92270931 | ||
|
|
f61321d5a6 | ||
|
|
08ab2f8649 | ||
|
|
82962c4b42 | ||
|
|
bd24e8a4ad | ||
|
|
6224d9a292 | ||
|
|
bbc58f3671 | ||
|
|
fcd620283f | ||
|
|
a78def3d2d | ||
|
|
43e94a5db0 | ||
|
|
e77bcc1267 | ||
|
|
9b458958b8 | ||
|
|
35419ade29 | ||
|
|
15bd2ee887 | ||
|
|
9394bafa8e | ||
|
|
94150a0c48 | ||
|
|
8955fdfc23 | ||
|
|
c13aa6a545 | ||
|
|
c73b50bd4a | ||
|
|
0a17a38bf1 | ||
|
|
0f7bfe1d66 | ||
|
|
cf3f488663 | ||
|
|
5f536fdb73 | ||
|
|
99d0b13cce | ||
|
|
b04937f012 | ||
|
|
91c9b059cf | ||
|
|
35cc643440 | ||
|
|
b23bb8c46a | ||
|
|
64fdf62c4b | ||
|
|
1c8a808571 | ||
|
|
d8f0295032 | ||
|
|
d59771ac2f | ||
|
|
45df093fac | ||
|
|
fba2078fc0 | ||
|
|
20a37fe2de | ||
|
|
8f6d26b65c | ||
|
|
b58a194c8a | ||
|
|
52f1b0a0ce | ||
|
|
c2b8fb223b | ||
|
|
20e4eff899 | ||
|
|
0efcca36d2 | ||
|
|
ab417802a0 | ||
|
|
73d68cce4a | ||
|
|
e4ac2de660 | ||
|
|
8d1241808a | ||
|
|
b810040145 | ||
|
|
c01d7dae2d | ||
|
|
dfca3c2483 | ||
|
|
1bb4be086f | ||
|
|
fd226c45f6 | ||
|
|
21fed5b25f | ||
|
|
dde093d321 | ||
|
|
b99fb247ac | ||
|
|
28930fdad4 | ||
|
|
ea4d1d3275 | ||
|
|
62e852d510 | ||
|
|
7ddd4d6461 | ||
|
|
6b9307de2a | ||
|
|
234046ce10 | ||
|
|
73b29cf1e2 | ||
|
|
4b3bf170c0 | ||
|
|
a7fbaba2d7 | ||
|
|
fc79241f3d | ||
|
|
a88c37ea56 | ||
|
|
9b2358b7f1 | ||
|
|
257135763f | ||
|
|
610a3499f2 | ||
|
|
69752b8837 | ||
|
|
610473b57c | ||
|
|
1e5721d7d5 | ||
|
|
6c2b45679a | ||
|
|
6785922379 | ||
|
|
4e85124aeb | ||
|
|
6b30a03f55 | ||
|
|
876894d8c6 | ||
|
|
ea20d94146 | ||
|
|
c7669777cb | ||
|
|
3b43bba4a0 | ||
|
|
0ab4946bf1 | ||
|
|
a7fb18d5c0 | ||
|
|
a0fbb0f861 | ||
|
|
e6c93ab1c0 | ||
|
|
7152213344 | ||
|
|
a8e913cfde | ||
|
|
4ac074f3dd | ||
|
|
436249597d | ||
|
|
016a742d90 | ||
|
|
6d863ac29c | ||
|
|
ae981fe57d | ||
|
|
0c6a75b722 | ||
|
|
bfd9b1b7c7 | ||
|
|
12f6b1ca45 | ||
|
|
04264110ee | ||
|
|
e4a112c329 | ||
|
|
ef4dee8886 | ||
|
|
e7ee21ca30 | ||
|
|
23ee480c4f | ||
|
|
7816271302 | ||
|
|
b57814f14a | ||
|
|
b18e86f81c | ||
|
|
7b1b503703 | ||
|
|
32d4febf10 | ||
|
|
814973af58 | ||
|
|
ecee642e10 | ||
|
|
9afc0f6667 | ||
|
|
e9e517533a | ||
|
|
4a531ccea1 | ||
|
|
fb8e0595c2 | ||
|
|
d748d6e400 | ||
|
|
6b99fa1f24 | ||
|
|
ca5abc635c | ||
|
|
35e75be0d0 | ||
|
|
cf401a659d | ||
|
|
bd56968efb | ||
|
|
a78bc686cd | ||
|
|
ad8c962c25 | ||
|
|
be91976498 | ||
|
|
57821b839e | ||
|
|
ad334ed09f | ||
|
|
a955937e02 | ||
|
|
3c42cc17c8 | ||
|
|
deeab036d3 | ||
|
|
a1badcd9a1 | ||
|
|
52762438c6 | ||
|
|
3294079b72 | ||
|
|
1c6bdf20b6 | ||
|
|
fac00be995 | ||
|
|
e7e8e99946 | ||
|
|
9f9749548a | ||
|
|
db1ac85acf | ||
|
|
d5eaeb429a | ||
|
|
4e7595d8d1 | ||
|
|
f25fdcdc3d | ||
|
|
7a4de75e07 | ||
|
|
545b57a57d | ||
|
|
32c3aa7979 | ||
|
|
013f703241 | ||
|
|
c463ad5fd6 | ||
|
|
412b8473fe | ||
|
|
02df2132b4 | ||
|
|
a964d5c93f | ||
|
|
eafc32a915 | ||
|
|
df4b84b4b9 | ||
|
|
6e094eb4fc | ||
|
|
9e7d7bcb4c | ||
|
|
63b3cc8c02 | ||
|
|
7a88786685 | ||
|
|
427889f8ca | ||
|
|
82c9c28439 | ||
|
|
c84c1f2e96 | ||
|
|
a3ee8672ed | ||
|
|
4cfde09016 | ||
|
|
0b8dcbebe9 | ||
|
|
aa12506221 | ||
|
|
39ed9dea01 | ||
|
|
6f095470ad | ||
|
|
2a5d2cc146 | ||
|
|
b5e8218551 | ||
|
|
062cc307fb | ||
|
|
e99ff1be35 | ||
|
|
404a213896 | ||
|
|
0a07f16ef6 | ||
|
|
f9bf8f9901 | ||
|
|
b6ae67bf3e | ||
|
|
191ce0798f | ||
|
|
40362590c8 | ||
|
|
87f6dc7c0b | ||
|
|
2f2c1f263a | ||
|
|
8841cbb3d0 | ||
|
|
a2e20a8092 | ||
|
|
6a7c7a0ab5 | ||
|
|
44a8c8e35d | ||
|
|
1cbfccc4eb | ||
|
|
e12c0b5536 | ||
|
|
b7a8781308 | ||
|
|
73a8fcd35b | ||
|
|
a2ad39f78d | ||
|
|
832635d6f5 | ||
|
|
dacb56bc20 | ||
|
|
e5a9821027 | ||
|
|
bbe666eb73 | ||
|
|
000cb3d80c | ||
|
|
40f85dbf5f | ||
|
|
d6646ebadf | ||
|
|
e02bddc78f | ||
|
|
08e679184b | ||
|
|
5c877e894b | ||
|
|
5918f03cb1 | ||
|
|
78263d716c | ||
|
|
15f4841328 | ||
|
|
ae5d50141b | ||
|
|
8839563ff8 | ||
|
|
6d954b2d5d | ||
|
|
6e125f15a4 | ||
|
|
e344921a06 | ||
|
|
4cbaf0dc70 | ||
|
|
ef7d2f4a82 | ||
|
|
5f7d998b0b | ||
|
|
2c14281168 | ||
|
|
9feab4bc79 | ||
|
|
63237bc112 | ||
|
|
99f4752c89 | ||
|
|
ef1ed5aa8b | ||
|
|
1258270ac4 | ||
|
|
bb7a2f5f6c | ||
|
|
c865d32d95 | ||
|
|
87c3b24488 | ||
|
|
5a5257294b | ||
|
|
a9ca951854 | ||
|
|
2a9353ee70 | ||
|
|
18e134b92a | ||
|
|
6c87e15a52 | ||
|
|
a710821c35 | ||
|
|
de4aeedce5 | ||
|
|
1f71a01453 | ||
|
|
6371d79d33 | ||
|
|
80d2218aa6 | ||
|
|
bc636f109c | ||
|
|
76d58af4d8 | ||
|
|
84e5417a8c | ||
|
|
89188958ec | ||
|
|
b5d24d751d | ||
|
|
3269061db4 | ||
|
|
9f576f43cc | ||
|
|
505c6e0e0e | ||
|
|
9936b49ee0 | ||
|
|
707bc765b9 | ||
|
|
8780c987ea | ||
|
|
7aa01f786d | ||
|
|
340e94d54e | ||
|
|
509b123064 | ||
|
|
8060b1c753 | ||
|
|
33ef3e7d59 | ||
|
|
704e5f7134 | ||
|
|
91bc3ab525 | ||
|
|
2e6bded9d0 | ||
|
|
c6e980ed96 | ||
|
|
32a932ad5c | ||
|
|
f6d2bd04e9 | ||
|
|
b0266b470f | ||
|
|
0ed969fa3f | ||
|
|
90a7b5e0d3 | ||
|
|
c5bf656fe7 | ||
|
|
6bce4533a3 | ||
|
|
8a8aa0016e | ||
|
|
7e4ebd330c | ||
|
|
c304845117 | ||
|
|
ee85f3e824 | ||
|
|
3f6f1dcd78 | ||
|
|
c271a4b2cb | ||
|
|
fa07dfb720 | ||
|
|
6f6b258f22 | ||
|
|
717b246cb6 | ||
|
|
5990e0c2eb | ||
|
|
827df80ec8 | ||
|
|
7117fae2b2 | ||
|
|
15e9462140 | ||
|
|
714f8327ea | ||
|
|
fedb77e304 | ||
|
|
06d2884a88 | ||
|
|
b50556802c | ||
|
|
b18dfdb9ba | ||
|
|
173f83808e | ||
|
|
fbe2d78331 | ||
|
|
e5fd9c6366 | ||
|
|
3623b991ff | ||
|
|
2cabd7879c | ||
|
|
a1a378d6f5 | ||
|
|
b016268fdb | ||
|
|
dfb31b78d9 | ||
|
|
4eed603d36 | ||
|
|
cdc10d6c4b | ||
|
|
cb03501eff | ||
|
|
c2e28ab5a6 | ||
|
|
24a166cb94 | ||
|
|
1d3ac0c9b3 | ||
|
|
ff29b62398 | ||
|
|
d9e016db8b | ||
|
|
6cfd50a7b8 | ||
|
|
6605d3812a | ||
|
|
be71abe580 | ||
|
|
518ff48e97 | ||
|
|
8a4add257f | ||
|
|
7842cd0bc0 | ||
|
|
ffe480ad44 | ||
|
|
e4d3f95257 | ||
|
|
245eabe85f | ||
|
|
22e7eb1ffc | ||
|
|
d663a58d65 | ||
|
|
fa5d5f1bcc | ||
|
|
7178095aef | ||
|
|
4704faa011 | ||
|
|
0f77d9df1f | ||
|
|
e02c3fca8b | ||
|
|
abe0838a63 | ||
|
|
6583e3d0c9 | ||
|
|
2093d68bfb | ||
|
|
d3a55d50c0 | ||
|
|
471733b243 | ||
|
|
47fa717bce | ||
|
|
cfc68e70b6 | ||
|
|
bd9ee62118 | ||
|
|
aaa874b099 | ||
|
|
958709faf2 | ||
|
|
52ab93013c | ||
|
|
9203fa3df2 | ||
|
|
9ef3edabce | ||
|
|
c771d75a00 | ||
|
|
1db27ab0e3 | ||
|
|
cd45f7051c | ||
|
|
588ea7978e | ||
|
|
946f12cf6a | ||
|
|
7e49bfa984 | ||
|
|
7b10d75aeb | ||
|
|
34e4963ccd | ||
|
|
9fc9ed805c | ||
|
|
db4c5bc3a3 | ||
|
|
024faa2561 | ||
|
|
b46459de5f | ||
|
|
ac7f025223 | ||
|
|
e4343650c6 | ||
|
|
80c5259b05 | ||
|
|
004a65d933 | ||
|
|
38289918c2 | ||
|
|
0e46f9f213 | ||
|
|
be03b973e5 | ||
|
|
a0bf2b3d3d | ||
|
|
755ab36e83 | ||
|
|
30975f7360 | ||
|
|
828307fc52 | ||
|
|
e79ca4fa4c | ||
|
|
43288eb5c0 | ||
|
|
9518595e48 | ||
|
|
3c4cd3743f | ||
|
|
b39cbffe14 | ||
|
|
f588d3f35b | ||
|
|
54b06872eb | ||
|
|
0786c608a4 | ||
|
|
c70b2eaa30 | ||
|
|
c417a95e1f | ||
|
|
5ae9be0291 | ||
|
|
e2a6e3ea58 | ||
|
|
8fd5fa185b | ||
|
|
66707661e9 | ||
|
|
4e18ec5951 | ||
|
|
05b77b5042 | ||
|
|
d6cfe11d97 | ||
|
|
dd4d59e4e7 | ||
|
|
7cb8626e16 | ||
|
|
89b5202adb | ||
|
|
40ae0c1449 | ||
|
|
81a8115c56 | ||
|
|
0ddd26bd51 | ||
|
|
69e2133a27 | ||
|
|
b04f85949b | ||
|
|
667dba01ae | ||
|
|
85a8cef628 | ||
|
|
3099acfd00 | ||
|
|
b00fe0b5f8 | ||
|
|
9a64b8bdb6 | ||
|
|
c01e493bd2 | ||
|
|
890236af23 | ||
|
|
ea678d805d | ||
|
|
29699418ff | ||
|
|
87bb36c39f | ||
|
|
7cb85ed73c | ||
|
|
c647771a6d | ||
|
|
6c3d737219 | ||
|
|
a1f38fed7a | ||
|
|
c36bb77286 | ||
|
|
1a1acdc3c9 | ||
|
|
ef8e60c405 | ||
|
|
31c1cc47bf | ||
|
|
351fed7359 | ||
|
|
f49e7cbe57 | ||
|
|
7da8ea5e99 | ||
|
|
8fa6a12a7c | ||
|
|
1070278eaf | ||
|
|
16b1a6b153 | ||
|
|
096a7534e0 | ||
|
|
b0897187d2 | ||
|
|
885d94882d | ||
|
|
11a3341e13 | ||
|
|
231890f78a | ||
|
|
a272feda6a | ||
|
|
bc936a0ca7 | ||
|
|
28030c2d13 | ||
|
|
7fe9176286 | ||
|
|
1105f9b8d6 | ||
|
|
e4653defa8 | ||
|
|
1c62a1e839 | ||
|
|
3a3bbfe201 | ||
|
|
1c38833998 | ||
|
|
38894177ee | ||
|
|
dce8416942 | ||
|
|
14219e9b42 | ||
|
|
7b459e7502 | ||
|
|
31824c0504 | ||
|
|
e203abae85 | ||
|
|
faf83b680b | ||
|
|
67dcbcb842 | ||
|
|
6533a25404 | ||
|
|
4dc760b0e9 | ||
|
|
25933b9043 | ||
|
|
a53aaa456e | ||
|
|
e8a7ea07a5 | ||
|
|
8817dc6b10 | ||
|
|
491ec04b46 | ||
|
|
8a5d4a683b | ||
|
|
dfc7c7357a | ||
|
|
690a2f7d34 | ||
|
|
58f22b24e4 | ||
|
|
3cce9f528b | ||
|
|
20fd5ac8cb | ||
|
|
9e05e086eb | ||
|
|
056e0adddf | ||
|
|
b36388200d | ||
|
|
9793e5741a | ||
|
|
143380c012 | ||
|
|
4b92254945 | ||
|
|
f9c1d8b4a6 | ||
|
|
c0c469339b | ||
|
|
0ca6343ed7 | ||
|
|
3db74c3427 | ||
|
|
48d5cb53bd | ||
|
|
fd7d2dbf53 | ||
|
|
6609697752 | ||
|
|
dcd6e1973e | ||
|
|
3614a6e932 | ||
|
|
931a0210e5 | ||
|
|
f9e7de4b42 | ||
|
|
8e0b79594e | ||
|
|
17122c4360 | ||
|
|
154f7b6a30 | ||
|
|
52e5543d0b | ||
|
|
3c304bd2ae | ||
|
|
26609bb8fd | ||
|
|
de3fa9aaa4 | ||
|
|
788665f84c | ||
|
|
3943782971 | ||
|
|
8f899c40f2 | ||
|
|
a1f582399e | ||
|
|
440b63f662 | ||
|
|
7d2cc3b56b | ||
|
|
5fe3422469 | ||
|
|
6c02cedb1e | ||
|
|
3cc2f1dcad | ||
|
|
773cdc5877 | ||
|
|
361a7329d7 | ||
|
|
29910f1236 | ||
|
|
4a164016f5 | ||
|
|
cebd3e62a4 | ||
|
|
2562a38fa1 | ||
|
|
d46c922bbf | ||
|
|
66b59982f7 | ||
|
|
ad397ccf7f | ||
|
|
bdef80ede7 | ||
|
|
385dcbc75a | ||
|
|
74cf501c8f | ||
|
|
0c200d6748 | ||
|
|
e65a36c517 | ||
|
|
126b54ad40 | ||
|
|
78637751af | ||
|
|
f96526ee3a | ||
|
|
b3c7a91f3d | ||
|
|
b8daeef0c4 | ||
|
|
2b662944cf | ||
|
|
3d516df01e | ||
|
|
26b4a9b15b | ||
|
|
0f8af273ae | ||
|
|
fa29a31da9 | ||
|
|
9e0d2606d8 | ||
|
|
338dedd6e0 | ||
|
|
1f893b1393 | ||
|
|
b783d6f928 | ||
|
|
8ca0d40f05 | ||
|
|
2f40a80434 | ||
|
|
812d8eb5bb | ||
|
|
bf5f548349 | ||
|
|
ebf90e72b9 | ||
|
|
31d7d42edf | ||
|
|
833875b42f | ||
|
|
b901c10f3c | ||
|
|
8ab678bd97 | ||
|
|
55d5072f46 | ||
|
|
a379ffd0f2 | ||
|
|
4501d73134 | ||
|
|
5f15774ec7 | ||
|
|
c9e057599e | ||
|
|
66851f5625 | ||
|
|
b33c235b4d | ||
|
|
d6693b6114 | ||
|
|
36b4d26c78 | ||
|
|
617592d90a | ||
|
|
15a77b8070 | ||
|
|
606eccd22b | ||
|
|
5613450313 | ||
|
|
c59b5564af | ||
|
|
330b086b8b | ||
|
|
9837ef4f36 | ||
|
|
add46b3251 | ||
|
|
e169199107 | ||
|
|
92fe654850 | ||
|
|
b257486404 | ||
|
|
bdf2e33f40 | ||
|
|
224d361923 | ||
|
|
3452fa56df | ||
|
|
cd256235da | ||
|
|
361a164f2a | ||
|
|
0e60d4b198 | ||
|
|
67b47e39b4 | ||
|
|
8f54310f63 | ||
|
|
c7a7494d7e | ||
|
|
af88b3166d | ||
|
|
b7837b2a14 | ||
|
|
950ddc749e | ||
|
|
df081ef0cf | ||
|
|
7b24f90d9f | ||
|
|
f2e4579fd8 | ||
|
|
97cb351827 | ||
|
|
c1ec53fdbb | ||
|
|
98214aa429 | ||
|
|
ce7deac2dd | ||
|
|
612092b867 | ||
|
|
92579d5949 | ||
|
|
9ab07060ae | ||
|
|
0d45125d79 | ||
|
|
9ced152778 | ||
|
|
3685ab2e3e | ||
|
|
be605f11f2 | ||
|
|
8cca8df976 | ||
|
|
990a31e961 | ||
|
|
5db201c342 | ||
|
|
a625e30dd4 | ||
|
|
b236cdd060 | ||
|
|
2db9899184 | ||
|
|
fe5d6db986 | ||
|
|
7c7bf8fecf | ||
|
|
76e3a46378 | ||
|
|
16f3897fec | ||
|
|
045e120854 | ||
|
|
2b7fcce9b2 | ||
|
|
9685931694 | ||
|
|
1dc844435a | ||
|
|
18892379de | ||
|
|
620d61c8dc | ||
|
|
9f91398875 | ||
|
|
a29b1154a9 | ||
|
|
34d19a471a | ||
|
|
2ef6477d7c | ||
|
|
26e6800836 | ||
|
|
9dbbcf3872 | ||
|
|
6b3343e1e4 | ||
|
|
ef48f754a5 | ||
|
|
3be1ede847 | ||
|
|
7bff1b61e8 | ||
|
|
6affd0eb68 | ||
|
|
0a112d15e0 | ||
|
|
4e03f582bb | ||
|
|
8f186c1c5e | ||
|
|
cd1bae9a1f | ||
|
|
60796c26ca | ||
|
|
28927f950d | ||
|
|
95f16ebc8c | ||
|
|
25bca8385d | ||
|
|
965c7f23b4 | ||
|
|
33082af9cc | ||
|
|
1f7f3565b0 | ||
|
|
f784363696 | ||
|
|
37bd51e138 | ||
|
|
c367728c43 | ||
|
|
61f0f5d884 | ||
|
|
5f2ebeead7 | ||
|
|
7646037fc7 | ||
|
|
eac6d285ff | ||
|
|
b921d5e734 | ||
|
|
831d808e63 | ||
|
|
451b88d7e3 | ||
|
|
f916682a71 | ||
|
|
2d76bcf0cf | ||
|
|
8db294efe6 | ||
|
|
5cc5149aed | ||
|
|
7ecb01dc9f | ||
|
|
8bf1a545d9 | ||
|
|
efb2be2f94 | ||
|
|
bd2edda494 | ||
|
|
991172eae4 | ||
|
|
fc7631f9aa | ||
|
|
a21efb7d2f | ||
|
|
03bc844ad0 | ||
|
|
f7bdc35ed6 | ||
|
|
0efdffd857 | ||
|
|
6a16a42d0c | ||
|
|
e9c00c72b1 | ||
|
|
ab22f36b8a | ||
|
|
c57497cd91 | ||
|
|
ba123236e5 | ||
|
|
338b6e4607 | ||
|
|
f88c717560 | ||
|
|
f8ffc92db5 | ||
|
|
98c23c172c | ||
|
|
781c107d8c | ||
|
|
186668c075 | ||
|
|
cf9f785193 | ||
|
|
72d2d3f224 | ||
|
|
087c76b394 | ||
|
|
4f9fb2c8c3 | ||
|
|
334e43e764 | ||
|
|
7843256402 | ||
|
|
0522ba35fe | ||
|
|
24d3b52e0b | ||
|
|
3177110f0f | ||
|
|
e1b8243a67 | ||
|
|
b1c6ce3885 | ||
|
|
0b4b25a11e | ||
|
|
1176fe984a | ||
|
|
6ca768c3ee | ||
|
|
3da1659c8d | ||
|
|
9aa4cd319c | ||
|
|
5af866cdca | ||
|
|
2b421fa447 | ||
|
|
30ec964325 | ||
|
|
714d7d72eb | ||
|
|
687aa0f363 | ||
|
|
8363ab07a7 | ||
|
|
c46a757339 | ||
|
|
557864395b | ||
|
|
3f7a85d80b | ||
|
|
8d18d2ce1f | ||
|
|
7141ba1587 | ||
|
|
44d350a225 | ||
|
|
239b8e72d9 | ||
|
|
279bdb6fb1 | ||
|
|
a0cea819da | ||
|
|
9ab7f60544 | ||
|
|
aaf7191bf3 | ||
|
|
628c9be0c8 | ||
|
|
733052720c | ||
|
|
a10f007194 | ||
|
|
6fa50c58d3 | ||
|
|
c54a58d6e4 | ||
|
|
34cb1ea3fd | ||
|
|
f640b0ca91 | ||
|
|
60e16da42e | ||
|
|
0cdceb95d6 | ||
|
|
70bd22d925 | ||
|
|
82462dd647 | ||
|
|
c0466e943d | ||
|
|
b187b4695d | ||
|
|
b1956d2a37 | ||
|
|
590b622e5f | ||
|
|
3d8174396a | ||
|
|
b8dc6e9bd9 | ||
|
|
8ee99109dc | ||
|
|
902041d4ee | ||
|
|
cc34aef47e | ||
|
|
0afbbe7c7a | ||
|
|
8004553ba7 | ||
|
|
0023b2846a | ||
|
|
34775c1816 | ||
|
|
e0759e704b | ||
|
|
0aa225ca78 | ||
|
|
b43b4ee5c0 | ||
|
|
0cdb8cecbf | ||
|
|
fd6a306742 | ||
|
|
7f3b3d2277 | ||
|
|
8be5b977bf | ||
|
|
d7ddb15f9c | ||
|
|
9a6a1798d0 | ||
|
|
14196fd349 | ||
|
|
941b89a523 | ||
|
|
a5f9e5f8c0 | ||
|
|
80c3356c8f | ||
|
|
914136b750 | ||
|
|
f9a60795f5 | ||
|
|
19640927c7 | ||
|
|
22faac7e36 | ||
|
|
30d260ab32 | ||
|
|
115120d066 | ||
|
|
1327844736 | ||
|
|
29904f3cb7 | ||
|
|
50395594b7 | ||
|
|
9360af88b3 | ||
|
|
376370336c | ||
|
|
70df6e3302 | ||
|
|
0a1fc2dc12 | ||
|
|
9857f6e437 | ||
|
|
56d6ebe916 | ||
|
|
81134ea2d4 | ||
|
|
a9f3e7fc54 | ||
|
|
eb84e2f8c9 | ||
|
|
61cfa0e86d | ||
|
|
0a01b8ade9 | ||
|
|
1457efa9a4 | ||
|
|
fa5c7add7a | ||
|
|
d644eba4d1 | ||
|
|
9c422c1a8f | ||
|
|
b6db37202f | ||
|
|
4ca3891089 | ||
|
|
4c7ed01776 | ||
|
|
45c922c377 | ||
|
|
f854c258bd | ||
|
|
a643fac073 | ||
|
|
861f105bea | ||
|
|
bf10ce9f1e | ||
|
|
ccf521d0a8 | ||
|
|
6075d98eaa | ||
|
|
a3801fc243 | ||
|
|
a1f0c05f3a | ||
|
|
a568c96929 | ||
|
|
d58bcf3c0e | ||
|
|
985f2e6436 | ||
|
|
ad441fa793 | ||
|
|
316300cc86 | ||
|
|
5c4f37b234 | ||
|
|
77b51a072d | ||
|
|
2536e1ae6a | ||
|
|
14822c9599 | ||
|
|
e53c37adc9 | ||
|
|
c37539354c | ||
|
|
ae0277f33c | ||
|
|
b863896249 | ||
|
|
5b42f8b743 | ||
|
|
3883fab614 | ||
|
|
61d6bcec4b | ||
|
|
3b5902b033 | ||
|
|
3a88c21a3b | ||
|
|
91a5055dee | ||
|
|
7befd1469f | ||
|
|
c72ebe495c | ||
|
|
19e06b97e6 | ||
|
|
7519825303 | ||
|
|
d9315bf309 | ||
|
|
8c36c809a0 | ||
|
|
8138aa3cb2 | ||
|
|
87aef3ca78 | ||
|
|
a3f1d26d6b | ||
|
|
06cebc5670 | ||
|
|
867fd62d77 | ||
|
|
650cdf2916 | ||
|
|
ebf461f2fd | ||
|
|
27fa319b2a | ||
|
|
d95ac894f4 | ||
|
|
ae84a8dd11 | ||
|
|
2fc963f986 | ||
|
|
be1f938ebd | ||
|
|
cccf4d503d | ||
|
|
9dad2a8ac6 | ||
|
|
75af104f07 | ||
|
|
76ecba245b | ||
|
|
3697c2ced8 | ||
|
|
b9d1d84716 | ||
|
|
64b2d547ce | ||
|
|
d8d2ff7e4e | ||
|
|
8aa5dc6482 | ||
|
|
474ba20e61 | ||
|
|
bdea2d02a9 | ||
|
|
c4307481f1 | ||
|
|
b8ac1b28bd | ||
|
|
24038cda95 | ||
|
|
86c82e9608 | ||
|
|
daab5d150b | ||
|
|
9ff82bdb90 | ||
|
|
c6d70ef1cf | ||
|
|
15d4bb3c76 | ||
|
|
3e698981fd | ||
|
|
9d45c934a5 | ||
|
|
c2bf9cf93e | ||
|
|
b3c6fd7f26 | ||
|
|
ccd155de71 | ||
|
|
1f90d2e46b | ||
|
|
4c5d974c22 | ||
|
|
392eda1cbc | ||
|
|
a9da3279e8 | ||
|
|
1ce8351180 | ||
|
|
96c334478a | ||
|
|
f1b0875b05 | ||
|
|
cea9e11c83 | ||
|
|
f098b39200 | ||
|
|
012d948b59 | ||
|
|
3334cd0a71 | ||
|
|
d63d53fd88 | ||
|
|
a7fa39b2fd | ||
|
|
40bb42e193 | ||
|
|
9c382c639b | ||
|
|
a43cde38f1 | ||
|
|
c35d2e08cd | ||
|
|
3377c383c1 | ||
|
|
c00e6d95cd | ||
|
|
725fccf4ed | ||
|
|
13129bd219 | ||
|
|
4561977bcf | ||
|
|
40be8a91f5 | ||
|
|
2a04d5830b | ||
|
|
82a38574f3 | ||
|
|
fea3a33c2b | ||
|
|
9a502cdf6f | ||
|
|
4b616299cf | ||
|
|
102243e064 | ||
|
|
4b21ac5ebe | ||
|
|
4dd7363dd3 | ||
|
|
3d5e5ab78f | ||
|
|
73045a1b21 | ||
|
|
871173a7cf | ||
|
|
0002313093 | ||
|
|
948cf5cca6 | ||
|
|
d40230879c | ||
|
|
ab22b775f1 | ||
|
|
42c85224ba | ||
|
|
e57444a353 | ||
|
|
3c6503d495 | ||
|
|
149b518f48 | ||
|
|
74621447ff | ||
|
|
3280952931 | ||
|
|
9e670e2736 | ||
|
|
9fc6347a2f | ||
|
|
ec7a15a192 | ||
|
|
7f99982810 | ||
|
|
935d83aaf8 | ||
|
|
0ff6edd546 | ||
|
|
94f629585a | ||
|
|
89c04be02f | ||
|
|
3151965ea8 | ||
|
|
bdf5159be1 | ||
|
|
0499ebbea3 | ||
|
|
d5843b7236 | ||
|
|
1c9c574a90 | ||
|
|
39acf20e48 | ||
|
|
52eb6ed5ab | ||
|
|
ee78d2d59d | ||
|
|
60dc5c4a38 | ||
|
|
50a0dc0355 | ||
|
|
3f681ec914 | ||
|
|
0bf499f191 | ||
|
|
389695a0d6 | ||
|
|
07f1afb312 | ||
|
|
ae91e61304 | ||
|
|
6248991b01 | ||
|
|
7f2d57ef62 | ||
|
|
31f8f884f1 | ||
|
|
4f4af5985a | ||
|
|
a716fdf6d4 | ||
|
|
9717f64abd | ||
|
|
adf239183a | ||
|
|
6cf209c79c | ||
|
|
decc5fb3c0 | ||
|
|
1e0820d613 | ||
|
|
70124d5177 | ||
|
|
269de65201 | ||
|
|
1d11abbfb6 | ||
|
|
700f308d6e | ||
|
|
21b6928ca6 | ||
|
|
998c67a649 | ||
|
|
fb99e878b0 | ||
|
|
1619adfc27 | ||
|
|
5510fb473f | ||
|
|
be1878cb2b | ||
|
|
15ab121cbd | ||
|
|
aa79b0e861 | ||
|
|
b80e550bcd | ||
|
|
dbc40b5814 | ||
|
|
0d5696a644 | ||
|
|
ceffa05802 | ||
|
|
d5668920b6 | ||
|
|
516f2da144 | ||
|
|
33c94e1888 | ||
|
|
51ab58cd91 | ||
|
|
aa7798d1d1 | ||
|
|
9067a1fc92 | ||
|
|
4024b6c564 | ||
|
|
d39730928b | ||
|
|
e1f049229c | ||
|
|
8f2676ec19 | ||
|
|
32d26248dc | ||
|
|
16f926401b | ||
|
|
66d60d3599 | ||
|
|
5a35ab6c34 | ||
|
|
ba1542bd31 | ||
|
|
453060945a | ||
|
|
c8351be461 | ||
|
|
9954da22a6 | ||
|
|
907b5611eb | ||
|
|
5f075de212 | ||
|
|
8fcf3c5079 | ||
|
|
07cee90c7a | ||
|
|
75ad495b98 | ||
|
|
0bb7288ad2 | ||
|
|
ad72415532 | ||
|
|
0ad0353fc0 | ||
|
|
9fa0dcd7aa | ||
|
|
1f2e80cd39 | ||
|
|
6cb6034d43 | ||
|
|
25134c6ac6 | ||
|
|
92bf42878a | ||
|
|
9f4582d158 | ||
|
|
68af73970e | ||
|
|
b6ed8d4975 | ||
|
|
d07d3645ce | ||
|
|
123759ab17 | ||
|
|
f2f1f893d8 | ||
|
|
db93a8eed2 | ||
|
|
12ab6d4a7d | ||
|
|
add759e889 | ||
|
|
f315f7977d | ||
|
|
f2f6701ebd | ||
|
|
1a92794d33 | ||
|
|
7640deb798 | ||
|
|
f1e8ef1cf6 | ||
|
|
5e5ac0162e | ||
|
|
0c013820f0 | ||
|
|
4b3a9e5847 | ||
|
|
e4982256a4 | ||
|
|
babc4927a8 | ||
|
|
6dd84cf469 | ||
|
|
a8800e3899 | ||
|
|
5f03496046 | ||
|
|
41500c17a2 | ||
|
|
2dcfde8b9a | ||
|
|
5c3305d8fa | ||
|
|
0d1fe99f53 | ||
|
|
4c03ffeec7 | ||
|
|
8101d17482 | ||
|
|
bc7b4dcc2a | ||
|
|
3db8b9078d | ||
|
|
943dbbefd3 | ||
|
|
480abcb853 | ||
|
|
60aaaff58e | ||
|
|
e3b889bbe8 | ||
|
|
ac5506a43b | ||
|
|
b29f533a3b | ||
|
|
a8ee86b09e | ||
|
|
0238c53302 | ||
|
|
665e3c806f | ||
|
|
8c96838441 | ||
|
|
4a722daec6 | ||
|
|
4e0cdbcb91 | ||
|
|
08976624cd | ||
|
|
fdeba94653 | ||
|
|
d3b100b7e5 | ||
|
|
1de3e18b08 | ||
|
|
d5c3c95682 | ||
|
|
dabe1e29ed | ||
|
|
203d1c0cfc | ||
|
|
7edd8601be | ||
|
|
a4423247f4 | ||
|
|
4834b203a0 | ||
|
|
bbabb32d13 | ||
|
|
95112d6bdf | ||
|
|
36cdca5a3e | ||
|
|
6980a9f3fc | ||
|
|
7b09479cd2 | ||
|
|
5825fd6f36 | ||
|
|
2d5b45dd82 | ||
|
|
52dda1d1fe | ||
|
|
420624bee4 | ||
|
|
8abde7b7d0 | ||
|
|
9e5b1ba28e | ||
|
|
b9c7d3c18e | ||
|
|
10aeccbbe5 | ||
|
|
15d351ebc2 | ||
|
|
7194f31cb6 | ||
|
|
84b7e82446 | ||
|
|
8264423b1a | ||
|
|
37f897f3bf | ||
|
|
fe3efac145 | ||
|
|
9773aebefc | ||
|
|
06f2b8c371 | ||
|
|
e8f0bb8350 | ||
|
|
9bfa6b827b | ||
|
|
b21bc17a58 | ||
|
|
f4d5d417d0 | ||
|
|
91fc83621e | ||
|
|
461feca0ca | ||
|
|
5e9afab3f7 | ||
|
|
2599ca6450 | ||
|
|
fc99ad3a39 | ||
|
|
10e1c3e72c | ||
|
|
af5dedd4d4 | ||
|
|
3b986c1076 | ||
|
|
72f77e8b7c | ||
|
|
e893bf676f | ||
|
|
80eb34f611 | ||
|
|
5d01947552 | ||
|
|
d3a025ef7b | ||
|
|
c466df841e | ||
|
|
b3c6e2a0f3 | ||
|
|
076c9cfed7 | ||
|
|
c3f3d12f83 | ||
|
|
44974034ec | ||
|
|
d6175acd38 | ||
|
|
62eee5f05c | ||
|
|
d4e5201913 | ||
|
|
f4d584765a | ||
|
|
26e224f852 | ||
|
|
252358ed66 | ||
|
|
475afeb7c8 | ||
|
|
7cbbb846eb | ||
|
|
25f947968c | ||
|
|
cad824dcbc | ||
|
|
e506f50b00 | ||
|
|
96ec149a98 | ||
|
|
8c913512f6 | ||
|
|
4cc307299d | ||
|
|
407c6b4c5f | ||
|
|
8f87070434 | ||
|
|
4a63996ee2 | ||
|
|
0358fe7620 | ||
|
|
55e64395ed | ||
|
|
ff5fb18e14 | ||
|
|
52dd960857 | ||
|
|
430221c2de | ||
|
|
217bdf8f92 | ||
|
|
38c6c869bf | ||
|
|
84d46da67e | ||
|
|
eb9d6240d7 | ||
|
|
2d44a871b0 | ||
|
|
3f89f350ff | ||
|
|
1a8407a782 | ||
|
|
cf288a3f73 | ||
|
|
f1f37fb180 | ||
|
|
fb0dd079fd | ||
|
|
a6c584c85c | ||
|
|
77adf35a30 | ||
|
|
dc6951c2a9 | ||
|
|
d14ba3f0f7 | ||
|
|
78ddf36e35 | ||
|
|
d42734624d | ||
|
|
b5dbd9d59b | ||
|
|
bed3e1289b | ||
|
|
b11ca4e60e | ||
|
|
4fcf3aa2bd | ||
|
|
dc39da8ca5 | ||
|
|
c10c87d28e | ||
|
|
c6fe6f1cc5 | ||
|
|
1c2bbeb26d | ||
|
|
17ed3692d0 | ||
|
|
966a00f41e | ||
|
|
fd8d8f89aa | ||
|
|
305bb74072 | ||
|
|
7f4dcdd134 | ||
|
|
aac37dcce1 | ||
|
|
f539c662a5 | ||
|
|
c82f346dd0 | ||
|
|
21b4a87837 | ||
|
|
ae73bcf24b | ||
|
|
2a3b56bde1 | ||
|
|
b8ebededd8 | ||
|
|
227c4c422c | ||
|
|
652bfb93cc | ||
|
|
c2278e3536 | ||
|
|
caa2fca4e8 | ||
|
|
745cb0175c | ||
|
|
e5165a780f | ||
|
|
b4b91af02b | ||
|
|
5649ff9c2e | ||
|
|
5b4bf6c62a | ||
|
|
93cb662282 | ||
|
|
00a8715e58 | ||
|
|
7ecd479b3e | ||
|
|
8fe7d3aaec | ||
|
|
f32a693393 | ||
|
|
17ebc01597 | ||
|
|
827fb698e1 | ||
|
|
32bdf10fd2 | ||
|
|
b795e6c3d2 | ||
|
|
42ba524e4e | ||
|
|
317c6d96e3 | ||
|
|
3692d1499f | ||
|
|
b21fbad8a3 | ||
|
|
743334a68a | ||
|
|
951413eb38 | ||
|
|
32dcdef853 | ||
|
|
34c9254d4a | ||
|
|
14012a4668 | ||
|
|
575debca63 | ||
|
|
763cac8532 | ||
|
|
43faacd7a7 | ||
|
|
1d4e307e96 | ||
|
|
7f8933b0de | ||
|
|
81608ff025 | ||
|
|
db63675b8e | ||
|
|
f74a83bc46 | ||
|
|
bc1deba3e4 | ||
|
|
d6113a8f0a | ||
|
|
2062cd48ea | ||
|
|
1c965ef515 | ||
|
|
58291b7156 | ||
|
|
afd1648d80 | ||
|
|
21814ffa9a | ||
|
|
9d3522da54 | ||
|
|
e07a76755e | ||
|
|
ba46bcdeae | ||
|
|
8d7e44314c | ||
|
|
35a67498c7 | ||
|
|
90dd934f95 | ||
|
|
4087045542 | ||
|
|
d11cef5907 | ||
|
|
76c91d226c | ||
|
|
c2b4dd2afd | ||
|
|
25b39cb39a | ||
|
|
35dcb7b88b | ||
|
|
e5f7e7c26e | ||
|
|
c5c11fd6a6 | ||
|
|
8134083419 | ||
|
|
a87e624198 | ||
|
|
e4c62d20b4 | ||
|
|
fa195d9e55 | ||
|
|
5ef5773d23 | ||
|
|
6eea52afdf | ||
|
|
80e64af30f | ||
|
|
563b6ddc36 | ||
|
|
c051ab9dc4 | ||
|
|
87737a8bdb | ||
|
|
94273d80b0 | ||
|
|
a08ec2a4bd | ||
|
|
d246c556f4 | ||
|
|
65aa365e38 | ||
|
|
eeeae449b4 | ||
|
|
17c10a7ba2 | ||
|
|
69f4383678 | ||
|
|
07852a7295 | ||
|
|
20b7e9b6b5 | ||
|
|
75f43ccea4 | ||
|
|
59e5785e93 | ||
|
|
b38f52dba9 | ||
|
|
2a6b17a48e | ||
|
|
a6c056a894 | ||
|
|
5c3442a71f | ||
|
|
390253242f | ||
|
|
9ab80fe1ac | ||
|
|
91fdd09e7a | ||
|
|
db5bd5c8a4 | ||
|
|
ef94c2fe7c | ||
|
|
72a25ed8e1 | ||
|
|
eb065e218f | ||
|
|
33426736fc | ||
|
|
896658d5ce | ||
|
|
b14135ed72 | ||
|
|
a1baf2e32d | ||
|
|
f9aa2d3bce | ||
|
|
c95d0e0696 | ||
|
|
ad4b84d446 | ||
|
|
3e27d5fcb0 | ||
|
|
48a100f49a | ||
|
|
698649f981 | ||
|
|
780078c3aa | ||
|
|
4c25e4ddee | ||
|
|
c0a5ac2ac5 | ||
|
|
0435409870 | ||
|
|
c521269409 | ||
|
|
1e252b7e4c | ||
|
|
d72b1edc48 | ||
|
|
f7307e8e01 | ||
|
|
127905f04b | ||
|
|
261c6dabd5 | ||
|
|
cae84bbf02 | ||
|
|
cdb2bc52fa | ||
|
|
cd2972eee0 | ||
|
|
4036aa8d0e | ||
|
|
52c6927c44 | ||
|
|
a16e0a21a2 | ||
|
|
e796b21157 | ||
|
|
1c6bc478b4 | ||
|
|
98f39c6388 | ||
|
|
570c83571b | ||
|
|
c0c38d89e0 | ||
|
|
b866cfc03c | ||
|
|
28c2755b37 | ||
|
|
57bfc5c73a | ||
|
|
0f3f7d53a3 | ||
|
|
529e50fd7f | ||
|
|
2fa283f91d | ||
|
|
029a9ade93 | ||
|
|
f1ca8b15c8 | ||
|
|
4d8edd5da9 | ||
|
|
6c63990653 | ||
|
|
5b521409c6 | ||
|
|
3268fc1014 | ||
|
|
19afb4941b | ||
|
|
40e5111d41 | ||
|
|
a3a40e1e74 | ||
|
|
101caa6826 | ||
|
|
875fed8d77 | ||
|
|
69e28eb000 | ||
|
|
e5d3a8360c | ||
|
|
4545d9285b | ||
|
|
6702024805 | ||
|
|
78bad4842b | ||
|
|
b9a913cfed | ||
|
|
6f5a6f353f | ||
|
|
790c4f589d | ||
|
|
cd1bd3461f | ||
|
|
0280dcd6a8 | ||
|
|
fc337292bc | ||
|
|
fb1daa0e21 | ||
|
|
579b9dc0c2 | ||
|
|
dedd0be352 | ||
|
|
1c7d9c3513 | ||
|
|
0c7dfe2af4 | ||
|
|
8d1351a8a3 | ||
|
|
e6e68a6036 | ||
|
|
24658edc45 | ||
|
|
09eaa3116a | ||
|
|
e9bff466b5 | ||
|
|
5d77f50160 | ||
|
|
2ab91e363f | ||
|
|
34d881426f | ||
|
|
13ecaa0ad4 | ||
|
|
ce6185b1f7 | ||
|
|
2cfde6b75a | ||
|
|
37d0354751 | ||
|
|
0a0edcf203 | ||
|
|
d6aad2ea28 | ||
|
|
63084506ee | ||
|
|
c5d313574f | ||
|
|
caab998212 | ||
|
|
aa037cc3d9 | ||
|
|
642bffe374 | ||
|
|
d682b154fc | ||
|
|
d4a06d98cf | ||
|
|
856b5e16b1 | ||
|
|
a0aa208860 | ||
|
|
037a11e04f | ||
|
|
bd8a1d715f | ||
|
|
54ab1dc091 | ||
|
|
9471e63857 | ||
|
|
fa4a403f38 | ||
|
|
d608d65bf4 | ||
|
|
c0f2df172a | ||
|
|
788ef5d81c | ||
|
|
1c6b5cffe1 | ||
|
|
c04382b623 | ||
|
|
0bbe51f8fd | ||
|
|
ff7d7d15a0 | ||
|
|
4b3d083d3a | ||
|
|
a566dd390b | ||
|
|
7d1442da04 | ||
|
|
17fc982f55 | ||
|
|
ba417e2274 | ||
|
|
d345094b75 | ||
|
|
6da477480d | ||
|
|
e274088c06 | ||
|
|
1bcaa73c5c | ||
|
|
ca94e8f621 | ||
|
|
1c4e198f59 | ||
|
|
fdd13f9c66 | ||
|
|
4333ab624e | ||
|
|
9fe1eb3a42 | ||
|
|
ad251a7682 | ||
|
|
1fa740de2d | ||
|
|
466b89064a | ||
|
|
2748cb0ba3 | ||
|
|
aef0d5bdde | ||
|
|
c71e8f024a | ||
|
|
9411f07321 | ||
|
|
9b2a5c9bbf | ||
|
|
2b275523a0 | ||
|
|
31fe2f6da4 | ||
|
|
f95db623a5 | ||
|
|
a46313e483 | ||
|
|
31c330826e | ||
|
|
c4cf800142 | ||
|
|
b64a2b0006 | ||
|
|
a3702f2270 | ||
|
|
d221b1d470 | ||
|
|
0b22a6bc1d | ||
|
|
07e8acd003 | ||
|
|
9fce617c57 | ||
|
|
8d5c736975 | ||
|
|
4ccec05186 | ||
|
|
a4f456f002 | ||
|
|
fbdb941c27 | ||
|
|
a41cd42e8d | ||
|
|
77521e4627 | ||
|
|
b6a1242bac | ||
|
|
2f325cfe26 | ||
|
|
193b0ad0f0 | ||
|
|
ed476b7793 | ||
|
|
720fd94b7f | ||
|
|
ff87da105c | ||
|
|
a875e65536 | ||
|
|
0b2c6bb662 | ||
|
|
e44e2fbbb7 | ||
|
|
b3c93644fd | ||
|
|
a56b7ff636 | ||
|
|
c724236930 | ||
|
|
4853320b2b | ||
|
|
ba1acb6ac1 | ||
|
|
f32a6320fc | ||
|
|
9f914ce36a | ||
|
|
b037644e5a | ||
|
|
afd8c59f83 | ||
|
|
8aa4af3e91 | ||
|
|
630a8a2b97 | ||
|
|
dc34c4d00c | ||
|
|
fb42729dec | ||
|
|
b06989216a | ||
|
|
e5144f08cd | ||
|
|
c4a60190e8 | ||
|
|
efe9e4fa4c | ||
|
|
45800b1559 | ||
|
|
b0b2b8104f | ||
|
|
8dbc012825 | ||
|
|
a434176063 | ||
|
|
a013f750c7 | ||
|
|
aa1f49d02f | ||
|
|
7125a26309 | ||
|
|
329a35ebf0 | ||
|
|
d30043f595 | ||
|
|
745dfa1911 | ||
|
|
76203f49a7 | ||
|
|
870a915377 | ||
|
|
c174fce227 | ||
|
|
2b6e42e919 | ||
|
|
df73e1e5a3 | ||
|
|
3e902311d4 | ||
|
|
64a0037265 | ||
|
|
bcd4e38093 | ||
|
|
181a77d627 | ||
|
|
b353595ba9 | ||
|
|
75e3bb4f17 | ||
|
|
d2fa9192d4 | ||
|
|
4bcadc2de4 | ||
|
|
8ddff74260 | ||
|
|
95940fdb64 | ||
|
|
9cd5708948 | ||
|
|
d361683d79 | ||
|
|
9ad17a01f7 | ||
|
|
22ca1d443c | ||
|
|
2662e875ca | ||
|
|
8ae0d07ec1 | ||
|
|
76a9edb7f5 | ||
|
|
0ccb464e5b | ||
|
|
bef600efa2 | ||
|
|
58a182cd33 | ||
|
|
aa43334f41 | ||
|
|
a2a4c97f6c | ||
|
|
4217ba99fd | ||
|
|
589725f5cc | ||
|
|
3fea4602f8 | ||
|
|
8ea6aae875 | ||
|
|
2c70b2af68 | ||
|
|
54a2cbcb42 | ||
|
|
fdef821c60 | ||
|
|
dfa798a35d | ||
|
|
39b8eb6ff1 | ||
|
|
6cf71f67a9 | ||
|
|
f2e919725e | ||
|
|
869599126e | ||
|
|
3b1b200f6f | ||
|
|
93c646e3e4 | ||
|
|
3552f80a21 | ||
|
|
66d3a63998 | ||
|
|
6447825978 | ||
|
|
18b7df9fca | ||
|
|
c3781cab96 | ||
|
|
776098dba6 | ||
|
|
8d1b4f61e7 | ||
|
|
c13e2bdb96 | ||
|
|
4682254157 | ||
|
|
d7ca6b9213 | ||
|
|
4a76afbde8 | ||
|
|
a68349c23a | ||
|
|
920e005366 | ||
|
|
659f339020 | ||
|
|
3ee2d463af | ||
|
|
686ddb5460 | ||
|
|
e5d62488b7 | ||
|
|
eb93dd5005 | ||
|
|
6999d02d2d | ||
|
|
790e2b1427 | ||
|
|
a29c7cdfe4 | ||
|
|
6b7cd692a6 | ||
|
|
4d3925872a | ||
|
|
2bd0f6934a | ||
|
|
51783f17ed | ||
|
|
ce3aef3526 | ||
|
|
ee70afdfbb | ||
|
|
d96c4a56a2 | ||
|
|
9a39513dea | ||
|
|
8f22d63315 | ||
|
|
7f2a5bb95e | ||
|
|
0118dbd5fb | ||
|
|
09405de26c | ||
|
|
efa5ee0e57 | ||
|
|
80d558f37a | ||
|
|
901adc3fc7 | ||
|
|
01417be954 | ||
|
|
43b780cbe6 | ||
|
|
e83f36a12f | ||
|
|
77e3fc4ab0 | ||
|
|
eafd1adaba | ||
|
|
6b53abb7c9 | ||
|
|
f994c5d284 | ||
|
|
6fda220107 | ||
|
|
da290ed1c3 | ||
|
|
7e9cd80a1c | ||
|
|
379b7413d8 | ||
|
|
9181a4df16 | ||
|
|
df982afd51 | ||
|
|
5c2c3b4317 | ||
|
|
92d1309103 | ||
|
|
c43ee3c1d6 | ||
|
|
e0726e5283 | ||
|
|
5f3775584b | ||
|
|
77873d63c5 | ||
|
|
9e6b09765e | ||
|
|
1ad6ea4049 | ||
|
|
7c41da1cb9 | ||
|
|
adcf4bfc53 | ||
|
|
7a6321a9c1 | ||
|
|
d56b27a7b0 | ||
|
|
ed7657ab5f | ||
|
|
a414838416 | ||
|
|
93646577dc | ||
|
|
46db66038e | ||
|
|
efc4e9ce56 | ||
|
|
8d5eac7f80 | ||
|
|
7b94e49b81 | ||
|
|
c35fd4bdc8 | ||
|
|
98590e2d90 | ||
|
|
e6da0e5dd5 | ||
|
|
cb2baf747d | ||
|
|
a2f2eb03ce | ||
|
|
5c6acbb780 | ||
|
|
1be7031199 | ||
|
|
ed6399bde9 | ||
|
|
6709893781 | ||
|
|
686a426cda | ||
|
|
4f90bc7813 | ||
|
|
e307b289ae | ||
|
|
3baeff61a7 | ||
|
|
93ab9d12ee | ||
|
|
36e1317792 | ||
|
|
fa3e90a021 | ||
|
|
782a69cf13 | ||
|
|
d495f351c0 | ||
|
|
30bd3d2d52 | ||
|
|
ff5a21cca5 | ||
|
|
f8abb73c92 | ||
|
|
e97f323d9a | ||
|
|
3d27a4c05d | ||
|
|
9dbc13dbe4 | ||
|
|
c46a4c75b1 | ||
|
|
0bded73f16 | ||
|
|
1333733684 | ||
|
|
003be934de | ||
|
|
93ef20d358 | ||
|
|
94e1a6f0ba | ||
|
|
8661d09d57 | ||
|
|
0e5e21dc4e | ||
|
|
3b25c4987c | ||
|
|
2212eb17aa | ||
|
|
768bac1db8 | ||
|
|
3aef75085f | ||
|
|
ce8bef638a | ||
|
|
f0a0c90304 | ||
|
|
cd6c32b21d | ||
|
|
b31876d2d1 | ||
|
|
ebab8a190e | ||
|
|
1b7ce8e7a5 | ||
|
|
646bb6bd79 | ||
|
|
5a84b97ca9 | ||
|
|
6d41b5a4a1 | ||
|
|
a8bce36f3b | ||
|
|
ac2132f8ba | ||
|
|
cab4b57abe | ||
|
|
938fb30359 | ||
|
|
62346d7d9d | ||
|
|
cf1e5ca64b | ||
|
|
7d2d683d96 | ||
|
|
fe5042f1c3 | ||
|
|
a1dd76aee0 | ||
|
|
d1c91be167 | ||
|
|
9748d99f34 | ||
|
|
c90ffbeb62 | ||
|
|
eb7fafeabf | ||
|
|
3e50629462 | ||
|
|
65281a4554 | ||
|
|
454ec09d6a | ||
|
|
60e3c6858d | ||
|
|
f911f5b4fc | ||
|
|
ad1694d291 | ||
|
|
1130965f26 | ||
|
|
fe1f28998b | ||
|
|
45727fce05 | ||
|
|
d5c23e5add | ||
|
|
e3a8285f6c | ||
|
|
a791221cf6 | ||
|
|
b954d9b403 | ||
|
|
5e7e24a271 | ||
|
|
ffb1e598f6 | ||
|
|
bc2da8a645 | ||
|
|
6f2be3ed30 | ||
|
|
033a7bffb3 | ||
|
|
f2b2ea61a1 | ||
|
|
6f0783acc4 | ||
|
|
ce60aa3823 | ||
|
|
8075e70606 | ||
|
|
4402fc2d0a | ||
|
|
3e3ecda551 | ||
|
|
50beb8f346 | ||
|
|
8e033e3e06 | ||
|
|
dc029a318b | ||
|
|
8e91bc2c8e | ||
|
|
0ff5b4e90b | ||
|
|
20dec19bfe | ||
|
|
d261fbff26 | ||
|
|
6594b33bcc | ||
|
|
a1bb6cc1b1 | ||
|
|
7ce195b68e | ||
|
|
16d8d04aaa | ||
|
|
59565f7d90 | ||
|
|
43784a2495 | ||
|
|
3811d7469e | ||
|
|
c72b40a1e1 | ||
|
|
f00933969d | ||
|
|
759adc45e3 | ||
|
|
27ecf78372 | ||
|
|
c91b83a7ba | ||
|
|
39373ee63a | ||
|
|
2db64c69ae | ||
|
|
a699b71c02 | ||
|
|
6c07d22cda | ||
|
|
a2ee900ed5 | ||
|
|
e709f31b99 | ||
|
|
35afb12756 | ||
|
|
9bed9fe162 | ||
|
|
4ff5553804 | ||
|
|
32213be7a7 | ||
|
|
84894a73e1 | ||
|
|
b6ea185ce7 | ||
|
|
814ac0f731 | ||
|
|
a40bb29da3 | ||
|
|
e9b90079c0 | ||
|
|
dba383c27e | ||
|
|
42059b5817 | ||
|
|
f92a17c01b | ||
|
|
d6552ce333 | ||
|
|
0db89bde5a | ||
|
|
56a12185d4 | ||
|
|
c40170db5d | ||
|
|
1df3e9c414 | ||
|
|
b1570df8b9 | ||
|
|
023fd1ce36 | ||
|
|
a7fe74bc0c | ||
|
|
26c9abd9da | ||
|
|
a5e34645c5 | ||
|
|
b2831c0a19 | ||
|
|
648c1ea0f9 | ||
|
|
9cd927e06a | ||
|
|
4272413f55 | ||
|
|
e1711b7af6 | ||
|
|
f7d3f27d45 | ||
|
|
3a7a47f82d | ||
|
|
cc211706d5 | ||
|
|
22f74be4cd | ||
|
|
5a00d14f94 | ||
|
|
ecb4e7bf9f | ||
|
|
56e5b546e1 | ||
|
|
272f5a2f4f | ||
|
|
ddcbe78a01 | ||
|
|
00b6c964e2 | ||
|
|
d7d2b06ecc | ||
|
|
fafc59360d | ||
|
|
19e105785e | ||
|
|
b87ac09e43 | ||
|
|
af9092d7c7 | ||
|
|
24a1ffd652 | ||
|
|
662813cc58 | ||
|
|
d890b78290 | ||
|
|
58747d7d4a | ||
|
|
0773a4f39c | ||
|
|
66cc7f8a1f | ||
|
|
01ab40bf4a | ||
|
|
4c09147fd1 | ||
|
|
f9f426d788 | ||
|
|
ff8fa1bf31 | ||
|
|
59f99e4f6a | ||
|
|
7449ce9c3b | ||
|
|
f6bc8f0a1f | ||
|
|
4d10b8cdee | ||
|
|
5a61c5de09 | ||
|
|
f84d0db811 | ||
|
|
36ce3b08fe | ||
|
|
da8ea5b545 | ||
|
|
fad3dbf4cd | ||
|
|
034d12c347 | ||
|
|
c94dbf1d9a | ||
|
|
e516687a9e | ||
|
|
4a2f77b0a6 | ||
|
|
7b29ecba71 | ||
|
|
11241b8e07 | ||
|
|
52bbd1f20b | ||
|
|
4044750515 | ||
|
|
b670c546b9 | ||
|
|
f37bbf93cb | ||
|
|
87311ab41a | ||
|
|
ecb4d1845c | ||
|
|
35c232ab25 | ||
|
|
df0be2e251 | ||
|
|
871b3a102b | ||
|
|
02299e3892 | ||
|
|
6af4d6f5b8 | ||
|
|
4fb5700367 | ||
|
|
8579276381 | ||
|
|
7ba60b22c5 | ||
|
|
031932f41c | ||
|
|
079d0a89b1 | ||
|
|
c4fdce6d64 | ||
|
|
5604c2b29f | ||
|
|
74b5ab2b47 | ||
|
|
c29cbfe123 | ||
|
|
6fe5cb1ffd | ||
|
|
7edd5a7a8e | ||
|
|
c1edc1b99b | ||
|
|
4d1d890f72 | ||
|
|
fe0f82fa2b | ||
|
|
84083a65a8 | ||
|
|
fc91c6bc08 | ||
|
|
09120171ba | ||
|
|
a362f920dc | ||
|
|
9d7729f548 | ||
|
|
ed56e177cf | ||
|
|
9db28bd502 | ||
|
|
aded70eb2e | ||
|
|
dfbad85465 | ||
|
|
52076fe182 | ||
|
|
5575c3cb13 | ||
|
|
637d32efff | ||
|
|
fd54658e53 | ||
|
|
2f39a8d76e | ||
|
|
6a3e793500 | ||
|
|
3b3ffeda6b | ||
|
|
f7d92a3b11 | ||
|
|
d9d9ba8bf1 | ||
|
|
f5d9090183 | ||
|
|
705ecd1ef1 | ||
|
|
08b5266a86 | ||
|
|
ecc4846ba8 | ||
|
|
4aab705d11 | ||
|
|
4615a68bcc | ||
|
|
bf6934e8ac | ||
|
|
af8c304bd4 | ||
|
|
51dac5a5a8 | ||
|
|
56463d9e36 | ||
|
|
a6a339dc59 | ||
|
|
8423304ab5 | ||
|
|
bb7408dbe9 | ||
|
|
7eff4dcf02 | ||
|
|
d7ee3fec3d | ||
|
|
5e026a3e8d | ||
|
|
d5e117b89f | ||
|
|
c87a5501df | ||
|
|
7584ebba0b | ||
|
|
66075e3960 | ||
|
|
193ba781a0 | ||
|
|
3e5dd64acc | ||
|
|
d66ab7d389 | ||
|
|
d2e6b27ecd | ||
|
|
0588541357 | ||
|
|
096ea84af6 | ||
|
|
04d0cfd510 | ||
|
|
7653f969ec | ||
|
|
c4ab6a4a8d | ||
|
|
d1ecd1318f | ||
|
|
8d65b1427d | ||
|
|
e693a6057e | ||
|
|
d0aa490ac3 | ||
|
|
0b6cad7d4f | ||
|
|
14e6c6d9a6 | ||
|
|
b2061347a5 | ||
|
|
79979f0a3b | ||
|
|
48d70b2349 | ||
|
|
b1f6309662 | ||
|
|
1acde76292 | ||
|
|
3b4867d7ab | ||
|
|
5ae965f4d3 | ||
|
|
742427b77b | ||
|
|
3d70a101f1 | ||
|
|
99062a5ea3 | ||
|
|
056d87f7ae | ||
|
|
df5a58772c | ||
|
|
e45db05b8e | ||
|
|
d234f74703 | ||
|
|
3ae2a1be4a | ||
|
|
2b828abd90 | ||
|
|
4b598b1575 |
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
charset = utf-8
|
||||
|
||||
# 2 space indentation
|
||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.bat]
|
||||
charset = latin1
|
||||
|
||||
# Unfortunately, EditorConfig doesn't support space configuration inside import braces directly.
|
||||
# You'll need to rely on your linter/formatter like ESLint or Prettier for that.
|
||||
2
.env.framework
Normal file
2
.env.framework
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_BUILD_TYPE = Production
|
||||
VITE_BUILD_PLATFORM = Framework
|
||||
2
.env.shell
Normal file
2
.env.shell
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_BUILD_TYPE = Production
|
||||
VITE_BUILD_PLATFORM = Shell
|
||||
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Bug 反馈
|
||||
description: 报告可能的 NapCat 异常行为
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
欢迎来到 NapCat 的 Issue Tracker!请填写以下表格来提交 Bug。
|
||||
在提交新的 Bug 反馈前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||
* 不与现有的某一 issue 重复
|
||||
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 运行 QQNT 的系统版本
|
||||
placeholder: Windows 10 Pro Workstation 22H2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: qqnt-version
|
||||
attributes:
|
||||
label: QQNT 版本
|
||||
description: 可在 QQNT 的「关于」的设置页中找到
|
||||
placeholder: 9.9.7-21804
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: napcat-version
|
||||
attributes:
|
||||
label: NapCat 版本
|
||||
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
|
||||
placeholder: 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: onebot-client-version
|
||||
attributes:
|
||||
label: OneBot 客户端
|
||||
description: 连接至 NapCat 的客户端版本信息
|
||||
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: 发生了什么?
|
||||
description: 填写你认为的 NapCat 的不正常行为
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-reproduce
|
||||
attributes:
|
||||
label: 如何复现
|
||||
description: 填写应当如何操作才能触发这个不正常行为
|
||||
placeholder: |
|
||||
1. xxx
|
||||
2. xxx
|
||||
3. xxx
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-expected
|
||||
attributes:
|
||||
label: 期望的结果?
|
||||
description: 填写你认为 NapCat 应当执行的正常行为
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: napcat-log
|
||||
attributes:
|
||||
label: NapCat 运行日志
|
||||
description: 粘贴相关日志内容到此处
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: onebot-client-log
|
||||
attributes:
|
||||
label: OneBot 客户端运行日志
|
||||
description: 粘贴 OneBot 客户端的相关日志内容到此处
|
||||
render: shell
|
||||
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
47
.github/workflows/build.yml
vendored
Normal file
47
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: "Build Action"
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
Build-LiteLoader:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Build NapCat.Framework
|
||||
run: |
|
||||
npm i && cd napcat.webui && npm i && cd .. || exit 1
|
||||
npm run build:framework && npm run depend || exit 1
|
||||
rm package-lock.json
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: dist
|
||||
Build-Shell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Build NapCat.Shell
|
||||
run: |
|
||||
npm i && cd napcat.webui && npm i && cd .. || exit 1
|
||||
npm run build:shell && npm run depend || exit 1
|
||||
rm package-lock.json
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: dist
|
||||
152
.github/workflows/release.yml
vendored
Normal file
152
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
name: "Build Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract version from tag
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Check Version
|
||||
run: |
|
||||
ls
|
||||
node ./script/checkVersion.cjs
|
||||
sh ./checkVersion.sh
|
||||
Build-LiteLoader:
|
||||
needs: [check-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'NapNeko/NapCatQQ'
|
||||
submodules: true
|
||||
ref: main
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Build NuCat Framework
|
||||
run: |
|
||||
npm i
|
||||
cd napcat.webui
|
||||
npm i
|
||||
cd ..
|
||||
npm run build:framework
|
||||
cd dist
|
||||
npm i --omit=dev
|
||||
cd ..
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: dist
|
||||
Build-Shell:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-version]
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'NapNeko/NapCatQQ'
|
||||
submodules: true
|
||||
ref: main
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Build NuCat Shell
|
||||
run: |
|
||||
npm i
|
||||
cd napcat.webui
|
||||
npm i
|
||||
cd ..
|
||||
npm run build:shell
|
||||
cd dist
|
||||
npm i --omit=dev
|
||||
cd ..
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: dist
|
||||
|
||||
release-napcat:
|
||||
needs: [Build-LiteLoader,Build-Shell]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'NapNeko/NapCatQQ'
|
||||
submodules: true
|
||||
ref: main
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
|
||||
- name: Download All Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Compress subdirectories
|
||||
run: |
|
||||
cd ./NapCat.Shell/
|
||||
zip -q -r NapCat.Shell.zip *
|
||||
cd ..
|
||||
cd ./NapCat.Framework/
|
||||
zip -q -r NapCat.Framework.zip *
|
||||
cd ..
|
||||
rm ./NapCat.Shell.zip -rf
|
||||
rm ./NapCat.Framework.zip -rf
|
||||
mv ./NapCat.Shell/NapCat.Shell.zip ./
|
||||
mv ./NapCat.Framework/NapCat.Framework.zip ./
|
||||
|
||||
mkdir ./NapCat.Framework.Windows.Once
|
||||
unzip -q ./external/LiteLoaderWrapper.zip -d ./NapCat.Framework.Windows.Once
|
||||
cd ./NapCat.Framework.Windows.Once
|
||||
ls
|
||||
mkdir -p ./LL/plugins/NapCatQQ
|
||||
unzip -q ../NapCat.Framework.zip -d ./LL/plugins/NapCatQQ
|
||||
zip -q -r NapCat.Framework.Windows.Once.zip *
|
||||
cd ..
|
||||
mv ./NapCat.Framework.Windows.Once/NapCat.Framework.Windows.Once.zip ./
|
||||
- name: Extract version from tag
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
|
||||
- name: Clone Changes Log
|
||||
run: curl -o CHANGELOG.md https://fastly.jsdelivr.net/gh/NapNeko/NapCatQQ@main/docs/changelogs/CHANGELOG.v${{ env.VERSION }}.md
|
||||
|
||||
- name: Create Release Draft and Upload Artifacts
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: NapCat V${{ env.VERSION }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body_path: CHANGELOG.md
|
||||
files: |
|
||||
NapCat.Framework.zip
|
||||
NapCat.Shell.zip
|
||||
NapCat.Framework.Windows.Once.zip
|
||||
draft: true
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
/localdebug/
|
||||
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/*
|
||||
|
||||
# Build
|
||||
*.db
|
||||
checkVersion.sh
|
||||
bun.lockb
|
||||
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"printWidth": 120,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
Limited Redistribution License for NapCat
|
||||
|
||||
Copyright © 2024 Mlikiowa
|
||||
|
||||
1. Usage and Reproduction:
|
||||
- Unauthorized use, reproduction, modification, or distribution of this code is prohibited without explicit permission from the main author of the NapCat repository.
|
||||
|
||||
2. Redistribution:
|
||||
- Redistribution of this code is permitted, provided that the full text of this license is included, and the source and copyright information is clearly stated.
|
||||
- Minor modifications and extensions are allowed for redistribution purposes, but the modified code must not be publicly released.
|
||||
|
||||
3. Non-Commercial Use:
|
||||
- This code is not to be used for any commercial purposes.
|
||||
|
||||
4. Additional Permissions:
|
||||
- Any rights not explicitly addressed in this license must be requested from and granted by the main author of the NapCat repository.
|
||||
|
||||
5. Disclaimer:
|
||||
- This code is provided "as is," without any express or implied warranties, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. In no event shall the author be liable for any damages or other liability arising from, out of, or in connection with the use or distribution of this code.
|
||||
59
README.md
59
README.md
@@ -1,14 +1,53 @@
|
||||
# NapCatQQ
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## 介绍
|
||||
无
|
||||
---
|
||||
## 欢迎回家
|
||||
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
## 下载与安装
|
||||
前往release获取
|
||||
## 特性介绍
|
||||
- [x] **安装简单**:就算是笨蛋也能使用
|
||||
- [x] **性能友好**:就算是低内存也能使用
|
||||
- [x] **接口丰富**:就算是没有也能使用
|
||||
- [x] **稳定好用**:就算是被捉也能使用
|
||||
|
||||
## 使用与配置
|
||||
参考文档
|
||||
## 使用框架
|
||||
|
||||
## 开源与安全
|
||||
为了防止过于扩散与违规使用,未来 NapCat 发版都会不公布源码,在未来形势有所转变下可能会发布源码。
|
||||
代码将进行混淆与插桩,请不要违法使用与宣传本项目。
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||
|
||||
**首次使用**请务必查看如下文档看使用教程
|
||||
|
||||
### 文档地址
|
||||
|
||||
[Cloudflare.Worker](https://doc.napneko.icu/)
|
||||
|
||||
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
||||
|
||||
[Github.IO](https://napneko.github.io/)
|
||||
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
[Server.Other](https://napcat.cyou/)
|
||||
|
||||
|
||||
## 回家旅途
|
||||
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
|
||||
|
||||
## 感谢他们
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
||||
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
|
||||
|
||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 特殊感谢
|
||||
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
|
||||
|
||||
## 开源附加
|
||||
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
||||
|
||||
70
eslint.config.mjs
Normal file
70
eslint.config.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import _import from "eslint-plugin-import";
|
||||
import { fixupPluginRules } from "@eslint/compat";
|
||||
import globals from "globals";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const filename = fileURLToPath(import.meta.url);
|
||||
const dirname = path.dirname(filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default [{
|
||||
ignores: ["src/core/proto/"],
|
||||
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
import: fixupPluginRules(_import),
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: ["error", 4],
|
||||
semi: ["error", "always"],
|
||||
"no-unused-vars": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
},
|
||||
}, {
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
},
|
||||
}];
|
||||
BIN
external/LiteLoaderWrapper.zip
vendored
Normal file
BIN
external/LiteLoaderWrapper.zip
vendored
Normal file
Binary file not shown.
BIN
launcher/NapCatWinBootHook.dll
Normal file
BIN
launcher/NapCatWinBootHook.dll
Normal file
Binary file not shown.
BIN
launcher/NapCatWinBootMain.exe
Normal file
BIN
launcher/NapCatWinBootMain.exe
Normal file
Binary file not shown.
32
launcher/launcher-user.bat
Normal file
32
launcher/launcher-user.bat
Normal file
@@ -0,0 +1,32 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
pause
|
||||
33
launcher/launcher-win10-user.bat
Normal file
33
launcher/launcher-win10-user.bat
Normal file
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
||||
|
||||
pause
|
||||
40
launcher/launcher-win10.bat
Normal file
40
launcher/launcher-win10.bat
Normal file
@@ -0,0 +1,40 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
||||
39
launcher/launcher.bat
Normal file
39
launcher/launcher.bat
Normal file
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
chcp 65001
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||
:loop_read
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
|
||||
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
5
launcher/loadNapCat.js
Normal file
5
launcher/loadNapCat.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const path = require('path');
|
||||
const CurrentPath = path.dirname(__filename);
|
||||
(async () => {
|
||||
await import("file://" + path.join(CurrentPath, './napcat/napcat.mjs'));
|
||||
})();
|
||||
26
launcher/qqnt.json
Normal file
26
launcher/qqnt.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"version": "9.9.16-29456",
|
||||
"verHash": "dd395162",
|
||||
"linuxVersion": "3.2.13-29456",
|
||||
"linuxVerHash": "e379390a",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
"productName": "QQ",
|
||||
"author": {
|
||||
"name": "Tencent",
|
||||
"email": "QQ-Team@tencent.com"
|
||||
},
|
||||
"homepage": "https://im.qq.com",
|
||||
"sideEffects": true,
|
||||
"bin": {
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "29456",
|
||||
"isPureShell": true,
|
||||
"isByteCodeShell": true,
|
||||
"platform": "win32",
|
||||
"eleArch": "x64"
|
||||
}
|
||||
4
launcher/quickLoginExample.bat
Normal file
4
launcher/quickLoginExample.bat
Normal file
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
REM ./launcher.bat 123456
|
||||
REM ./launcher-win10.bat 123456
|
||||
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可
|
||||
33
manifest.json
Normal file
33
manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"manifest_version": 4,
|
||||
"type": "extension",
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.2.3",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
"name": "MliKiowa",
|
||||
"link": "https://github.com/MliKiowa"
|
||||
},
|
||||
{
|
||||
"name": "Young",
|
||||
"link": "https://github.com/Wesley-Young"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"repo": "NapNeko/NapCatQQ",
|
||||
"branch": "main"
|
||||
},
|
||||
"platform": [
|
||||
"win32",
|
||||
"linux",
|
||||
"darwin"
|
||||
],
|
||||
"injects": {
|
||||
"renderer": "./renderer.js",
|
||||
"main": "./liteloader.cjs",
|
||||
"preload": "./preload.cjs"
|
||||
}
|
||||
}
|
||||
24
napcat.webui/.gitignore
vendored
Normal file
24
napcat.webui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
napcat.webui/.vscode/extensions.json
vendored
Normal file
3
napcat.webui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
5
napcat.webui/README.md
Normal file
5
napcat.webui/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
52
napcat.webui/eslint.config.mjs
Normal file
52
napcat.webui/eslint.config.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import vue from 'eslint-plugin-vue';
|
||||
import prettier from 'eslint-plugin-prettier/recommended';
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
...ts.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-var-requires': 'warn',
|
||||
},
|
||||
},
|
||||
...vue.configs['flat/base'],
|
||||
{
|
||||
files: ['*.vue', '**/*.vue'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
indent: ['error', 4],
|
||||
semi: ['error', 'always'],
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn',
|
||||
'@typescript-eslint/no-var-requires': 'warn',
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'vue/v-for-delimiter-style': ['error', 'in'],
|
||||
'vue/require-name-property': 'warn',
|
||||
'vue/prefer-true-attribute-shorthand': 'warn',
|
||||
'prefer-arrow-callback': 'warn',
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
{
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
},
|
||||
];
|
||||
13
napcat.webui/index.html
Normal file
13
napcat.webui/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NapCat WebUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
napcat.webui/package.json
Normal file
34
napcat.webui/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "napcat.webui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||
"webui:dev": "vite",
|
||||
"webui:build": "vite build",
|
||||
"webui:preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"tdesign-icons-vue-next": "^0.3.3",
|
||||
"tdesign-vue-next": "^1.10.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"globals": "^15.12.0",
|
||||
"terser": "^5.36.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^5.4.10",
|
||||
"vue-tsc": "^2.1.8"
|
||||
}
|
||||
}
|
||||
BIN
napcat.webui/public/logo.png
Normal file
BIN
napcat.webui/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
BIN
napcat.webui/public/logo_webui.png
Normal file
BIN
napcat.webui/public/logo_webui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
1
napcat.webui/public/vite.svg
Normal file
1
napcat.webui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
112
napcat.webui/src/App.vue
Normal file
112
napcat.webui/src/App.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div id="app" theme-mode="dark">
|
||||
<router-view />
|
||||
</div>
|
||||
<div v-if="show">
|
||||
<t-sticky-tool shape="round" placement="right-bottom" :offset="[-50, 10]" @click="changeTheme">
|
||||
<t-sticky-item label="浅色" popup="切换浅色模式">
|
||||
<template #icon><sunny-icon /></template>
|
||||
</t-sticky-item>
|
||||
<t-sticky-item label="深色" popup="切换深色模式">
|
||||
<template #icon><mode-dark-icon /></template>
|
||||
</t-sticky-item>
|
||||
<t-sticky-item label="自动" popup="跟随系统">
|
||||
<template #icon><control-platform-icon /></template>
|
||||
</t-sticky-item>
|
||||
</t-sticky-tool>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
|
||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
||||
interface Item {
|
||||
label: string;
|
||||
popup: string;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
item: Item;
|
||||
}
|
||||
enum ThemeMode {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
Auto = 'auto',
|
||||
}
|
||||
const themeLabelMap: Record<string, ThemeMode> = {
|
||||
"浅色": ThemeMode.Light,
|
||||
"深色": ThemeMode.Dark,
|
||||
"自动": ThemeMode.Auto,
|
||||
};
|
||||
const show = ref<boolean>(true);
|
||||
const createSetThemeAttributeFunction = () => {
|
||||
let mediaQueryForAutoTheme: MediaQueryList | null = null;
|
||||
return (mode: ThemeMode | null) => {
|
||||
const element = document.documentElement;
|
||||
if (mode === ThemeMode.Dark) {
|
||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
||||
} else if (mode === ThemeMode.Light) {
|
||||
element.removeAttribute('theme-mode');
|
||||
} else if (mode === ThemeMode.Auto) {
|
||||
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
||||
} else {
|
||||
element.removeAttribute('theme-mode');
|
||||
}
|
||||
};
|
||||
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQueryForAutoTheme.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQueryForAutoTheme.dispatchEvent(event);
|
||||
onBeforeUnmount(() => {
|
||||
if (mediaQueryForAutoTheme) {
|
||||
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const setThemeAttribute = createSetThemeAttributeFunction();
|
||||
|
||||
const getStoredTheme = (): ThemeMode | null => {
|
||||
return localStorage.getItem('theme') as ThemeMode | null;
|
||||
};
|
||||
|
||||
const initTheme = () => {
|
||||
const storedTheme = getStoredTheme();
|
||||
if (storedTheme === null) {
|
||||
setThemeAttribute(ThemeMode.Auto);
|
||||
} else {
|
||||
setThemeAttribute(storedTheme);
|
||||
}
|
||||
};
|
||||
|
||||
const changeTheme = (context: Context) => {
|
||||
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
|
||||
console.log(themeLabel);
|
||||
setThemeAttribute(themeLabel);
|
||||
localStorage.setItem('theme', themeLabel);
|
||||
};
|
||||
const haddingFbars = () => {
|
||||
show.value = !smallScreen.matches;
|
||||
if (smallScreen.matches) {
|
||||
localStorage.setItem('theme', 'auto');
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
haddingFbars();
|
||||
window.addEventListener('resize', haddingFbars);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', haddingFbars);
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
||||
BIN
napcat.webui/src/assets/Sotheby.ttf
Normal file
BIN
napcat.webui/src/assets/Sotheby.ttf
Normal file
Binary file not shown.
BIN
napcat.webui/src/assets/logo.png
Normal file
BIN
napcat.webui/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 335 KiB |
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
1
napcat.webui/src/assets/vue.svg
Normal file
1
napcat.webui/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
205
napcat.webui/src/backend/shell.ts
Normal file
205
napcat.webui/src/backend/shell.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
|
||||
export class QQLoginManager {
|
||||
private retCredential: string;
|
||||
private readonly apiPrefix: string;
|
||||
|
||||
//调试时http://127.0.0.1:6099/api 打包时 ../api
|
||||
constructor(retCredential: string, apiPrefix: string = '../api') {
|
||||
this.retCredential = retCredential;
|
||||
this.apiPrefix = apiPrefix;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
public async GetOB11Config(): Promise<OneBotConfig> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data as OneBotConfig;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting OB11 config:', error);
|
||||
}
|
||||
return {} as OneBotConfig;
|
||||
}
|
||||
|
||||
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ config: JSON.stringify(config) }),
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting OB11 config:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async checkQQLoginStatus(): Promise<boolean> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return QQLoginResponseJson.data.isLogin;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking QQ login status:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return QQLoginResponseJson.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking QQ login status:', error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async checkWebUiLogined(): Promise<boolean> {
|
||||
try {
|
||||
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (LoginResponse.status == 200) {
|
||||
const LoginResponseJson = await LoginResponse.json();
|
||||
if (LoginResponseJson.code == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking web UI login status:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async loginWithToken(token: string): Promise<string | null> {
|
||||
try {
|
||||
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token: token }),
|
||||
});
|
||||
const loginResponseJson = await loginResponse.json();
|
||||
const retCode = loginResponseJson.code;
|
||||
if (retCode === 0) {
|
||||
this.retCredential = loginResponseJson.data.Credential;
|
||||
return this.retCredential;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error logging in with token:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getQQLoginQrcode(): Promise<string> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return QQLoginResponseJson.data.qrcode || '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting QQ login QR code:', error);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public async getQQQuickLoginList(): Promise<string[]> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return QQLoginResponseJson.data || [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting QQ quick login list:', error);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ uin: uin }),
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return { result: true, errMsg: '' };
|
||||
} else {
|
||||
return { result: false, errMsg: QQLoginResponseJson.message };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting quick login:', error);
|
||||
}
|
||||
return { result: false, errMsg: '接口异常' };
|
||||
}
|
||||
}
|
||||
58
napcat.webui/src/components/Dashboard.vue
Normal file
58
napcat.webui/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<t-layout class="dashboard-container">
|
||||
<div ref="menuRef">
|
||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
||||
</div>
|
||||
<t-layout>
|
||||
<router-view />
|
||||
</t-layout>
|
||||
</t-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import SidebarMenu from './webui/Nav.vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
interface MenuItem {
|
||||
value: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
route: string;
|
||||
}
|
||||
|
||||
const menuItems = ref<MenuItem[]>([
|
||||
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
|
||||
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
|
||||
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
|
||||
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
||||
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
|
||||
]);
|
||||
const menuRef = ref<HTMLDivElement | null>(null);
|
||||
emitter.on('sendMenu', (event) => {
|
||||
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
|
||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
||||
});
|
||||
onMounted(() => {
|
||||
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
185
napcat.webui/src/components/QQLogin.vue
Normal file
185
napcat.webui/src/components/QQLogin.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<t-card class="layout">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">QQ Login</h2>
|
||||
<div class="login-methods">
|
||||
<t-tooltip content="快速登录">
|
||||
<t-button
|
||||
id="quick-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'quick' }"
|
||||
@click="loginMethod = 'quick'"
|
||||
>Quick Login</t-button
|
||||
>
|
||||
</t-tooltip>
|
||||
<t-tooltip content="二维码登录">
|
||||
<t-button
|
||||
id="qrcode-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="loginMethod = 'qrcode'"
|
||||
>QR Code</t-button
|
||||
>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||
<t-select
|
||||
id="quick-login-select"
|
||||
v-model="selectedAccount"
|
||||
placeholder="Select Account"
|
||||
@change="selectAccount"
|
||||
>
|
||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
||||
</t-select>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
||||
<canvas ref="qrcodeCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const router = useRouter();
|
||||
const loginMethod = ref<'quick' | 'qrcode'>('quick');
|
||||
const quickLoginList = ref<string[]>([]);
|
||||
const selectedAccount = ref<string>('');
|
||||
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
||||
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
||||
let heartBeatTimer: number | null = null;
|
||||
let qrcodeUrl: string = '';
|
||||
const selectAccount = async (accountName: string): Promise<void> => {
|
||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||
if (result) {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
await MessagePlugin.success('登录成功即将跳转');
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else {
|
||||
await MessagePlugin.error('登录失败,' + errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
|
||||
if (!canvas) {
|
||||
console.error('Canvas element not found');
|
||||
return;
|
||||
}
|
||||
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
|
||||
if (error) {
|
||||
console.error('Error generating QR Code:', error);
|
||||
} else {
|
||||
console.log('QR Code generated!');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const HeartBeat = async (): Promise<void> => {
|
||||
const isLogined = await qqLoginManager.checkQQLoginStatusWithQrcode();
|
||||
if (isLogined?.isLogin) {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
// //判断是否已经调转
|
||||
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
|
||||
// return;
|
||||
// }
|
||||
await MessagePlugin.success('登录成功即将跳转');
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
|
||||
qrcodeUrl = isLogined.qrcodeurl;
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
}
|
||||
};
|
||||
|
||||
const InitPages = async (): Promise<void> => {
|
||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
InitPages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-container {
|
||||
width: 90%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.login-methods {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-method {
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.login-method.active {
|
||||
background-color: #e6f0ff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.login-form,
|
||||
.qrcode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sotheby-font {
|
||||
font-family: Sotheby, Helvetica, monospace;
|
||||
font-size: 3.125rem;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #888;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
153
napcat.webui/src/components/WebUiLogin.vue
Normal file
153
napcat.webui/src/components/WebUiLogin.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<t-card class="layout">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">WebUi Login</h2>
|
||||
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
||||
<t-form-item name="password">
|
||||
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
||||
<template #prefix-icon>
|
||||
<lock-on-icon />
|
||||
</template>
|
||||
</t-input>
|
||||
</t-form-item>
|
||||
<t-form-item>
|
||||
<t-button theme="primary" type="submit" block>登录</t-button>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import '../css/style.css';
|
||||
import '../css/font.css';
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { LockOnIcon } from 'tdesign-icons-vue-next';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
interface FormData {
|
||||
token: string;
|
||||
}
|
||||
|
||||
const formData: FormData = reactive({
|
||||
token: '',
|
||||
});
|
||||
|
||||
const handleLoginSuccess = async (credential: string) => {
|
||||
localStorage.setItem('auth', credential);
|
||||
await checkLoginStatus();
|
||||
};
|
||||
|
||||
const handleLoginFailure = (message: string) => {
|
||||
MessagePlugin.error(message);
|
||||
};
|
||||
|
||||
const checkLoginStatus = async () => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
return;
|
||||
}
|
||||
const loginManager = new QQLoginManager(storedCredential);
|
||||
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
|
||||
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
|
||||
if (!isWenUiLoggedIn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
|
||||
if (isQQLoggedIn) {
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else {
|
||||
await router.push({ path: '/qqlogin' });
|
||||
}
|
||||
};
|
||||
|
||||
const loginWithToken = async (token: string) => {
|
||||
const loginManager = new QQLoginManager('');
|
||||
const credential = await loginManager.loginWithToken(token);
|
||||
if (credential) {
|
||||
await handleLoginSuccess(credential);
|
||||
} else {
|
||||
handleLoginFailure('登录失败,请检查Token');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const token = url.searchParams.get('token');
|
||||
if (token) {
|
||||
loginWithToken(token);
|
||||
}
|
||||
checkLoginStatus();
|
||||
});
|
||||
|
||||
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
||||
if (validateResult) {
|
||||
await loginWithToken(formData.token);
|
||||
} else {
|
||||
handleLoginFailure('请填写Token');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-container {
|
||||
width: 90%;
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.tdesign-demo-block-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.tdesign-demo-block-column-large {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 32px;
|
||||
}
|
||||
|
||||
.tdesign-demo-block-row {
|
||||
display: flex;
|
||||
column-gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sotheby-font {
|
||||
font-family: Sotheby, Helvetica, monospace;
|
||||
font-size: 3.125rem;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #888;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
127
napcat.webui/src/components/webui/Nav.vue
Normal file
127
napcat.webui/src/components/webui/Nav.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
||||
<template #logo>
|
||||
<div class="logo">
|
||||
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
||||
<div class="logo-textBox">
|
||||
<div class="logo-text">{{ collapsed ? '' : 'NapCat' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||
<t-tooltip :disabled="!collapsed" :content="item.label" placement="right">
|
||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</t-menu-item>
|
||||
</t-tooltip>
|
||||
</router-link>
|
||||
<template #operations>
|
||||
<t-button
|
||||
:disabled="disBtn"
|
||||
class="t-demo-collapse-btn"
|
||||
variant="text"
|
||||
shape="square"
|
||||
@click="changeCollapsed"
|
||||
>
|
||||
<template #icon><t-icon :name="iconName" /></template>
|
||||
</t-button>
|
||||
</template>
|
||||
</t-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, onMounted, watch } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
|
||||
type MenuItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
menuItems: MenuItem[];
|
||||
}>();
|
||||
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
||||
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
||||
const disBtn = ref<boolean>(false);
|
||||
|
||||
const changeCollapsed = (): void => {
|
||||
collapsed.value = !collapsed.value;
|
||||
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||
};
|
||||
watch(collapsed, (newValue, oldValue) => {
|
||||
setTimeout(() => {
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
}, 300);
|
||||
});
|
||||
onMounted(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
disBtn.value = e.matches;
|
||||
if (e.matches) {
|
||||
collapsed.value = e.matches;
|
||||
}
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQuery.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQuery.dispatchEvent(event);
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-menu {
|
||||
width: 100px; /* 移动端侧边栏宽度 */
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.logo-img {
|
||||
object-fit: contain;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.logo-textBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.logo-text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 22px;
|
||||
font-family: Sotheby, Helvetica, monospace;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
6
napcat.webui/src/css/font.css
Normal file
6
napcat.webui/src/css/font.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Sotheby';
|
||||
src: url('../assets/Sotheby.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
84
napcat.webui/src/css/style.css
Normal file
84
napcat.webui/src/css/style.css
Normal file
@@ -0,0 +1,84 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
87
napcat.webui/src/main.ts
Normal file
87
napcat.webui/src/main.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import {
|
||||
Button as TButton,
|
||||
Input as TInput,
|
||||
Form as TForm,
|
||||
FormItem as TFormItem,
|
||||
Select as TSelect,
|
||||
Option as TOption,
|
||||
Menu as TMenu,
|
||||
MenuItem as TMenuItem,
|
||||
Icon as TIcon,
|
||||
Submenu as TSubmenu,
|
||||
Col as TCol,
|
||||
Row as TRow,
|
||||
Card as TCard,
|
||||
Divider as TDivider,
|
||||
Link as TLink,
|
||||
List as TList,
|
||||
Alert as TAlert,
|
||||
Tag as TTag,
|
||||
Descriptions as TDescriptionsProps,
|
||||
DescriptionsItem as TDescriptionsItem,
|
||||
Collapse as TCollapse,
|
||||
CollapsePanel as TCollapsePanel,
|
||||
ListItem as TListItem,
|
||||
Tabs as TTabs,
|
||||
TabPanel as TTabPanel,
|
||||
Space as TSpace,
|
||||
Checkbox as TCheckbox,
|
||||
Popup as TPopup,
|
||||
Dialog as TDialog,
|
||||
Switch as TSwitch,
|
||||
Tooltip as Tooltip,
|
||||
StickyTool as TStickyTool,
|
||||
StickyItem as TStickyItem,
|
||||
Layout as TLayout,
|
||||
Content as TContent,
|
||||
Footer as TFooter,
|
||||
Aside as TAside,
|
||||
Popconfirm as Tpopconfirm,
|
||||
Empty as TEmpty,
|
||||
} from 'tdesign-vue-next';
|
||||
import { router } from './router';
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(TButton);
|
||||
app.use(TInput);
|
||||
app.use(TForm);
|
||||
app.use(TFormItem);
|
||||
app.use(TSelect);
|
||||
app.use(TOption);
|
||||
app.use(TMenu);
|
||||
app.use(TMenuItem);
|
||||
app.use(TIcon);
|
||||
app.use(TSubmenu);
|
||||
app.use(TCol);
|
||||
app.use(TRow);
|
||||
app.use(TCard);
|
||||
app.use(TDivider);
|
||||
app.use(TLink);
|
||||
app.use(TList);
|
||||
app.use(TAlert);
|
||||
app.use(TTag);
|
||||
app.use(TDescriptionsProps);
|
||||
app.use(TDescriptionsItem);
|
||||
app.use(TCollapse);
|
||||
app.use(TCollapsePanel);
|
||||
app.use(TListItem);
|
||||
app.use(TTabs);
|
||||
app.use(TTabPanel);
|
||||
app.use(TSpace);
|
||||
app.use(TCheckbox);
|
||||
app.use(TPopup);
|
||||
app.use(TDialog);
|
||||
app.use(TSwitch);
|
||||
app.use(Tooltip);
|
||||
app.use(TStickyTool);
|
||||
app.use(TStickyItem);
|
||||
app.use(TLayout);
|
||||
app.use(TContent);
|
||||
app.use(TFooter);
|
||||
app.use(TAside);
|
||||
app.use(Tpopconfirm);
|
||||
app.use(TEmpty);
|
||||
app.mount('#app');
|
||||
66
napcat.webui/src/pages/AboutUs.vue
Normal file
66
napcat.webui/src/pages/AboutUs.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="about-us">
|
||||
<div>
|
||||
<t-divider content="面板关于信息" align="left" />
|
||||
<t-alert theme="success" message="NapCat.WebUi is running" />
|
||||
<t-list class="list">
|
||||
<t-list-item class="list-item">
|
||||
<span class="item-label">开发人员:</span>
|
||||
<span class="item-content">
|
||||
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item class="list-item">
|
||||
<span class="item-label">版本信息:</span>
|
||||
<span class="item-content">
|
||||
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
|
||||
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
|
||||
<t-tag class="tag-item" theme="success">
|
||||
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
|
||||
</t-tag>
|
||||
</span>
|
||||
</t-list-item>
|
||||
</t-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import pkg from '../../package.json';
|
||||
import { napCatVersion } from '../../../src/common/version';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about-us {
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
6
napcat.webui/src/pages/BasicInfo.vue
Normal file
6
napcat.webui/src/pages/BasicInfo.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="basic-info">
|
||||
<h1>面板基础信息</h1>
|
||||
<p>这里显示面板的基础信息。</p>
|
||||
</div>
|
||||
</template>
|
||||
6
napcat.webui/src/pages/Log.vue
Normal file
6
napcat.webui/src/pages/Log.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div class="log-view">
|
||||
<h1>面板日志信息</h1>
|
||||
<p>这里显示面板的日志信息。</p>
|
||||
</div>
|
||||
</template>
|
||||
540
napcat.webui/src/pages/NetWork.vue
Normal file
540
napcat.webui/src/pages/NetWork.vue
Normal file
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<div ref="headerBox" class="title">
|
||||
<t-divider content="网络配置" align="left" />
|
||||
<t-divider align="right">
|
||||
<t-button @click="addConfig()">
|
||||
<template #icon><add-icon /></template>
|
||||
添加配置</t-button>
|
||||
</t-divider>
|
||||
</div>
|
||||
<div v-if="loadPage" ref="setting" class="setting">
|
||||
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
|
||||
<t-tab-panel value="all" label="全部"></t-tab-panel>
|
||||
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
|
||||
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
||||
</t-tabs>
|
||||
</div>
|
||||
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
|
||||
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
|
||||
<div v-for="(item, index) in cardConfig" :key="index">
|
||||
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
|
||||
:header-bordered="true" class="setting-card">
|
||||
<template #actions>
|
||||
<t-space>
|
||||
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
|
||||
<t-popconfirm theme="danger" content="确认删除" @confirm="delConfig(item)">
|
||||
<delete-icon size="20px"></delete-icon>
|
||||
</t-popconfirm>
|
||||
</t-space>
|
||||
</template>
|
||||
<div class="setting-content">
|
||||
<t-card class="card-address" :style="{
|
||||
borderLeft: '7px solid ' + (item.enable ?
|
||||
'var(--td-success-color)' :
|
||||
'var(--td-error-color)')
|
||||
}">
|
||||
<div class="local-box" v-if="item.host&&item.port">
|
||||
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<server-filled-icon class="local-icon" size="20px"></server-filled-icon>
|
||||
<strong class="local" >{{ item.url }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||
</div>
|
||||
</t-card>
|
||||
<t-collapse :default-value="[0]" expand-mutex style="margin-top:10px;" class="info-coll">
|
||||
<t-collapse-panel header="基础信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions-item v-if="item.token" label="连接密钥">
|
||||
<div v-if="mediumScreen.matches||largeScreen.matches" class="token-view">
|
||||
<span>{{ showToken ? item.token : '******' }}</span>
|
||||
<browse-icon class="browse-icon" v-if="showToken" size="18px"
|
||||
@click="showToken = false"></browse-icon>
|
||||
<browse-off-icon class="browse-icon" v-else size="18px"
|
||||
@click="showToken = true"></browse-off-icon>
|
||||
</div>
|
||||
<div v-else>
|
||||
<t-popup :showArrow="true" trigger="click">
|
||||
<t-tag theme="primary">点击查看</t-tag>
|
||||
<template #content>
|
||||
<div @click="copyText(item.token)">{{item.token}}</div>
|
||||
</template>
|
||||
</t-popup>
|
||||
</div>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
<t-collapse-panel header="状态信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
||||
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'">
|
||||
{{ item.debug ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
|
||||
label="Websocket 功能">
|
||||
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
|
||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
|
||||
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
|
||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="上报自身消息">
|
||||
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
|
||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="强制推送事件">
|
||||
<t-tag class="tag-item"
|
||||
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
|
||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
</t-collapse>
|
||||
</div>
|
||||
</t-card>
|
||||
</div>
|
||||
<div style="height: 20vh"></div>
|
||||
</div>
|
||||
<t-card v-else>
|
||||
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
||||
</t-card>
|
||||
</div>
|
||||
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
|
||||
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
|
||||
<div slot="body" class="dialog-body" >
|
||||
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
|
||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
|
||||
label="名称" name="name">
|
||||
<t-input v-model="newTab.name" />
|
||||
</t-form-item>
|
||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
||||
label="类型" name="type">
|
||||
<t-select v-model="newTab.type" @change="onloadDefault">
|
||||
<t-option value="httpServers">HTTP 服务器</t-option>
|
||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
||||
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<div>
|
||||
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||
:config="newTab.data" />
|
||||
</div>
|
||||
</t-form>
|
||||
</div>
|
||||
</t-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
|
||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
import {
|
||||
mergeNetworkDefaultConfig,
|
||||
mergeOneBotConfigs,
|
||||
NetworkConfig,
|
||||
OneBotConfig,
|
||||
} from '../../../src/onebot/config/config';
|
||||
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
||||
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
|
||||
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
|
||||
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const showToken = ref<boolean>(false);
|
||||
const infoOneCol = ref<boolean>(true);
|
||||
const tabsWidth = ref<number>(0);
|
||||
const menuWidth = ref<number>(0);
|
||||
const cardWidth = ref<number>(0);
|
||||
const cardHeight = ref<number>(0);
|
||||
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
|
||||
const largeScreen = window.matchMedia('(min-width: 1025px)');
|
||||
const headerBox = ref<HTMLDivElement | null>(null);
|
||||
const setting = ref<HTMLDivElement | null>(null);
|
||||
const loadPage = ref<boolean>(false);
|
||||
const visibleBody = ref<boolean>(false);
|
||||
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
|
||||
const dialogTitle = ref<string>('');
|
||||
|
||||
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
|
||||
|
||||
const componentMap: Record<
|
||||
ComponentKey,
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent
|
||||
> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
};
|
||||
|
||||
//操作类型
|
||||
const operateType = ref<string>('');
|
||||
//配置项索引
|
||||
const configIndex = ref<number>(0);
|
||||
//保存时所用数据
|
||||
const networkConfig: NetworkConfig & { [key: string]: any; } = {
|
||||
websocketClients: [],
|
||||
websocketServers: [],
|
||||
httpClients: [],
|
||||
httpServers: [],
|
||||
};
|
||||
|
||||
//挂载的数据
|
||||
const WebConfg = ref(
|
||||
new Map<string, Array<null>>([
|
||||
['all', []],
|
||||
['httpServers', []],
|
||||
['httpClients', []],
|
||||
['websocketServers', []],
|
||||
['websocketClients', []],
|
||||
])
|
||||
);
|
||||
const typeCh: Record<ComponentKey, string> = {
|
||||
httpServers: 'HTTP 服务器',
|
||||
httpClients: 'HTTP 客户端',
|
||||
websocketServers: 'WebSocket 服务器',
|
||||
websocketClients: 'WebSocket 客户端',
|
||||
};
|
||||
const cardConfig = ref<any>([]);
|
||||
const getComponent = (type: ComponentKey) => {
|
||||
return componentMap[type];
|
||||
};
|
||||
const getKeyByValue = (obj: typeof typeCh, value: string): string | undefined => {
|
||||
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
|
||||
};
|
||||
|
||||
const addConfig = () => {
|
||||
dialogTitle.value = '添加配置';
|
||||
newTab.value = { name: '', data: {}, type: '' };
|
||||
operateType.value = 'add';
|
||||
visibleBody.value = true;
|
||||
};
|
||||
|
||||
const editConfig = (item: any) => {
|
||||
dialogTitle.value = '修改配置';
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: item, type: type };
|
||||
}
|
||||
operateType.value = 'edit';
|
||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||
visibleBody.value = true;
|
||||
};
|
||||
const delConfig = (item: any) => {
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: item, type: type };
|
||||
}
|
||||
configIndex.value = configIndex.value = networkConfig[newTab.value.type].findIndex(
|
||||
(obj: any) => obj.name === item.name
|
||||
);
|
||||
operateType.value = 'delete';
|
||||
saveConfig();
|
||||
};
|
||||
|
||||
const selectType = (key: ComponentKey) => {
|
||||
cardConfig.value = WebConfg.value.get(key);
|
||||
};
|
||||
|
||||
const onloadDefault = (key: ComponentKey) => {
|
||||
console.log(key);
|
||||
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
||||
};
|
||||
//检测重名
|
||||
const checkName = (name: string) => {
|
||||
const allConfigs = WebConfg.value.get('all')?.findIndex((obj: any) => obj.name === name);
|
||||
if (newTab.value.name === '' || newTab.value.type === '') {
|
||||
MessagePlugin.error('请填写完整信息');
|
||||
return false;
|
||||
} else if (allConfigs === -1 || newTab.value.data.name === name) {
|
||||
return true;
|
||||
} else {
|
||||
MessagePlugin.error('名称已存在');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
//保存
|
||||
const saveConfig = async () => {
|
||||
if (operateType.value == 'add') {
|
||||
if (!checkName(newTab.value.name)) return;
|
||||
newTab.value.data.name = newTab.value.name;
|
||||
networkConfig[newTab.value.type].push(newTab.value.data);
|
||||
} else if (operateType.value == 'edit') {
|
||||
if (!checkName(newTab.value.name)) return;
|
||||
newTab.value.data.name = newTab.value.name;
|
||||
networkConfig[newTab.value.type][configIndex.value] = newTab.value.data;
|
||||
} else if (operateType.value == 'delete') {
|
||||
networkConfig[newTab.value.type].splice(configIndex.value, 1);
|
||||
}
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
userConfig.network = networkConfig;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
operateType.value = '';
|
||||
configIndex.value = 0;
|
||||
MessagePlugin.success('配置保存成功');
|
||||
await loadConfig();
|
||||
visibleBody.value = false;
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
console.error('No stored credential found');
|
||||
return;
|
||||
}
|
||||
const loginManager = new QQLoginManager(storedCredential);
|
||||
return await loginManager.GetOB11Config();
|
||||
};
|
||||
|
||||
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
console.error('No stored credential found');
|
||||
return false;
|
||||
}
|
||||
const loginManager = new QQLoginManager(storedCredential);
|
||||
return await loginManager.SetOB11Config(config);
|
||||
};
|
||||
|
||||
//获取卡片数据
|
||||
const getAllData = (data: NetworkConfig) => {
|
||||
cardConfig.value = [];
|
||||
WebConfg.value.set('all', []);
|
||||
for (const key in data) {
|
||||
const configs = data[key as keyof NetworkConfig];
|
||||
if (key in mergeNetworkDefaultConfig) {
|
||||
networkConfig[key] = [...configs];
|
||||
const newConfigsArray = configs.map((config: any) => ({
|
||||
...config,
|
||||
type: typeCh[key as ComponentKey],
|
||||
}));
|
||||
WebConfg.value.set(key, newConfigsArray);
|
||||
const allConfigs = WebConfg.value.get('all');
|
||||
if (allConfigs) {
|
||||
const newAllConfigs = [...allConfigs, ...newConfigsArray];
|
||||
WebConfg.value.set('all', newAllConfigs);
|
||||
}
|
||||
cardConfig.value = WebConfg.value.get('all');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||
getAllData(mergedConfig.network);
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = async (text: string) => {
|
||||
const input = document.createElement('input');
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
await navigator.clipboard.writeText(text);
|
||||
document.body.removeChild(input);
|
||||
MessagePlugin.success('复制成功');
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
// 得根据卡片宽度改,懒得改了;先不管了
|
||||
// if(window.innerWidth < 540) {
|
||||
// infoOneCol.value= true
|
||||
// } else {
|
||||
// infoOneCol.value= false
|
||||
// }
|
||||
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
||||
if (mediumScreen.matches) {
|
||||
cardWidth.value = (tabsWidth.value - 20) / 2;
|
||||
} else if (largeScreen.matches) {
|
||||
cardWidth.value = (tabsWidth.value - 40) / 3;
|
||||
} else {
|
||||
cardWidth.value = tabsWidth.value;
|
||||
}
|
||||
loadPage.value = true;
|
||||
setTimeout(() => {
|
||||
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
||||
}, 300);
|
||||
};
|
||||
emitter.on('sendWidth', (width) => {
|
||||
if (typeof width === 'number' && !isNaN(width)) {
|
||||
menuWidth.value = width;
|
||||
handleResize();
|
||||
}
|
||||
});
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const cachedWidth = localStorage.getItem('menuWidth');
|
||||
if (cachedWidth) {
|
||||
menuWidth.value = parseInt(cachedWidth);
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 300);
|
||||
}
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.setting {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.setting-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setting-card {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-address svg {
|
||||
fill: var(--td-brand-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.local-box {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.local-icon{
|
||||
flex: 1;
|
||||
}
|
||||
.local {
|
||||
flex: 6;
|
||||
margin: 0 10px 0 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
.copy-icon {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
|
||||
.token-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.token-view span {
|
||||
flex: 5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.browse-icon{
|
||||
flex: 2;
|
||||
}
|
||||
:global(.t-dialog__ctx .t-dialog--defaul) {
|
||||
margin: 0 20px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-box {
|
||||
margin: 10px 20px 0 20px;
|
||||
}
|
||||
|
||||
.card-none {
|
||||
line-height: 400px !important;
|
||||
}
|
||||
|
||||
|
||||
.dialog-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.setting-card .t-card__title {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.setting-card .t-card__description {
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-address .t-card__body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.setting-base-info .t-descriptions__header {
|
||||
font-size: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-base-info .t-descriptions__label {
|
||||
padding: 0 var(--td-comp-paddingLR-l) !important;
|
||||
}
|
||||
|
||||
.setting-base-info tr>td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-coll .t-collapse-panel__wrapper .t-collapse-panel__content {
|
||||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
|
||||
}
|
||||
</style>
|
||||
148
napcat.webui/src/pages/OtherConfig.vue
Normal file
148
napcat.webui/src/pages/OtherConfig.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="title">
|
||||
<t-divider content="其余配置" align="left" />
|
||||
</div>
|
||||
<t-card class="card">
|
||||
<div class="other-config-container">
|
||||
<div class="other-config">
|
||||
<t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
|
||||
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
||||
<t-input v-model="otherConfig.musicSignUrl" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
||||
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
|
||||
<t-switch v-model="otherConfig.parseMultMsg" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<div class="button-container">
|
||||
<t-button @click="saveConfig">保存</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||
musicSignUrl: '',
|
||||
enableLocalFile2Url: false,
|
||||
parseMultMsg: true
|
||||
});
|
||||
|
||||
const labelAlign = ref<string>();
|
||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
console.error('No stored credential found');
|
||||
return;
|
||||
}
|
||||
const loginManager = new QQLoginManager(storedCredential);
|
||||
return await loginManager.GetOB11Config();
|
||||
};
|
||||
|
||||
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
console.error('No stored credential found');
|
||||
return false;
|
||||
}
|
||||
const loginManager = new QQLoginManager(storedCredential);
|
||||
return await loginManager.SetOB11Config(config);
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (userConfig) {
|
||||
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
||||
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
||||
otherConfig.value.parseMultMsg = userConfig.parseMultMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (userConfig) {
|
||||
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
||||
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
||||
userConfig.parseMultMsg = otherConfig.value.parseMultMsg ?? true;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
MessagePlugin.success('配置保存成功');
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
labelAlign.value = 'top';
|
||||
} else {
|
||||
labelAlign.value = 'left';
|
||||
}
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQuery.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQuery.dispatchEvent(event);
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
.card {
|
||||
margin: 0 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.other-config-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.other-config {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
49
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
49
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpClientConfig;
|
||||
}>();
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
55
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
55
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 CORS">
|
||||
<t-checkbox v-model="config.enableCors" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 WS">
|
||||
<t-checkbox v-model="config.enableWebsocket" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpServerConfig;
|
||||
}>();
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
52
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
52
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketClientConfig;
|
||||
}>();
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
58
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
58
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-checkbox v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="上报自身消息">
|
||||
<t-checkbox v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="强制推送事件">
|
||||
<t-checkbox v-model="config.enableForcePushEvent" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-checkbox v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketServerConfig;
|
||||
}>();
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
32
napcat.webui/src/router/index.ts
Normal file
32
napcat.webui/src/router/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
|
||||
import Dashboard from '../components/Dashboard.vue';
|
||||
import BasicInfo from '../pages/BasicInfo.vue';
|
||||
import AboutUs from '../pages/AboutUs.vue';
|
||||
import LogView from '../pages/Log.vue';
|
||||
import NetWork from '../pages/NetWork.vue';
|
||||
import QQLogin from '../components/QQLogin.vue';
|
||||
import WebUiLogin from '../components/WebUiLogin.vue';
|
||||
import OtherConfig from '../pages/OtherConfig.vue';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/', redirect: '/webui' },
|
||||
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
|
||||
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: Dashboard,
|
||||
children: [
|
||||
{ path: '', redirect: 'basic-info' },
|
||||
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
|
||||
{ path: 'network-config', component: NetWork, name: 'NetWork' },
|
||||
{ path: 'log-view', component: LogView, name: 'LogView' },
|
||||
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
|
||||
{ path: 'about-us', component: AboutUs, name: 'AboutUs' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
3
napcat.webui/src/ts/event-bus.ts
Normal file
3
napcat.webui/src/ts/event-bus.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import mitt from 'mitt';
|
||||
const emitter = mitt();
|
||||
export default emitter;
|
||||
1
napcat.webui/src/vite-env.d.ts
vendored
Normal file
1
napcat.webui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
34
napcat.webui/tsconfig.json
Normal file
34
napcat.webui/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{"path": "./tsconfig.node.json"}]
|
||||
}
|
||||
11
napcat.webui/tsconfig.node.json
Normal file
11
napcat.webui/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
41
napcat.webui/vite.config.ts
Normal file
41
napcat.webui/vite.config.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
import path from 'path';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ['defaults', 'not IE 11'],
|
||||
modernPolyfills: ['web.structured-clone'],
|
||||
}),
|
||||
],
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:6099',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 4000,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
chunkFileNames: 'static/js/[name]-[hash].js',
|
||||
entryFileNames: 'static/js/[name]-[hash].js',
|
||||
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
|
||||
manualChunks(id: string) {
|
||||
if (id.includes('node_modules')) {
|
||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
60
package.json
Normal file
60
package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.2.3",
|
||||
"scripts": {
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
||||
"build:webui": "cd napcat.webui && vite build",
|
||||
"dev:framework": "vite build --mode framework",
|
||||
"dev:shell": "vite build --mode shell",
|
||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||
"depend": "cd dist && npm install --omit=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@eslint/compat": "^1.2.2",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@napneko/nap-proto-core": "^0.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^12.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"globals": "^15.12.0",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"piscina": "^4.7.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
2
script/KillQQ.bat
Normal file
2
script/KillQQ.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
taskkill /f /im QQ.exe
|
||||
55
script/checkVersion.cjs
Normal file
55
script/checkVersion.cjs
Normal file
@@ -0,0 +1,55 @@
|
||||
const fs = require("fs");
|
||||
const process = require("process");
|
||||
|
||||
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
|
||||
try {
|
||||
const packageJson = require("../package.json");
|
||||
const manifsetJson = require("../manifest.json");
|
||||
|
||||
const currentVersion = packageJson.version;
|
||||
const targetVersion = process.env.VERSION;
|
||||
|
||||
const manifestCurrentVersion = manifsetJson.version;
|
||||
const manifestTargetVersion = process.env.VERSION;
|
||||
|
||||
console.log("[NapCat] [CheckVersion] currentVersion:", currentVersion, "targetVersion:", targetVersion);
|
||||
console.log("[NapCat] [CheckVersion] manifestCurrentVersion:", manifestCurrentVersion, "manifestTargetVersion:", manifestTargetVersion);
|
||||
|
||||
// 验证 targetVersion 格式
|
||||
if (!targetVersion || typeof targetVersion !== 'string') {
|
||||
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
|
||||
return;
|
||||
}
|
||||
// 验证 manifestTargetVersion 格式
|
||||
if (!manifestTargetVersion || typeof manifestTargetVersion !== 'string') {
|
||||
console.log("[NapCat] [CheckVersion] manifest目标版本格式不正确或未设置!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入脚本文件的统一函数
|
||||
const writeScriptToFile = (content) => {
|
||||
fs.writeFileSync("./checkVersion.sh", content, { flag: 'w' });
|
||||
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
|
||||
};
|
||||
|
||||
if (currentVersion === targetVersion && manifestCurrentVersion === manifestTargetVersion) {
|
||||
// 不需要更新版本,写入一个简单的脚本
|
||||
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
|
||||
writeScriptToFile(simpleScript);
|
||||
} else {
|
||||
// 更新版本,构建安全的sed命令
|
||||
const safeScriptContent = `
|
||||
#!/bin/bash
|
||||
git config --global user.email "nanaeonn@outlook.com"
|
||||
git config --global user.name "Mlikiowa"
|
||||
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
|
||||
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
|
||||
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
|
||||
git add .
|
||||
git commit -m "release: v${targetVersion}"
|
||||
git push -u origin main`;
|
||||
writeScriptToFile(safeScriptContent);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[NapCat] [CheckVersion] 检测过程中发生错误:", error);
|
||||
}
|
||||
9
src/common/audio-worker.ts
Normal file
9
src/common/audio-worker.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { encode } from "silk-wasm";
|
||||
|
||||
export interface EncodeArgs {
|
||||
input: ArrayBufferView | ArrayBuffer
|
||||
sampleRate: number
|
||||
}
|
||||
export default async ({ input, sampleRate }: EncodeArgs) => {
|
||||
return await encode(input, sampleRate);
|
||||
};
|
||||
102
src/common/audio.ts
Normal file
102
src/common/audio.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import Piscina from 'piscina';
|
||||
import fsPromise from 'fs/promises';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { EncodeArgs } from "@/common/audio-worker";
|
||||
|
||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||
const EXIT_CODES = [0, 255];
|
||||
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
|
||||
|
||||
async function getWorkerPath() {
|
||||
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
|
||||
}
|
||||
|
||||
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||
filename: await getWorkerPath(),
|
||||
});
|
||||
|
||||
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||
const duration = Math.max(1, Math.floor(pttFileInfo.size / 1024 / 3)); // 3kb/s
|
||||
logger.log('通过文件大小估算语音的时长:', duration);
|
||||
return duration;
|
||||
}
|
||||
|
||||
async function convert(filePath: string, pcmPath: string, logger: LogWrapper): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const cp = spawn(FFMPEG_PATH, ['-y', '-i', filePath, '-ar', '24000', '-ac', '1', '-f', 's16le', pcmPath]);
|
||||
cp.on('error', (err: Error) => {
|
||||
logger.log('FFmpeg处理转换出错: ', err.message);
|
||||
reject(err);
|
||||
});
|
||||
cp.on('exit', async (code, signal) => {
|
||||
if (code == null || EXIT_CODES.includes(code)) {
|
||||
try {
|
||||
const data = await fsPromise.readFile(pcmPath);
|
||||
await fsPromise.unlink(pcmPath);
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
logger.log(`FFmpeg exit: code=${code ?? 'unknown'} sig=${signal ?? 'unknown'}`);
|
||||
reject(new Error('FFmpeg处理转换失败'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleWavFile(
|
||||
file: Buffer,
|
||||
filePath: string,
|
||||
pcmPath: string,
|
||||
logger: LogWrapper
|
||||
): Promise<{ input: Buffer; sampleRate: number }> {
|
||||
const { fmt } = getWavFileInfo(file);
|
||||
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
||||
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
}
|
||||
return { input: file, sampleRate: fmt.sampleRate };
|
||||
}
|
||||
|
||||
export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: LogWrapper) {
|
||||
try {
|
||||
const file = await fsPromise.readFile(filePath);
|
||||
const pttPath = path.join(TEMP_DIR, randomUUID());
|
||||
if (!isSilk(file)) {
|
||||
logger.log(`语音文件${filePath}需要转换成silk`);
|
||||
const pcmPath = `${pttPath}.pcm`;
|
||||
const { input, sampleRate } = isWav(file)
|
||||
? (await handleWavFile(file, filePath, pcmPath, logger))
|
||||
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
|
||||
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
|
||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||
return {
|
||||
converted: true,
|
||||
path: pttPath,
|
||||
duration: silk.duration / 1000,
|
||||
};
|
||||
} else {
|
||||
let duration = 0;
|
||||
try {
|
||||
duration = getDuration(file) / 1000;
|
||||
} catch (e: any) {
|
||||
logger.log('获取语音文件时长失败, 使用文件大小推测时长', filePath, e.stack);
|
||||
duration = await guessDuration(filePath, logger);
|
||||
}
|
||||
return {
|
||||
converted: false,
|
||||
path: filePath,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.logError.bind(logger)('convert silk failed', error.stack);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
74
src/common/config-base.ts
Normal file
74
src/common/config-base.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import type { NapCatCore } from '@/core';
|
||||
|
||||
export abstract class ConfigBase<T> {
|
||||
name: string;
|
||||
core: NapCatCore;
|
||||
configPath: string;
|
||||
configData: T = {} as T;
|
||||
|
||||
protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) {
|
||||
this.name = name;
|
||||
this.core = core;
|
||||
this.configPath = configPath;
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
this.read(copy_default);
|
||||
}
|
||||
|
||||
protected getKeys(): string[] | null {
|
||||
// 决定 key 在json配置文件中的顺序
|
||||
return null;
|
||||
}
|
||||
|
||||
getConfigPath(pathName: string | undefined): string {
|
||||
if (!pathName) {
|
||||
const filename = `${this.name}.json`;
|
||||
const mainPath = this.core.context.pathWrapper.binaryPath;
|
||||
return path.join(mainPath, 'config', filename);
|
||||
} else {
|
||||
const filename = `${this.name}_${pathName}.json`;
|
||||
return path.join(this.configPath, filename);
|
||||
}
|
||||
}
|
||||
|
||||
read(copy_default: boolean = true): T {
|
||||
const logger = this.core.context.logger;
|
||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||
if (!fs.existsSync(configPath) && copy_default) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||
} catch (e: any) {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
}
|
||||
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||
fs.writeFileSync(configPath, '{}');
|
||||
}
|
||||
try {
|
||||
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
|
||||
return this.configData;
|
||||
} catch (e: any) {
|
||||
if (e instanceof SyntaxError) {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
} else {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
save(newConfigData: T = this.configData) {
|
||||
const logger = this.core.context.logger;
|
||||
const selfInfo = this.core.selfInfo;
|
||||
this.configData = newConfigData;
|
||||
const configPath = this.getConfigPath(selfInfo.uin);
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||
} catch (e: any) {
|
||||
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/common/event.ts
Normal file
267
src/common/event.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { NodeIQQNTWrapperSession } from '@/core/wrapper';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { ListenerNamingMapping, ServiceNamingMapping } from '@/core';
|
||||
|
||||
interface InternalMapKey {
|
||||
timeout: number;
|
||||
createtime: number;
|
||||
func: (...arg: any[]) => any;
|
||||
checker: ((...args: any[]) => boolean) | undefined;
|
||||
}
|
||||
|
||||
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
||||
|
||||
type FuncKeys<T> = Extract<
|
||||
{
|
||||
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
||||
}[keyof T],
|
||||
string
|
||||
>;
|
||||
|
||||
export type ListenerClassBase = Record<string, string>;
|
||||
|
||||
export class NTEventWrapper {
|
||||
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
|
||||
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
|
||||
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
|
||||
|
||||
constructor(
|
||||
wrapperSession: NodeIQQNTWrapperSession,
|
||||
) {
|
||||
this.WrapperSession = wrapperSession;
|
||||
}
|
||||
|
||||
createProxyDispatch(ListenerMainName: string) {
|
||||
const dispatcherListenerFunc = this.dispatcherListener.bind(this);
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(target: any, prop: any, receiver: any) {
|
||||
if (typeof target[prop] === 'undefined') {
|
||||
// 如果方法不存在,返回一个函数,这个函数调用existentMethod
|
||||
return (...args: any[]) => {
|
||||
dispatcherListenerFunc(ListenerMainName, prop, ...args).then();
|
||||
};
|
||||
}
|
||||
// 如果方法存在,正常返回
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
createEventFunction<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
||||
const eventNameArr = eventName.split('/');
|
||||
type eventType = {
|
||||
[key: string]: () => { [key: string]: (...params: Parameters<T>) => Promise<ReturnType<T>> };
|
||||
};
|
||||
if (eventNameArr.length > 1) {
|
||||
const serviceName = 'get' + eventNameArr[0].replace('NodeIKernel', '');
|
||||
const eventName = eventNameArr[1];
|
||||
const services = (this.WrapperSession as unknown as eventType)[serviceName]();
|
||||
let event = services[eventName];
|
||||
//重新绑定this
|
||||
event = event.bind(services);
|
||||
if (event) {
|
||||
return event as T;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
createListenerFunction<T>(listenerMainName: string, uniqueCode: string = ''): T {
|
||||
const existListener = this.listenerManager.get(listenerMainName + uniqueCode);
|
||||
if (!existListener) {
|
||||
const Listener = this.createProxyDispatch(listenerMainName);
|
||||
const ServiceSubName = /^NodeIKernel(.*?)Listener$/.exec(listenerMainName)![1];
|
||||
const Service = `NodeIKernel${ServiceSubName}Service/addKernel${ServiceSubName}Listener`;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
this.createEventFunction(Service)(Listener as T);
|
||||
this.listenerManager.set(listenerMainName + uniqueCode, Listener);
|
||||
return Listener as T;
|
||||
}
|
||||
return existListener as T;
|
||||
}
|
||||
|
||||
//统一回调清理事件
|
||||
async dispatcherListener(ListenerMainName: string, ListenerSubName: string, ...args: any[]) {
|
||||
this.EventTask.get(ListenerMainName)
|
||||
?.get(ListenerSubName)
|
||||
?.forEach((task, uuid) => {
|
||||
if (task.createtime + task.timeout < Date.now()) {
|
||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.delete(uuid);
|
||||
return;
|
||||
}
|
||||
if (task?.checker?.(...args)) {
|
||||
task.func(...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async callNoListenerEvent<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
>(
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
...args: Parameters<EventType>
|
||||
): Promise<Awaited<ReturnType<EventType>>> {
|
||||
return (this.createEventFunction(serviceAndMethod))!(...args);
|
||||
}
|
||||
|
||||
async registerListen<
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
|
||||
>(
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||
waitTimes = 1,
|
||||
timeout = 5000,
|
||||
) {
|
||||
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
|
||||
function sendDataCallback() {
|
||||
if (complete == 0) {
|
||||
reject(new Error(' ListenerName:' + listenerAndMethod + ' timeout'));
|
||||
} else {
|
||||
resolve(retData!);
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutRef = setTimeout(sendDataCallback, timeout);
|
||||
const eventCallback = {
|
||||
timeout: timeout,
|
||||
createtime: Date.now(),
|
||||
checker: checker,
|
||||
func: (...args: Parameters<ListenerType>) => {
|
||||
complete++;
|
||||
retData = args;
|
||||
if (complete >= waitTimes) {
|
||||
clearTimeout(timeoutRef);
|
||||
sendDataCallback();
|
||||
}
|
||||
},
|
||||
};
|
||||
if (!this.EventTask.get(ListenerMainName)) {
|
||||
this.EventTask.set(ListenerMainName, new Map());
|
||||
}
|
||||
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
|
||||
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
|
||||
}
|
||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
|
||||
this.createListenerFunction(ListenerMainName);
|
||||
});
|
||||
}
|
||||
|
||||
async callNormalEventV2<
|
||||
Service extends keyof ServiceNamingMapping,
|
||||
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||
Listener extends keyof ListenerNamingMapping,
|
||||
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
|
||||
>(
|
||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||
args: Parameters<EventType>,
|
||||
checkerEvent: (ret: Awaited<ReturnType<EventType>>) => boolean = () => true,
|
||||
checkerListener: (...args: Parameters<ListenerType>) => boolean = () => true,
|
||||
callbackTimesToWait = 1,
|
||||
timeout = 5000,
|
||||
) {
|
||||
const id = randomUUID();
|
||||
let complete = 0;
|
||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||
let retEvent: any = {};
|
||||
|
||||
function sendDataCallback(resolve: any, reject: any) {
|
||||
if (complete == 0) {
|
||||
reject(
|
||||
new Error(
|
||||
'Timeout: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||
}
|
||||
}
|
||||
|
||||
const ListenerNameList = listenerAndMethod.split('/');
|
||||
const ListenerMainName = ListenerNameList[0];
|
||||
const ListenerSubName = ListenerNameList[1];
|
||||
|
||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
||||
(resolve, reject) => {
|
||||
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
|
||||
|
||||
const eventCallback = {
|
||||
timeout: timeout,
|
||||
createtime: Date.now(),
|
||||
checker: checkerListener,
|
||||
func: (...args: any[]) => {
|
||||
complete++;
|
||||
retData = args as Parameters<ListenerType>;
|
||||
if (complete >= callbackTimesToWait) {
|
||||
clearTimeout(timeoutRef);
|
||||
sendDataCallback(resolve, reject);
|
||||
}
|
||||
},
|
||||
};
|
||||
if (!this.EventTask.get(ListenerMainName)) {
|
||||
this.EventTask.set(ListenerMainName, new Map());
|
||||
}
|
||||
if (!this.EventTask.get(ListenerMainName)?.get(ListenerSubName)) {
|
||||
this.EventTask.get(ListenerMainName)?.set(ListenerSubName, new Map());
|
||||
}
|
||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
|
||||
this.createListenerFunction(ListenerMainName);
|
||||
|
||||
const eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
|
||||
|
||||
const eventRetHandle = (eventData: any) => {
|
||||
retEvent = eventData;
|
||||
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
||||
clearTimeout(timeoutRef);
|
||||
reject(
|
||||
new Error(
|
||||
'EventChecker Failed: NTEvent serviceAndMethod:' +
|
||||
serviceAndMethod +
|
||||
' ListenerName:' +
|
||||
listenerAndMethod +
|
||||
' EventRet:\n' +
|
||||
JSON.stringify(retEvent, null, 4) +
|
||||
'\n',
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
if (eventResult instanceof Promise) {
|
||||
eventResult.then((eventResult: any) => {
|
||||
eventRetHandle(eventResult);
|
||||
})
|
||||
.catch(reject);
|
||||
} else {
|
||||
eventRetHandle(eventResult);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
285
src/common/file.ts
Normal file
285
src/common/file.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import fs from 'fs';
|
||||
import { stat } from 'fs/promises';
|
||||
import crypto, { randomUUID } from 'crypto';
|
||||
import util from 'util';
|
||||
import path from 'node:path';
|
||||
import * as fileType from 'file-type';
|
||||
import { solveProblem } from '@/common/helper';
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
url: string;
|
||||
headers?: Record<string, string> | string;
|
||||
}
|
||||
|
||||
type Uri2LocalRes = {
|
||||
success: boolean,
|
||||
errMsg: string,
|
||||
fileName: string,
|
||||
ext: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export function checkFileExist(path: string, timeout: number = 3000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
function check() {
|
||||
if (fs.existsSync(path)) {
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error(`文件不存在: ${path}`));
|
||||
} else {
|
||||
setTimeout(check, 100);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export async function checkFileExistV2(path: string, timeout: number = 3000): Promise<void> {
|
||||
// 使用 Promise.race 来同时进行文件状态检查和超时计时
|
||||
// Promise.race 会返回第一个解决(resolve)或拒绝(reject)的 Promise
|
||||
await Promise.race([
|
||||
checkFile(path),
|
||||
timeoutPromise(timeout, `文件不存在: ${path}`),
|
||||
]);
|
||||
}
|
||||
|
||||
// 转换超时时间至 Promise
|
||||
function timeoutPromise(timeout: number, errorMsg: string): Promise<void> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(errorMsg));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
// 异步检查文件是否存在
|
||||
async function checkFile(path: string): Promise<void> {
|
||||
try {
|
||||
await stat(path);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
// 如果文件不存在,则抛出一个错误
|
||||
throw new Error(`文件不存在: ${path}`);
|
||||
} else {
|
||||
// 对于 stat 调用的其他错误,重新抛出
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
||||
}
|
||||
|
||||
export async function file2base64(path: string) {
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const result = {
|
||||
err: '',
|
||||
data: '',
|
||||
};
|
||||
try {
|
||||
try {
|
||||
await checkFileExist(path, 5000);
|
||||
} catch (e: any) {
|
||||
result.err = e.toString();
|
||||
return result;
|
||||
}
|
||||
const data = await readFile(path);
|
||||
result.data = data.toString('base64');
|
||||
} catch (err: any) {
|
||||
result.err = err.toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateFileMD5(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建一个流式读取器
|
||||
const stream = fs.createReadStream(filePath);
|
||||
const hash = crypto.createHash('md5');
|
||||
|
||||
stream.on('data', (data: Buffer) => {
|
||||
// 当读取到数据时,更新哈希对象的状态
|
||||
hash.update(data);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
// 文件读取完成,计算哈希
|
||||
const md5 = hash.digest('hex');
|
||||
resolve(md5);
|
||||
});
|
||||
|
||||
stream.on('error', (err: Error) => {
|
||||
// 处理可能的读取错误
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||
let url: string;
|
||||
let headers: Record<string, string> = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
|
||||
};
|
||||
if (typeof options === 'string') {
|
||||
url = options;
|
||||
headers['Host'] = new URL(url).hostname;
|
||||
} else {
|
||||
url = options.url;
|
||||
if (options.headers) {
|
||||
if (typeof options.headers === 'string') {
|
||||
headers = JSON.parse(options.headers);
|
||||
} else {
|
||||
headers = options.headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (useReferer && !headers['Referer']) {
|
||||
headers['Referer'] = url;
|
||||
}
|
||||
const fetchRes = await fetch(url, { headers }).catch((err) => {
|
||||
if (err.cause) {
|
||||
throw err.cause;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
|
||||
export async function httpDownload(options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||
const useReferer = typeof options === 'string';
|
||||
let resp = await tryDownload(options);
|
||||
if (resp.status === 403 && useReferer) {
|
||||
resp = await tryDownload(options, true);
|
||||
}
|
||||
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
|
||||
const blob = await resp.blob();
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
|
||||
export async function checkFileV2(filePath: string) {
|
||||
try {
|
||||
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
if (ext) {
|
||||
fs.renameSync(filePath, filePath + `.${ext}`);
|
||||
filePath += `.${ext}`;
|
||||
return { success: true, ext: ext, path: filePath };
|
||||
}
|
||||
} catch (e) {
|
||||
// log("获取文件类型失败", filePath,e.stack)
|
||||
}
|
||||
return { success: false, ext: '', path: filePath };
|
||||
}
|
||||
|
||||
export enum FileUriType {
|
||||
Unknown = 0,
|
||||
Local = 1,
|
||||
Remote = 2,
|
||||
Base64 = 3
|
||||
}
|
||||
|
||||
export async function checkUriType(Uri: string) {
|
||||
const LocalFileRet = await solveProblem((uri: string) => {
|
||||
if (fs.existsSync(uri)) {
|
||||
return { Uri: uri, Type: FileUriType.Local };
|
||||
}
|
||||
return undefined;
|
||||
}, Uri);
|
||||
if (LocalFileRet) return LocalFileRet;
|
||||
const OtherFileRet = await solveProblem((uri: string) => {
|
||||
// 再判断是否是Http
|
||||
if (uri.startsWith('http:') || uri.startsWith('https:')) {
|
||||
return { Uri: uri, Type: FileUriType.Remote };
|
||||
}
|
||||
// 再判断是否是Base64
|
||||
if (uri.startsWith('base64:')) {
|
||||
return { Uri: uri, Type: FileUriType.Base64 };
|
||||
}
|
||||
// 默认file://
|
||||
if (uri.startsWith('file:')) {
|
||||
// 兼容file:///
|
||||
// file:///C:/1.jpg
|
||||
if (uri.startsWith('file:///') && process.platform === 'win32') {
|
||||
const filePath: string = uri.slice(8);
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
// 处理默认规范
|
||||
// file://C:\1.jpg
|
||||
// file:///test/1.jpg
|
||||
const filePath: string = uri.slice(7);
|
||||
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
if (uri.startsWith('data:')) {
|
||||
const data = uri.split(',')[1];
|
||||
if (data) return { Uri: data, Type: FileUriType.Base64 };
|
||||
}
|
||||
}, Uri);
|
||||
if (OtherFileRet) return OtherFileRet;
|
||||
|
||||
return { Uri: Uri, Type: FileUriType.Unknown };
|
||||
}
|
||||
|
||||
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
|
||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||
|
||||
//解析失败
|
||||
const tempName = randomUUID();
|
||||
if (!filename) filename = randomUUID();
|
||||
|
||||
//解析Http和Https协议
|
||||
if (UriType == FileUriType.Unknown) {
|
||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||
}
|
||||
|
||||
//解析File协议和本地文件
|
||||
if (UriType == FileUriType.Local) {
|
||||
const fileExt = path.extname(HandledUri);
|
||||
let filename = path.basename(HandledUri, fileExt);
|
||||
filename += fileExt;
|
||||
//复制文件到临时文件并保持后缀
|
||||
const filenameTemp = tempName + fileExt;
|
||||
const filePath = path.join(dir, filenameTemp);
|
||||
fs.copyFileSync(HandledUri, filePath);
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
|
||||
//接下来都要有文件名
|
||||
if (UriType == FileUriType.Remote) {
|
||||
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
|
||||
if (pathInfo.name) {
|
||||
const pathlen = 200 - dir.length - pathInfo.name.length;
|
||||
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
|
||||
if (pathInfo.ext) {
|
||||
filename += pathInfo.ext;
|
||||
}
|
||||
}
|
||||
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
|
||||
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
|
||||
const filePath = path.join(dir, tempName + fileExt);
|
||||
const buffer = await httpDownload(HandledUri);
|
||||
//没有文件就创建
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
|
||||
//解析Base64
|
||||
if (UriType == FileUriType.Base64) {
|
||||
const base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||
const buffer = Buffer.from(base64, 'base64');
|
||||
let filePath = path.join(dir, filename);
|
||||
let fileExt = '';
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
const { success, ext, path: fileTypePath } = await checkFileV2(filePath);
|
||||
if (success) {
|
||||
filePath = fileTypePath;
|
||||
fileExt = ext;
|
||||
filename = filename + '.' + ext;
|
||||
}
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
}
|
||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||
}
|
||||
114
src/common/forward-msg-builder.ts
Normal file
114
src/common/forward-msg-builder.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import { PacketMsg } from "@/core/packet/message/message";
|
||||
|
||||
interface ForwardMsgJson {
|
||||
app: string
|
||||
config: ForwardMsgJsonConfig,
|
||||
desc: string,
|
||||
extra: ForwardMsgJsonExtra,
|
||||
meta: ForwardMsgJsonMeta,
|
||||
prompt: string,
|
||||
ver: string,
|
||||
view: string
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonConfig {
|
||||
autosize: number,
|
||||
forward: number,
|
||||
round: number,
|
||||
type: string,
|
||||
width: number
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonExtra {
|
||||
filename: string,
|
||||
tsum: number,
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonMeta {
|
||||
detail: ForwardMsgJsonMetaDetail
|
||||
}
|
||||
|
||||
interface ForwardMsgJsonMetaDetail {
|
||||
news: {
|
||||
text: string
|
||||
}[],
|
||||
resid: string,
|
||||
source: string,
|
||||
summary: string,
|
||||
uniseq: string
|
||||
}
|
||||
|
||||
interface ForwardAdaptMsg {
|
||||
senderName?: string;
|
||||
isGroupMsg?: boolean;
|
||||
msg?: ForwardAdaptMsgElement[];
|
||||
}
|
||||
|
||||
interface ForwardAdaptMsgElement {
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export class ForwardMsgBuilder {
|
||||
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
const id = crypto.randomUUID();
|
||||
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||
if (!source) {
|
||||
source = isGroupMsg ? "群聊的聊天记录" : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
|
||||
}
|
||||
if (!news) {
|
||||
news = msg.length === 0 ? [{
|
||||
text: "Nya~ This message is send from NapCat.Packet!",
|
||||
}] : msg.map(m => ({
|
||||
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||
}));
|
||||
}
|
||||
if (!summary) {
|
||||
summary = `查看${msg.length}条转发消息`;
|
||||
}
|
||||
if (!prompt) {
|
||||
prompt = "[聊天记录]";
|
||||
}
|
||||
return {
|
||||
app: "com.tencent.multimsg",
|
||||
config: {
|
||||
autosize: 1,
|
||||
forward: 1,
|
||||
round: 1,
|
||||
type: "normal",
|
||||
width: 300
|
||||
},
|
||||
desc: prompt,
|
||||
extra: {
|
||||
filename: id,
|
||||
tsum: msg.length,
|
||||
},
|
||||
meta: {
|
||||
detail: {
|
||||
news,
|
||||
resid: resId,
|
||||
source,
|
||||
summary,
|
||||
uniseq: id,
|
||||
}
|
||||
},
|
||||
prompt,
|
||||
ver: "0.0.0.5",
|
||||
view: "contact",
|
||||
};
|
||||
}
|
||||
|
||||
static fromResId(resId: string): ForwardMsgJson {
|
||||
return this.build(resId, []);
|
||||
}
|
||||
|
||||
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||
return this.build(resId, packetMsg.map(msg => ({
|
||||
senderName: msg.senderName,
|
||||
isGroupMsg: msg.groupId !== undefined,
|
||||
msg: msg.msg.map(m => ({
|
||||
preview: m.valid ? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||
}))
|
||||
})), source, news, summary, prompt);
|
||||
}
|
||||
}
|
||||
280
src/common/helper.ts
Normal file
280
src/common/helper.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { Peer, QQLevel } from '@/core';
|
||||
|
||||
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||
try {
|
||||
const result = func(...args);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function solveAsyncProblem<T extends (...args: any[]) => Promise<any>>(func: T, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>> | undefined> {
|
||||
return new Promise<Awaited<ReturnType<T>> | undefined>((resolve) => {
|
||||
func(...args).then((result) => {
|
||||
resolve(result);
|
||||
}).catch(() => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUID {
|
||||
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decodeModelId(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
modelId: string,
|
||||
fileId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
modelId,
|
||||
fileId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
|
||||
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decode(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
msgId: string,
|
||||
elementId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
msgId,
|
||||
elementId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function PromiseTimer<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
const timeoutPromise = new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('PromiseTimer: Operation timed out')), ms),
|
||||
);
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
export async function runAllWithTimeout<T>(tasks: Promise<T>[], timeout: number): Promise<T[]> {
|
||||
const wrappedTasks = tasks.map((task) =>
|
||||
PromiseTimer(task, timeout).then(
|
||||
(result) => ({ status: 'fulfilled', value: result }),
|
||||
(error) => ({ status: 'rejected', reason: error }),
|
||||
),
|
||||
);
|
||||
const results = await Promise.all(wrappedTasks);
|
||||
return results
|
||||
.filter((result) => result.status === 'fulfilled')
|
||||
.map((result) => (result as { status: 'fulfilled'; value: T }).value);
|
||||
}
|
||||
|
||||
export function isNull(value: any) {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
export function isNumeric(str: string) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
export function truncateString(obj: any, maxLength = 500) {
|
||||
if (obj !== null && typeof obj === 'object') {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
if (typeof obj[key] === 'string') {
|
||||
// 如果是字符串且超过指定长度,则截断
|
||||
if (obj[key].length > maxLength) {
|
||||
obj[key] = obj[key].substring(0, maxLength) + '...';
|
||||
}
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
// 如果是对象或数组,则递归调用
|
||||
truncateString(obj[key], maxLength);
|
||||
}
|
||||
});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function isEqual(obj1: any, obj2: any) {
|
||||
if (obj1 === obj2) return true;
|
||||
if (obj1 == null || obj2 == null) return false;
|
||||
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return obj1 === obj2;
|
||||
|
||||
const keys1 = Object.keys(obj1);
|
||||
const keys2 = Object.keys(obj2);
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
|
||||
for (const key of keys1) {
|
||||
if (!isEqual(obj1[key], obj2[key])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getDefaultQQVersionConfigInfo(): QQVersionConfigType {
|
||||
if (os.platform() === 'linux') {
|
||||
return {
|
||||
baseVersion: '3.2.12.28060',
|
||||
curVersion: '3.2.12.28060',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '27254',
|
||||
};
|
||||
}
|
||||
if (os.platform() === 'darwin') {
|
||||
return {
|
||||
baseVersion: '6.9.53.28060',
|
||||
curVersion: '6.9.53.28060',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '28060',
|
||||
};
|
||||
}
|
||||
return {
|
||||
baseVersion: '9.9.15-28131',
|
||||
curVersion: '9.9.15-28131',
|
||||
prevVersion: '',
|
||||
onErrorVersions: [],
|
||||
buildId: '28131',
|
||||
};
|
||||
}
|
||||
|
||||
export function getQQPackageInfoPath(exePath: string = '', version?: string): string {
|
||||
let packagePath;
|
||||
if (os.platform() === 'darwin') {
|
||||
packagePath = path.join(path.dirname(exePath), '..', 'Resources', 'app', 'package.json');
|
||||
} else if (os.platform() === 'linux') {
|
||||
packagePath = path.join(path.dirname(exePath), './resources/app/package.json');
|
||||
} else {
|
||||
packagePath = path.join(path.dirname(exePath), './versions/' + version + '/resources/app/package.json');
|
||||
}
|
||||
//下面是老版本兼容 未来去掉
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
packagePath = path.join(path.dirname(exePath), './resources/app/versions/' + version + '/package.json');
|
||||
}
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
export function getQQVersionConfigPath(exePath: string = ''): string | undefined {
|
||||
let configVersionInfoPath;
|
||||
if (os.platform() === 'win32') {
|
||||
configVersionInfoPath = path.join(path.dirname(exePath), 'versions', 'config.json');
|
||||
} else if (os.platform() === 'darwin') {
|
||||
const userPath = os.homedir();
|
||||
const appDataPath = path.resolve(userPath, './Library/Application Support/QQ');
|
||||
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
|
||||
} else {
|
||||
const userPath = os.homedir();
|
||||
const appDataPath = path.resolve(userPath, './.config/QQ');
|
||||
configVersionInfoPath = path.resolve(appDataPath, './versions/config.json');
|
||||
}
|
||||
if (typeof configVersionInfoPath !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
//老版本兼容 未来去掉
|
||||
if (!fs.existsSync(configVersionInfoPath)) {
|
||||
configVersionInfoPath = path.join(path.dirname(exePath), './resources/app/versions/config.json');
|
||||
}
|
||||
if (!fs.existsSync(configVersionInfoPath)) {
|
||||
return undefined;
|
||||
}
|
||||
return configVersionInfoPath;
|
||||
}
|
||||
|
||||
export function calcQQLevel(level?: QQLevel) {
|
||||
if (!level) return 0;
|
||||
const { crownNum, sunNum, moonNum, starNum } = level;
|
||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||
}
|
||||
|
||||
export function stringifyWithBigInt(obj: any) {
|
||||
return JSON.stringify(obj, (key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value
|
||||
);
|
||||
}
|
||||
|
||||
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||
const hexSequence = "A4 09 00 00 00 35";
|
||||
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
|
||||
const filePath = path.resolve(nodeMajor);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
let searchPosition = 0;
|
||||
while (true) {
|
||||
const index = fileContent.indexOf(sequenceBytes, searchPosition);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const start = index + sequenceBytes.length - 1;
|
||||
const end = fileContent.indexOf(0x00, start);
|
||||
if (end === -1) {
|
||||
break;
|
||||
}
|
||||
const content = fileContent.subarray(start, end);
|
||||
if (!content.every(byte => byte === 0x00)) {
|
||||
try {
|
||||
return content.toString("utf-8");
|
||||
} catch (error) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
searchPosition = end + 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
294
src/common/log.ts
Normal file
294
src/common/log.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import winston, { format, transports } from 'winston';
|
||||
import { truncateString } from '@/common/helper';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
function getFormattedTimestamp() {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = now.getDate().toString().padStart(2, '0');
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
const seconds = now.getSeconds().toString().padStart(2, '0');
|
||||
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
|
||||
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
|
||||
}
|
||||
|
||||
export class LogWrapper {
|
||||
fileLogEnabled = true;
|
||||
consoleLogEnabled = true;
|
||||
logger: winston.Logger;
|
||||
|
||||
constructor(logDir: string) {
|
||||
const filename = `${getFormattedTimestamp()}.log`;
|
||||
const logPath = path.join(logDir, filename);
|
||||
|
||||
this.logger = winston.createLogger({
|
||||
level: 'debug',
|
||||
format: format.combine(
|
||||
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
|
||||
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||
})
|
||||
),
|
||||
transports: [
|
||||
new transports.File({
|
||||
filename: logPath,
|
||||
level: 'debug',
|
||||
maxsize: 5 * 1024 * 1024, // 5MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
this.setLogSelfInfo({ nick: '', uid: '' });
|
||||
this.cleanOldLogs(logDir);
|
||||
}
|
||||
|
||||
cleanOldLogs(logDir: string) {
|
||||
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||
fs.readdir(logDir, (err, files) => {
|
||||
if (err) {
|
||||
this.logger.error('Failed to read log directory', err);
|
||||
return;
|
||||
}
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(logDir, file);
|
||||
this.deleteOldLogFile(filePath, oneWeekAgo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
this.logger.error('Failed to get file stats', err);
|
||||
return;
|
||||
}
|
||||
if (stats.mtime.getTime() < oneWeekAgo) {
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
this.logger.warn(`File already deleted: ${filePath}`);
|
||||
} else {
|
||||
this.logger.error('Failed to delete old log file', err);
|
||||
}
|
||||
} else {
|
||||
this.logger.info(`Deleted old log file: ${filePath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
|
||||
this.logger.transports.forEach((transport) => {
|
||||
if (transport instanceof transports.File) {
|
||||
transport.level = fileLogLevel;
|
||||
} else if (transport instanceof transports.Console) {
|
||||
transport.level = consoleLogLevel;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
|
||||
const userInfo = `${selfInfo.nick}`;
|
||||
this.logger.defaultMeta = { userInfo };
|
||||
}
|
||||
|
||||
setFileLogEnabled(isEnabled: boolean) {
|
||||
this.fileLogEnabled = isEnabled;
|
||||
this.logger.transports.forEach((transport) => {
|
||||
if (transport instanceof transports.File) {
|
||||
transport.silent = !isEnabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConsoleLogEnabled(isEnabled: boolean) {
|
||||
this.consoleLogEnabled = isEnabled;
|
||||
this.logger.transports.forEach((transport) => {
|
||||
if (transport instanceof transports.Console) {
|
||||
transport.silent = !isEnabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formatMsg(msg: any[]) {
|
||||
return msg.map(msgItem => {
|
||||
if (msgItem instanceof Error) {
|
||||
return msgItem.stack;
|
||||
} else if (typeof msgItem === 'object') {
|
||||
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
||||
}
|
||||
return msgItem;
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
_log(level: LogLevel, ...args: any[]) {
|
||||
const message = this.formatMsg(args);
|
||||
if (this.consoleLogEnabled && this.fileLogEnabled) {
|
||||
this.logger.log(level, message);
|
||||
} else if (this.consoleLogEnabled) {
|
||||
this.logger.log(level, message);
|
||||
} else if (this.fileLogEnabled) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
this._log(LogLevel.INFO, ...args);
|
||||
}
|
||||
|
||||
logDebug(...args: any[]) {
|
||||
this._log(LogLevel.DEBUG, ...args);
|
||||
}
|
||||
|
||||
logError(...args: any[]) {
|
||||
this._log(LogLevel.ERROR, ...args);
|
||||
}
|
||||
|
||||
logWarn(...args: any[]) {
|
||||
this._log(LogLevel.WARN, ...args);
|
||||
}
|
||||
|
||||
logFatal(...args: any[]) {
|
||||
this._log(LogLevel.FATAL, ...args);
|
||||
}
|
||||
|
||||
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
|
||||
const isSelfSent = msg.senderUin === selfInfo.uin;
|
||||
|
||||
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
||||
if (recursiveLevel > 2) {
|
||||
return '...';
|
||||
}
|
||||
|
||||
const tokens: string[] = [];
|
||||
|
||||
if (msg.chatType == ChatType.KCHATTYPEC2C) {
|
||||
tokens.push(`私聊 (${msg.peerUin})`);
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEGROUP) {
|
||||
if (recursiveLevel < 1) {
|
||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||
}
|
||||
if (msg.senderUin !== '0') {
|
||||
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
|
||||
}
|
||||
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
||||
tokens.push('移动设备');
|
||||
} else {
|
||||
tokens.push(`临时消息 (${msg.peerUin})`);
|
||||
}
|
||||
|
||||
for (const element of msg.elements) {
|
||||
tokens.push(msgElementToText(element, msg, recursiveLevel));
|
||||
}
|
||||
|
||||
return tokens.join(' ');
|
||||
}
|
||||
|
||||
function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
|
||||
if (element.textElement) {
|
||||
return textElementToText(element.textElement);
|
||||
}
|
||||
|
||||
if (element.replyElement) {
|
||||
return replyElementToText(element.replyElement, msg, recursiveLevel);
|
||||
}
|
||||
|
||||
if (element.picElement) {
|
||||
return '[图片]';
|
||||
}
|
||||
|
||||
if (element.fileElement) {
|
||||
return `[文件 ${element.fileElement.fileName}]`;
|
||||
}
|
||||
|
||||
if (element.videoElement) {
|
||||
return '[视频]';
|
||||
}
|
||||
|
||||
if (element.pttElement) {
|
||||
return `[语音 ${element.pttElement.duration}s]`;
|
||||
}
|
||||
|
||||
if (element.arkElement) {
|
||||
return '[卡片消息]';
|
||||
}
|
||||
|
||||
if (element.faceElement) {
|
||||
return `[表情 ${element.faceElement.faceText ?? ''}]`;
|
||||
}
|
||||
|
||||
if (element.marketFaceElement) {
|
||||
return element.marketFaceElement.faceName;
|
||||
}
|
||||
|
||||
if (element.markdownElement) {
|
||||
return '[Markdown 消息]';
|
||||
}
|
||||
|
||||
if (element.multiForwardMsgElement) {
|
||||
return '[转发消息]';
|
||||
}
|
||||
|
||||
if (element.elementType === ElementType.GreyTip) {
|
||||
return '[灰条消息]';
|
||||
}
|
||||
|
||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||
}
|
||||
|
||||
function textElementToText(textElement: any): string {
|
||||
if (textElement.atType === NTMsgAtType.ATTYPEUNKNOWN) {
|
||||
const originalContentLines = textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (textElement.atType === NTMsgAtType.ATTYPEALL) {
|
||||
return `@全体成员`;
|
||||
} else if (textElement.atType === NTMsgAtType.ATTYPEONE) {
|
||||
return `${textElement.content} (${textElement.atUid})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
|
||||
const recordMsgOrNull = msg.records.find(
|
||||
record => replyElement.sourceMsgIdInRecords === record.msgId,
|
||||
);
|
||||
return `[回复消息 ${recordMsgOrNull &&
|
||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
||||
?
|
||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
||||
}]`;
|
||||
}
|
||||
42
src/common/lru-cache.ts
Normal file
42
src/common/lru-cache.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export class LRUCache<K, V> {
|
||||
private capacity: number;
|
||||
public cache: Map<K, V>;
|
||||
|
||||
constructor(capacity: number) {
|
||||
this.capacity = capacity;
|
||||
this.cache = new Map<K, V>();
|
||||
}
|
||||
|
||||
public get(key: K): V | undefined {
|
||||
const value = this.cache.get(key);
|
||||
if (value !== undefined) {
|
||||
// Move the accessed key to the end to mark it as most recently used
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public put(key: K, value: V): void {
|
||||
if (this.cache.has(key)) {
|
||||
// If the key already exists, move it to the end to mark it as most recently used
|
||||
this.cache.delete(key);
|
||||
} else if (this.cache.size >= this.capacity) {
|
||||
// If the cache is full, remove the least recently used key (the first one in the map)
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
public resetCapacity(newCapacity: number): void {
|
||||
this.capacity = newCapacity;
|
||||
while (this.cache.size > this.capacity) {
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/common/message-unique.ts
Normal file
138
src/common/message-unique.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Peer } from '@/core';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export class LimitedHashTable<K, V> {
|
||||
private readonly keyToValue: Map<K, V> = new Map();
|
||||
private readonly valueToKey: Map<V, K> = new Map();
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
resize(count: number) {
|
||||
this.maxSize = count;
|
||||
}
|
||||
|
||||
set(key: K, value: V): void {
|
||||
this.keyToValue.set(key, value);
|
||||
this.valueToKey.set(value, key);
|
||||
while (this.keyToValue.size !== this.valueToKey.size) {
|
||||
this.keyToValue.clear();
|
||||
this.valueToKey.clear();
|
||||
}
|
||||
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
|
||||
const oldestKey = this.keyToValue.keys().next().value;
|
||||
if (oldestKey !== undefined) {
|
||||
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
|
||||
this.keyToValue.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(key: K): V | undefined {
|
||||
return this.keyToValue.get(key);
|
||||
}
|
||||
|
||||
getKey(value: V): K | undefined {
|
||||
return this.valueToKey.get(value);
|
||||
}
|
||||
|
||||
deleteByValue(value: V): void {
|
||||
const key = this.valueToKey.get(value);
|
||||
if (key !== undefined) {
|
||||
this.keyToValue.delete(key);
|
||||
this.valueToKey.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
deleteByKey(key: K): void {
|
||||
const value = this.keyToValue.get(key);
|
||||
if (value !== undefined) {
|
||||
this.keyToValue.delete(key);
|
||||
this.valueToKey.delete(value);
|
||||
}
|
||||
}
|
||||
|
||||
getKeyList(): K[] {
|
||||
return Array.from(this.keyToValue.keys());
|
||||
}
|
||||
|
||||
//获取最近刚写入的几个值
|
||||
getHeads(size: number): { key: K; value: V }[] | undefined {
|
||||
const keyList = this.getKeyList();
|
||||
if (keyList.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const result: { key: K; value: V }[] = [];
|
||||
const listSize = Math.min(size, keyList.length);
|
||||
for (let i = 0; i < listSize; i++) {
|
||||
const key = keyList[listSize - i];
|
||||
result.push({ key, value: this.keyToValue.get(key)! });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageUniqueWrapper {
|
||||
private readonly msgDataMap: LimitedHashTable<string, number>;
|
||||
private readonly msgIdMap: LimitedHashTable<string, number>;
|
||||
|
||||
constructor(maxMap: number = 1000) {
|
||||
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
||||
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
|
||||
}
|
||||
|
||||
getRecentMsgIds(Peer: Peer, size: number): string[] {
|
||||
const heads = this.msgIdMap.getHeads(size);
|
||||
if (!heads) {
|
||||
return [];
|
||||
}
|
||||
const data = heads.map((t) => MessageUnique.getMsgIdAndPeerByShortId(t.value));
|
||||
const ret = data.filter((t) => t?.Peer.chatType === Peer.chatType && t?.Peer.peerUid === Peer.peerUid);
|
||||
return ret.map((t) => t?.MsgId).filter((t) => t !== undefined);
|
||||
}
|
||||
|
||||
createUniqueMsgId(peer: Peer, msgId: string) {
|
||||
const key = `${msgId}|${peer.chatType}|${peer.peerUid}`;
|
||||
const hash = crypto.createHash('md5').update(key).digest();
|
||||
//设置第一个bit为0 保证shortId为正数
|
||||
hash[0] &= 0x7f;
|
||||
const shortId = hash.readInt32BE(0);
|
||||
//减少性能损耗
|
||||
this.msgIdMap.set(msgId, shortId);
|
||||
this.msgDataMap.set(key, shortId);
|
||||
return shortId;
|
||||
}
|
||||
|
||||
getMsgIdAndPeerByShortId(shortId: number): { MsgId: string; Peer: Peer } | undefined {
|
||||
const data = this.msgDataMap.getKey(shortId);
|
||||
if (data) {
|
||||
const [msgId, chatTypeStr, peerUid] = data.split('|');
|
||||
const peer: Peer = {
|
||||
chatType: parseInt(chatTypeStr),
|
||||
peerUid,
|
||||
guildId: '',
|
||||
};
|
||||
return { MsgId: msgId, Peer: peer };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getShortIdByMsgId(msgId: string): number | undefined {
|
||||
return this.msgIdMap.getValue(msgId);
|
||||
}
|
||||
|
||||
getPeerByMsgId(msgId: string) {
|
||||
const shortId = this.msgIdMap.getValue(msgId);
|
||||
if (!shortId) return undefined;
|
||||
return this.getMsgIdAndPeerByShortId(shortId);
|
||||
}
|
||||
|
||||
resize(maxSize: number): void {
|
||||
this.msgIdMap.resize(maxSize);
|
||||
this.msgDataMap.resize(maxSize);
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();
|
||||
35
src/common/path.ts
Normal file
35
src/common/path.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
export class NapCatPathWrapper {
|
||||
binaryPath: string;
|
||||
logsPath: string;
|
||||
configPath: string;
|
||||
cachePath: string;
|
||||
staticPath: string;
|
||||
|
||||
constructor(mainPath: string = dirname(fileURLToPath(import.meta.url))) {
|
||||
this.binaryPath = mainPath;
|
||||
let writePath: string;
|
||||
if (os.platform() === 'darwin') {
|
||||
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
|
||||
} else {
|
||||
writePath = this.binaryPath;
|
||||
}
|
||||
this.logsPath = path.join(writePath, 'logs');
|
||||
this.configPath = path.join(writePath, 'config');
|
||||
this.cachePath = path.join(writePath, 'cache');
|
||||
this.staticPath = path.join(this.binaryPath, 'static');
|
||||
if (!fs.existsSync(this.logsPath)) {
|
||||
fs.mkdirSync(this.logsPath, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(this.configPath)) {
|
||||
fs.mkdirSync(this.configPath, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(this.cachePath)) {
|
||||
fs.mkdirSync(this.cachePath, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/common/proxy-handler.ts
Normal file
20
src/common/proxy-handler.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { LogWrapper } from '@/common/log';
|
||||
|
||||
export function proxyHandlerOf(logger: LogWrapper) {
|
||||
return {
|
||||
get(target: any, prop: any, receiver: any) {
|
||||
if (typeof target[prop] === 'undefined') {
|
||||
// 如果方法不存在,返回一个函数,这个函数调用existentMethod
|
||||
return (..._args: unknown[]) => {
|
||||
logger.logDebug(`${target.constructor.name} has no method ${prop}`);
|
||||
};
|
||||
}
|
||||
// 如果方法存在,正常返回
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
|
||||
return new Proxy<T>(listener, proxyHandlerOf(logger));
|
||||
}
|
||||
106
src/common/qq-basic-info.ts
Normal file
106
src/common/qq-basic-info.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import fs from 'node:fs';
|
||||
import { systemPlatform } from '@/common/system';
|
||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
|
||||
import AppidTable from '@/core/external/appid.json';
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import { getMajorPath } from '@/core';
|
||||
|
||||
export class QQBasicInfoWrapper {
|
||||
QQMainPath: string | undefined;
|
||||
QQPackageInfoPath: string | undefined;
|
||||
QQVersionConfigPath: string | undefined;
|
||||
isQuickUpdate: boolean | undefined;
|
||||
QQVersionConfig: QQVersionConfigType | undefined;
|
||||
QQPackageInfo: QQPackageInfoType | undefined;
|
||||
QQVersionAppid: string | undefined;
|
||||
QQVersionQua: string | undefined;
|
||||
context: { logger: LogWrapper };
|
||||
|
||||
constructor(context: { logger: LogWrapper }) {
|
||||
//基础目录获取
|
||||
this.context = context;
|
||||
this.QQMainPath = process.execPath;
|
||||
this.QQVersionConfigPath = getQQVersionConfigPath(this.QQMainPath);
|
||||
|
||||
|
||||
//基础信息获取 无快更则启用默认模板填充
|
||||
this.isQuickUpdate = !!this.QQVersionConfigPath;
|
||||
this.QQVersionConfig = this.isQuickUpdate
|
||||
? JSON.parse(fs.readFileSync(this.QQVersionConfigPath!).toString())
|
||||
: getDefaultQQVersionConfigInfo();
|
||||
|
||||
this.QQPackageInfoPath = getQQPackageInfoPath(this.QQMainPath, this.QQVersionConfig?.curVersion);
|
||||
this.QQPackageInfo = JSON.parse(fs.readFileSync(this.QQPackageInfoPath).toString());
|
||||
const { appid: IQQVersionAppid, qua: IQQVersionQua } = this.getAppidV2();
|
||||
this.QQVersionAppid = IQQVersionAppid;
|
||||
this.QQVersionQua = IQQVersionQua;
|
||||
}
|
||||
|
||||
//基础函数
|
||||
getQQBuildStr() {
|
||||
return this.isQuickUpdate ? this.QQVersionConfig?.buildId : this.QQPackageInfo?.buildVersion;
|
||||
}
|
||||
|
||||
getFullQQVesion() {
|
||||
const version = this.isQuickUpdate ? this.QQVersionConfig?.curVersion : this.QQPackageInfo?.version;
|
||||
if (!version) throw new Error('QQ版本获取失败');
|
||||
return version;
|
||||
}
|
||||
|
||||
requireMinNTQQBuild(buildStr: string) {
|
||||
const currentBuild = +(this.getQQBuildStr() ?? '0');
|
||||
if (currentBuild == 0) throw new Error('QQBuildStr获取失败');
|
||||
return currentBuild >= parseInt(buildStr);
|
||||
}
|
||||
|
||||
//此方法不要直接使用
|
||||
getQUAFallback() {
|
||||
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
|
||||
win32: `V1_WIN_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
darwin: `V1_MAC_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
linux: `V1_LNX_${this.getFullQQVesion()}_${this.getQQBuildStr()}_GW_B`,
|
||||
};
|
||||
return platformMapping[systemPlatform] ?? (platformMapping.win32)!;
|
||||
}
|
||||
|
||||
getAppIdFallback() {
|
||||
const platformMapping: Partial<Record<NodeJS.Platform, string>> = {
|
||||
win32: '537246092',
|
||||
darwin: '537246140',
|
||||
linux: '537246140',
|
||||
};
|
||||
return platformMapping[systemPlatform] ?? '537246092';
|
||||
}
|
||||
|
||||
getAppidV2(): { appid: string; qua: string } {
|
||||
// 通过已有表 性能好
|
||||
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
||||
const fullVersion = this.getFullQQVesion();
|
||||
if (fullVersion) {
|
||||
const data = appidTbale[fullVersion];
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
// 通过Major拉取 性能差
|
||||
try {
|
||||
const majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||
if (majorAppid) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||
}
|
||||
} catch (error) {
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
}
|
||||
// 最终兜底为老版本
|
||||
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||
}
|
||||
getAppidV2ByMajor(QQVersion: string) {
|
||||
const majorPath = getMajorPath(QQVersion);
|
||||
const appid = parseAppidFromMajor(majorPath);
|
||||
return appid;
|
||||
}
|
||||
|
||||
}
|
||||
135
src/common/request.ts
Normal file
135
src/common/request.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export class RequestUtil {
|
||||
// 适用于获取服务器下发cookies时获取,仅GET
|
||||
static async HttpsGetCookies(url: string): Promise<{ [key: string]: string }> {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = client.get(url, (res) => {
|
||||
const cookies: { [key: string]: string } = {};
|
||||
|
||||
res.on('data', () => { }); // Necessary to consume the stream
|
||||
res.on('end', () => {
|
||||
this.handleRedirect(res, url, cookies)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
if (res.headers['set-cookie']) {
|
||||
this.extractCookies(res.headers['set-cookie'], cookies);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static async handleRedirect(res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
if (res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, url);
|
||||
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
|
||||
// 合并重定向过程中的cookies
|
||||
return { ...cookies, ...redirectCookies };
|
||||
}
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
|
||||
setCookieHeaders.forEach((cookie) => {
|
||||
const parts = cookie.split(';')[0].split('=');
|
||||
const key = parts[0];
|
||||
const value = parts[1];
|
||||
if (key && value && key.length > 0 && value.length > 0) {
|
||||
cookies[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 请求和回复都是JSON data传原始内容 自动编码json
|
||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
||||
[key: string]: string
|
||||
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true): Promise<T> {
|
||||
const option = new URL(url);
|
||||
const protocol = url.startsWith('https://') ? https : http;
|
||||
const options = {
|
||||
hostname: option.hostname,
|
||||
port: option.port,
|
||||
path: option.pathname + option.search,
|
||||
method: method,
|
||||
headers: headers,
|
||||
};
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Content-Length': Buffer.byteLength(postData),
|
||||
// },
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = protocol.request(options, (res: any) => {
|
||||
let responseBody = '';
|
||||
res.on('data', (chunk: string | Buffer) => {
|
||||
responseBody += chunk.toString();
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
if (isJsonRet) {
|
||||
const responseJson = JSON.parse(responseBody);
|
||||
resolve(responseJson as T);
|
||||
} else {
|
||||
resolve(responseBody as T);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||
}
|
||||
} catch (parseError: unknown) {
|
||||
reject(new Error((parseError as Error).message));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
||||
if (isArgJson) {
|
||||
req.write(JSON.stringify(data));
|
||||
} else {
|
||||
req.write(data);
|
||||
}
|
||||
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// 请求返回都是原始内容
|
||||
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||
}
|
||||
|
||||
static async createFormData(boundary: string, filePath: string): Promise<Buffer> {
|
||||
let type = 'image/png';
|
||||
if (filePath.endsWith('.jpg')) {
|
||||
type = 'image/jpeg';
|
||||
}
|
||||
const formDataParts = [
|
||||
`------${boundary}\r\n`,
|
||||
`Content-Disposition: form-data; name="share_image"; filename="${filePath}"\r\n`,
|
||||
'Content-Type: ' + type + '\r\n\r\n',
|
||||
];
|
||||
|
||||
const fileContent = readFileSync(filePath);
|
||||
const footer = `\r\n------${boundary}--`;
|
||||
return Buffer.concat([
|
||||
Buffer.from(formDataParts.join(''), 'utf8'),
|
||||
fileContent,
|
||||
Buffer.from(footer, 'utf8'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
src/common/system.ts
Normal file
21
src/common/system.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
// 缓解Win7设备兼容性问题
|
||||
let osName: string;
|
||||
|
||||
try {
|
||||
osName = os.hostname();
|
||||
} catch (e) {
|
||||
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
|
||||
}
|
||||
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export const systemPlatform = os.platform();
|
||||
export const cpuArch = os.arch();
|
||||
export const systemVersion = os.release();
|
||||
export const hostname = osName;
|
||||
export const downloadsPath = path.join(homeDir, 'Downloads');
|
||||
export const systemName = os.type();
|
||||
17
src/common/types.ts
Normal file
17
src/common/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
//QQVersionType
|
||||
type QQPackageInfoType = {
|
||||
version: string;
|
||||
buildVersion: string;
|
||||
platform: string;
|
||||
eleArch: string;
|
||||
}
|
||||
type QQVersionConfigType = {
|
||||
baseVersion: string;
|
||||
curVersion: string;
|
||||
prevVersion: string;
|
||||
onErrorVersions: Array<any>;
|
||||
buildId: string;
|
||||
}
|
||||
type QQAppidTableType = {
|
||||
[key: string]: { appid: string, qua: string };
|
||||
}
|
||||
1
src/common/version.ts
Normal file
1
src/common/version.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const napCatVersion = '4.2.3';
|
||||
71
src/common/video.ts
Normal file
71
src/common/video.ts
Normal file
File diff suppressed because one or more lines are too long
15
src/core/adapters/NodeIDependsAdapter.ts
Normal file
15
src/core/adapters/NodeIDependsAdapter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { MsfChangeReasonType, MsfStatusType } from "@/core/types/adapter";
|
||||
|
||||
export class NodeIDependsAdapter {
|
||||
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
||||
|
||||
}
|
||||
|
||||
onMSFSsoError(args: unknown) {
|
||||
|
||||
}
|
||||
|
||||
getGroupCode(args: unknown) {
|
||||
|
||||
}
|
||||
}
|
||||
10
src/core/adapters/NodeIDispatcherAdapter.ts
Normal file
10
src/core/adapters/NodeIDispatcherAdapter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class NodeIDispatcherAdapter {
|
||||
dispatchRequest(arg: unknown) {
|
||||
}
|
||||
|
||||
dispatchCall(arg: unknown) {
|
||||
}
|
||||
|
||||
dispatchCallWithJson(arg: unknown) {
|
||||
}
|
||||
}
|
||||
25
src/core/adapters/NodeIGlobalAdapter.ts
Normal file
25
src/core/adapters/NodeIGlobalAdapter.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export class NodeIGlobalAdapter {
|
||||
onLog(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onGetSrvCalTime(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onShowErrUITips(...args: unknown[]) {
|
||||
}
|
||||
|
||||
fixPicImgType(...args: unknown[]) {
|
||||
}
|
||||
|
||||
getAppSetting(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onInstallFinished(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onUpdateGeneralFlag(...args: unknown[]) {
|
||||
}
|
||||
|
||||
onGetOfflineMsg(...args: unknown[]) {
|
||||
}
|
||||
}
|
||||
3
src/core/adapters/index.ts
Normal file
3
src/core/adapters/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './NodeIDependsAdapter';
|
||||
export * from './NodeIDispatcherAdapter';
|
||||
export * from './NodeIGlobalAdapter';
|
||||
60
src/core/apis/collection.ts
Normal file
60
src/core/apis/collection.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { InstanceContext, NapCatCore } from '@/core';
|
||||
|
||||
export class NTQQCollectionApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async createCollection(authorUin: string, authorUid: string, authorName: string, brief: string, rawData: string) {
|
||||
return this.context.session.getCollectionService().createNewCollectionItem({
|
||||
commInfo: {
|
||||
bid: 1,
|
||||
category: 2,
|
||||
author: {
|
||||
type: 1,
|
||||
numId: authorUin,
|
||||
strId: authorName,
|
||||
groupId: '0',
|
||||
groupName: '',
|
||||
uid: authorUid,
|
||||
},
|
||||
customGroupId: '0',
|
||||
createTime: Date.now().toString(),
|
||||
sequence: Date.now().toString(),
|
||||
},
|
||||
richMediaSummary: {
|
||||
originalUri: '',
|
||||
publisher: '',
|
||||
richMediaVersion: 0,
|
||||
subTitle: '',
|
||||
title: '',
|
||||
brief: brief,
|
||||
picList: [],
|
||||
contentType: 1,
|
||||
},
|
||||
richMediaContent: {
|
||||
rawData: rawData,
|
||||
bizDataList: [],
|
||||
picList: [],
|
||||
fileList: [],
|
||||
},
|
||||
need_share_url: false,
|
||||
});
|
||||
}
|
||||
|
||||
async getAllCollection(category: number = 0, count: number = 50) {
|
||||
return this.context.session.getCollectionService().getCollectionItemList({
|
||||
category: category,
|
||||
groupId: -1,
|
||||
forceSync: true,
|
||||
forceFromDb: false,
|
||||
timeStamp: '0',
|
||||
count: count,
|
||||
searchDown: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
491
src/core/apis/file.ts
Normal file
491
src/core/apis/file.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import {
|
||||
ChatType,
|
||||
ElementType,
|
||||
IMAGE_HTTP_HOST,
|
||||
IMAGE_HTTP_HOST_NT,
|
||||
Peer,
|
||||
PicElement,
|
||||
PicSubType,
|
||||
PicType,
|
||||
RawMessage,
|
||||
SendFileElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
SendVideoElement,
|
||||
} from '@/core/types';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fsPromises from 'fs/promises';
|
||||
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
||||
import * as fileType from 'file-type';
|
||||
import imageSize from 'image-size';
|
||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
||||
import { RkeyManager } from '@/core/helper/rkey';
|
||||
import { calculateFileMD5 } from '@/common/file';
|
||||
import pathLib from 'node:path';
|
||||
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { encodeSilk } from '@/common/audio';
|
||||
import { SendMessageContext } from '@/onebot/api';
|
||||
import { getFileTypeForSendType } from '../helper/msg';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
rkeyManager: RkeyManager;
|
||||
packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
],
|
||||
this.context.logger
|
||||
);
|
||||
}
|
||||
|
||||
async copyFile(filePath: string, destPath: string) {
|
||||
await this.core.util.copyFile(filePath, destPath);
|
||||
}
|
||||
|
||||
async getFileSize(filePath: string): Promise<number> {
|
||||
return await this.core.util.getFileSize(filePath);
|
||||
}
|
||||
|
||||
async getVideoUrl(peer: Peer, msgId: string, elementId: string) {
|
||||
return (await this.context.session.getRichMediaService().getVideoPlayUrlV2(peer, msgId, elementId, 0, {
|
||||
downSourceType: 1,
|
||||
triggerType: 1,
|
||||
})).urlResult.domainUrl;
|
||||
}
|
||||
|
||||
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||
const fileMd5 = await calculateFileMD5(filePath);
|
||||
const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||
let fileName = `${path.basename(filePath)}`;
|
||||
if (fileName.indexOf('.') === -1) {
|
||||
fileName += ext;
|
||||
}
|
||||
|
||||
const mediaPath = this.context.session.getMsgService().getRichMediaFilePathForGuild({
|
||||
md5HexStr: fileMd5,
|
||||
fileName: fileName,
|
||||
elementType: elementType,
|
||||
elementSubType,
|
||||
thumbSize: 0,
|
||||
needCreate: true,
|
||||
downloadType: 1,
|
||||
file_uuid: '',
|
||||
});
|
||||
|
||||
await this.copyFile(filePath, mediaPath);
|
||||
const fileSize = await this.getFileSize(filePath);
|
||||
return {
|
||||
md5: fileMd5,
|
||||
fileName,
|
||||
path: mediaPath,
|
||||
fileSize,
|
||||
ext,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendFileElement(context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
fileSize,
|
||||
} = await this.core.apis.FileApi.uploadFile(filePath, ElementType.FILE);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.FILE,
|
||||
elementId: '',
|
||||
fileElement: {
|
||||
fileName: fileName || _fileName,
|
||||
folderId: folderId,
|
||||
filePath: path,
|
||||
fileSize: fileSize.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement(context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
return {
|
||||
elementType: ElementType.PIC,
|
||||
elementId: '',
|
||||
picElement: {
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
picWidth: imageSize.width,
|
||||
picHeight: imageSize.height,
|
||||
fileName: fileName,
|
||||
sourcePath: path,
|
||||
original: true,
|
||||
picType: await getFileTypeForSendType(picPath),
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
thumbFileSize: 0,
|
||||
summary,
|
||||
} as PicElement,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
const logger = this.core.context.logger;
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
time: 15,
|
||||
format: 'mp4',
|
||||
size: 0,
|
||||
filePath,
|
||||
};
|
||||
try {
|
||||
videoInfo = await getVideoInfo(filePath, logger);
|
||||
} catch (e) {
|
||||
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
|
||||
}
|
||||
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
|
||||
}
|
||||
const newFilePath = filePath + '.' + fileExt;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
context.deleteAfterSentFiles.push(newFilePath);
|
||||
filePath = newFilePath;
|
||||
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
videoInfo.size = fileSize;
|
||||
let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
|
||||
thumb = pathLib.dirname(thumb);
|
||||
|
||||
const thumbPath = new Map();
|
||||
const _thumbPath = await new Promise<string | undefined>((resolve, reject) => {
|
||||
const thumbFileName = `${md5}_0.png`;
|
||||
const thumbPath = pathLib.join(thumb, thumbFileName);
|
||||
ffmpeg(filePath)
|
||||
.on('error', (err) => {
|
||||
try {
|
||||
logger.logDebug('获取视频封面失败,使用默认封面', err);
|
||||
if (diyThumbPath) {
|
||||
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
||||
resolve(thumbPath);
|
||||
}).catch(reject);
|
||||
} else {
|
||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||
resolve(thumbPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
|
||||
}
|
||||
})
|
||||
.screenshots({
|
||||
timestamps: [0],
|
||||
filename: thumbFileName,
|
||||
folder: thumb,
|
||||
size: videoInfo.width + 'x' + videoInfo.height,
|
||||
})
|
||||
.on('end', () => {
|
||||
resolve(thumbPath);
|
||||
});
|
||||
});
|
||||
const thumbSize = _thumbPath ? (await fsPromises.stat(_thumbPath)).size : 0;
|
||||
thumbPath.set(0, _thumbPath);
|
||||
const thumbMd5 = _thumbPath ? await calculateFileMD5(_thumbPath) : '';
|
||||
context.deleteAfterSentFiles.push(path);
|
||||
const uploadName = (fileName || _fileName).toLocaleLowerCase().endsWith('.' + fileExt.toLocaleLowerCase()) ? (fileName || _fileName) : (fileName || _fileName) + '.' + fileExt;
|
||||
return {
|
||||
elementType: ElementType.VIDEO,
|
||||
elementId: '',
|
||||
videoElement: {
|
||||
fileName: uploadName,
|
||||
filePath: path,
|
||||
videoMd5: md5,
|
||||
thumbMd5,
|
||||
fileTime: videoInfo.time,
|
||||
thumbPath: thumbPath,
|
||||
thumbSize,
|
||||
thumbWidth: videoInfo.width,
|
||||
thumbHeight: videoInfo.height,
|
||||
fileSize: '' + fileSize,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||
}
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(silkPath, ElementType.PTT);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch(
|
||||
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
|
||||
);
|
||||
}
|
||||
return {
|
||||
elementType: ElementType.PTT,
|
||||
elementId: '',
|
||||
pttElement: {
|
||||
fileName: fileName,
|
||||
filePath: path,
|
||||
md5HexStr: md5,
|
||||
fileSize: fileSize.toString(),
|
||||
duration: duration ?? 1,
|
||||
formatType: 1,
|
||||
voiceType: 1,
|
||||
voiceChangeType: 0,
|
||||
canConvert2Text: true,
|
||||
waveAmplitudes: [
|
||||
0, 18, 9, 23, 16, 17, 16, 15, 44, 17, 24, 20, 14, 15, 17,
|
||||
],
|
||||
fileSubId: '',
|
||||
playState: 1,
|
||||
autoConvertText: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async downloadFileForModelId(peer: Peer, modelId: string, unknown: string, timeout = 1000 * 60 * 2) {
|
||||
const [, fileTransNotifyInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelRichMediaService/downloadFileForModelId',
|
||||
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
|
||||
[peer, [modelId], unknown],
|
||||
() => true,
|
||||
(arg) => arg?.commonFileInfo?.fileModelId === modelId,
|
||||
1,
|
||||
timeout,
|
||||
);
|
||||
return fileTransNotifyInfo.filePath;
|
||||
}
|
||||
|
||||
async downloadRawMsgMedia(msg: RawMessage[]) {
|
||||
const res = await Promise.all(
|
||||
msg.map(m =>
|
||||
Promise.all(
|
||||
m.elements
|
||||
.filter(element =>
|
||||
element.elementType === ElementType.PIC ||
|
||||
element.elementType === ElementType.VIDEO ||
|
||||
element.elementType === ElementType.PTT ||
|
||||
element.elementType === ElementType.FILE
|
||||
)
|
||||
.map(element =>
|
||||
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
msg.forEach((m, msgIndex) => {
|
||||
const elementResults = res[msgIndex];
|
||||
let elementIndex = 0;
|
||||
m.elements.forEach(element => {
|
||||
if (
|
||||
element.elementType === ElementType.PIC ||
|
||||
element.elementType === ElementType.VIDEO ||
|
||||
element.elementType === ElementType.PTT ||
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
||||
// 用于下载文件
|
||||
if (sourcePath && fs.existsSync(sourcePath)) {
|
||||
if (force) {
|
||||
try {
|
||||
await fsPromises.unlink(sourcePath);
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
} else {
|
||||
return sourcePath;
|
||||
}
|
||||
}
|
||||
const [, completeRetData] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/downloadRichMedia',
|
||||
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
|
||||
[{
|
||||
fileModelId: '0',
|
||||
downloadSourceType: 0,
|
||||
triggerType: 1,
|
||||
msgId: msgId,
|
||||
chatType: chatType,
|
||||
peerUid: peerUid,
|
||||
elementId: elementId,
|
||||
thumbSize: 0,
|
||||
downloadType: 1,
|
||||
filePath: thumbPath,
|
||||
}],
|
||||
() => true,
|
||||
(arg) => arg.msgElementId === elementId && arg.msgId === msgId,
|
||||
1,
|
||||
timeout,
|
||||
);
|
||||
return completeRetData.filePath;
|
||||
}
|
||||
|
||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
imageSize(filePath, (err: Error | null, dimensions) => {
|
||||
if (err) {
|
||||
reject(new Error(err.message));
|
||||
} else if (!dimensions) {
|
||||
reject(new Error('获取图片尺寸失败'));
|
||||
} else {
|
||||
resolve(dimensions);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
|
||||
const randomResultId = 100000 + Math.floor(Math.random() * 10000);
|
||||
let searchId = 0;
|
||||
const [, searchResult] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelFileAssistantService/searchFile',
|
||||
'NodeIKernelFileAssistantListener/onFileSearch',
|
||||
[
|
||||
keys,
|
||||
{ resultType: 2, pageLimit: 1 },
|
||||
randomResultId,
|
||||
],
|
||||
(ret) => {
|
||||
searchId = ret;
|
||||
return true;
|
||||
},
|
||||
result => result.searchId === searchId && result.resultId === randomResultId,
|
||||
);
|
||||
return searchResult.resultItems[0];
|
||||
}
|
||||
|
||||
async downloadFileById(
|
||||
fileId: string,
|
||||
fileSize: number = 1024576,
|
||||
estimatedTime: number = (fileSize * 1000 / 1024576) + 5000,
|
||||
) {
|
||||
const [, fileData] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelFileAssistantService/downloadFile',
|
||||
'NodeIKernelFileAssistantListener/onFileStatusChanged',
|
||||
[[fileId]],
|
||||
ret => ret.result === 0,
|
||||
status => status.fileStatus === 2 && status.fileProgress === '0',
|
||||
1,
|
||||
estimatedTime, // estimate 1MB/s
|
||||
);
|
||||
return fileData.filePath!;
|
||||
}
|
||||
|
||||
async getImageUrl(element: PicElement): Promise<string> {
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const url: string = element.originImageUrl ?? '';
|
||||
|
||||
const md5HexStr = element.md5HexStr;
|
||||
const fileMd5 = element.md5HexStr;
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
if (url && isNTV2 && imageFileId) {
|
||||
const rkeyData = await this.getRkeyData();
|
||||
return this.getImageUrlFromParsedUrl(imageFileId, imageAppid, rkeyData);
|
||||
}
|
||||
return this.getImageUrlFromMd5(fileMd5, md5HexStr);
|
||||
}
|
||||
|
||||
private async getRkeyData() {
|
||||
const rkeyData = {
|
||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||
online_rkey: false
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.core.apis.PacketApi.available) {
|
||||
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||
if (rkey_expired_private || rkey_expired_group) {
|
||||
this.packetRkey = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||
}
|
||||
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||
rkeyData.online_rkey = true;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||
}
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
try {
|
||||
const tempRkeyData = await this.rkeyManager.getRkey();
|
||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
|
||||
return rkeyData;
|
||||
}
|
||||
|
||||
private getImageUrlFromParsedUrl(imageFileId: string, appid: string, rkeyData: any): string {
|
||||
const rkey = appid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
if (rkeyData.online_rkey) {
|
||||
return IMAGE_HTTP_HOST_NT + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {
|
||||
if (fileMd5 || md5HexStr) {
|
||||
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr ?? '').toUpperCase()}/0`;
|
||||
}
|
||||
|
||||
this.context.logger.logDebug('图片url获取失败', { fileMd5, md5HexStr });
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
95
src/core/apis/friend.ts
Normal file
95
src/core/apis/friend.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { FriendV2 } from '@/core/types';
|
||||
import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
|
||||
export class NTQQFriendApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
async setBuddyRemark(uid: string, remark: string) {
|
||||
return this.context.session.getBuddyService().setBuddyRemark({ uid, remark });
|
||||
}
|
||||
async getBuddyV2SimpleInfoMap(refresh = false) {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
}
|
||||
|
||||
async getBuddy(refresh = false): Promise<FriendV2[]> {
|
||||
return Array.from((await this.getBuddyV2SimpleInfoMap(refresh)).values());
|
||||
}
|
||||
|
||||
async getBuddyIdMap(refresh = false): Promise<LimitedHashTable<string, string>> {
|
||||
const retMap: LimitedHashTable<string, string> = new LimitedHashTable<string, string>(5000);
|
||||
const data = await this.getBuddyV2SimpleInfoMap(refresh);
|
||||
data.forEach((value) => retMap.set(value.uin!, value.uid!));
|
||||
return retMap;
|
||||
}
|
||||
async delBuudy(uid: string, tempBlock = false, tempBothDel = false) {
|
||||
return this.context.session.getBuddyService().delBuddy({
|
||||
friendUid: uid,
|
||||
tempBlock: tempBlock,
|
||||
tempBothDel: tempBothDel
|
||||
});
|
||||
}
|
||||
async getBuddyV2ExWithCate() {
|
||||
const buddyService = this.context.session.getBuddyService();
|
||||
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||
const uids = buddyListV2.flatMap(item => {
|
||||
return item.buddyUids;
|
||||
});
|
||||
const data = await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
return buddyListV2.map(category => ({
|
||||
categoryId: category.categoryId,
|
||||
categorySortId: category.categorySortId,
|
||||
categoryName: category.categroyName,
|
||||
categoryMbCount: category.categroyMbCount,
|
||||
onlineCount: category.onlineCount,
|
||||
buddyList: category.buddyUids.map(uid => data.get(uid)).filter(value => !!value),
|
||||
}));
|
||||
}
|
||||
|
||||
async isBuddy(uid: string) {
|
||||
return this.context.session.getBuddyService().isBuddy(uid);
|
||||
}
|
||||
|
||||
async clearBuddyReqUnreadCnt() {
|
||||
return this.context.session.getBuddyService().clearBuddyReqUnreadCnt();
|
||||
}
|
||||
|
||||
async getBuddyReq() {
|
||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelBuddyService/getBuddyReq',
|
||||
'NodeIKernelBuddyListener/onBuddyReqChange',
|
||||
[],
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async handleFriendRequest(flag: string, accept: boolean) {
|
||||
const data = flag.split('|');
|
||||
if (data.length < 2) {
|
||||
return;
|
||||
}
|
||||
const friendUid = data[0];
|
||||
const reqTime = data[1];
|
||||
this.context.session.getBuddyService()?.approvalFriendRequest({
|
||||
friendUid: friendUid,
|
||||
reqTime: reqTime,
|
||||
accept,
|
||||
});
|
||||
}
|
||||
}
|
||||
524
src/core/apis/group.ts
Normal file
524
src/core/apis/group.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
import {
|
||||
GeneralCallResult,
|
||||
Group,
|
||||
GroupMember,
|
||||
NTGroupMemberRole,
|
||||
NTGroupRequestOperateTypes,
|
||||
InstanceContext,
|
||||
KickMemberV2Req,
|
||||
MemberExtSourceType,
|
||||
NapCatCore,
|
||||
} from '@/core';
|
||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
|
||||
export class NTQQGroupApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
groupCache: Map<string, Group> = new Map<string, Group>();
|
||||
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
|
||||
groups: Group[] = [];
|
||||
essenceLRU = new LimitedHashTable<number, string>(1000);
|
||||
session: any;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
async initApi() {
|
||||
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
}
|
||||
async initCache() {
|
||||
this.groups = await this.getGroups();
|
||||
for (const group of this.groups) {
|
||||
this.groupCache.set(group.groupCode, group);
|
||||
}
|
||||
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
|
||||
// process.pid 调试点
|
||||
}
|
||||
|
||||
async getCoreAndBaseInfo(uids: string[]) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
}
|
||||
|
||||
async fetchGroupEssenceList(groupCode: string) {
|
||||
const pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||||
return this.context.session.getGroupService().fetchGroupEssenceList({
|
||||
groupCode: groupCode,
|
||||
pageStart: 0,
|
||||
pageLimit: 300,
|
||||
}, pskey);
|
||||
}
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||
return (await data)[1];
|
||||
}
|
||||
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
||||
}
|
||||
|
||||
async setGroupAvatar(gc: string, filePath: string) {
|
||||
return this.context.session.getGroupService().setHeader(gc, filePath);
|
||||
}
|
||||
|
||||
async getGroups(forced = false) {
|
||||
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getGroupList',
|
||||
'NodeIKernelGroupListener/onGroupListUpdate',
|
||||
[forced],
|
||||
);
|
||||
return groupList;
|
||||
}
|
||||
|
||||
async getGroupExtFE0Info(groupCode: string[], forced = true) {
|
||||
return this.context.session.getGroupService().getGroupExt0xEF0Info(
|
||||
groupCode,
|
||||
[],
|
||||
{
|
||||
bindGuildId: 1,
|
||||
blacklistExpireTime: 1,
|
||||
companyId: 1,
|
||||
essentialMsgPrivilege: 1,
|
||||
essentialMsgSwitch: 1,
|
||||
fullGroupExpansionSeq: 1,
|
||||
fullGroupExpansionSwitch: 1,
|
||||
gangUpId: 1,
|
||||
groupAioBindGuildId: 1,
|
||||
groupBindGuildIds: 1,
|
||||
groupBindGuildSwitch: 1,
|
||||
groupExcludeGuildIds: 1,
|
||||
groupExtFlameData: 1,
|
||||
groupFlagPro1: 1,
|
||||
groupInfoExtSeq: 1,
|
||||
groupOwnerId: 1,
|
||||
groupSquareSwitch: 1,
|
||||
hasGroupCustomPortrait: 1,
|
||||
inviteRobotMemberExamine: 1,
|
||||
inviteRobotMemberSwitch: 1,
|
||||
inviteRobotSwitch: 1,
|
||||
isLimitGroupRtc: 1,
|
||||
lightCharNum: 1,
|
||||
luckyWord: 1,
|
||||
luckyWordId: 1,
|
||||
msgEventSeq: 1,
|
||||
qqMusicMedalSwitch: 1,
|
||||
reserve: 1,
|
||||
showPlayTogetherSwitch: 1,
|
||||
starId: 1,
|
||||
todoSeq: 1,
|
||||
viewedMsgDisappearTime: 1,
|
||||
},
|
||||
forced,
|
||||
);
|
||||
}
|
||||
|
||||
async getGroup(groupCode: string, forced = false) {
|
||||
let group = this.groupCache.get(groupCode.toString());
|
||||
if (!group) {
|
||||
try {
|
||||
const groupList = await this.getGroups(forced);
|
||||
if (groupList.length) {
|
||||
groupList.forEach(g => {
|
||||
this.groupCache.set(g.groupCode, g);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
group = this.groupCache.get(groupCode.toString());
|
||||
return group;
|
||||
}
|
||||
|
||||
async getGroupMemberAll(groupCode: string, forced = false) {
|
||||
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
||||
}
|
||||
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
const groupCodeStr = groupCode.toString();
|
||||
const memberUinOrUidStr = memberUinOrUid.toString();
|
||||
let members = this.groupMemberCache.get(groupCodeStr);
|
||||
if (!members) {
|
||||
try {
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
this.groupMemberCache.set(groupCodeStr, members);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getMember() {
|
||||
let member: GroupMember | undefined;
|
||||
if (isNumeric(memberUinOrUidStr)) {
|
||||
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
|
||||
} else {
|
||||
member = members!.get(memberUinOrUidStr);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
let member = getMember();
|
||||
if (!member) {
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
member = getMember();
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
async getGroupRecommendContactArkJson(groupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
|
||||
}
|
||||
|
||||
async CreatGroupFileFolder(groupCode: string, folderName: string) {
|
||||
return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName);
|
||||
}
|
||||
|
||||
async DelGroupFile(groupCode: string, files: string[]) {
|
||||
return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files);
|
||||
}
|
||||
|
||||
async DelGroupFileFolder(groupCode: string, folderId: string) {
|
||||
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
|
||||
}
|
||||
|
||||
async addGroupEssence(GroupCode: string, msgId: string) {
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
guildId: '',
|
||||
peerUid: GroupCode,
|
||||
}, msgId, 1, false);
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||
};
|
||||
return this.context.session.getGroupService().addGroupEssence(param);
|
||||
}
|
||||
|
||||
async kickMemberV2Inner(param: KickMemberV2Req) {
|
||||
return this.context.session.getGroupService().kickMemberV2(param);
|
||||
}
|
||||
|
||||
async deleteGroupBulletin(GroupCode: string, noticeId: string) {
|
||||
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||||
return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId);
|
||||
}
|
||||
|
||||
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
needDeleteLocalMsg: needDeleteLocalMsg,
|
||||
};
|
||||
return this.context.session.getGroupService().quitGroupV2(param);
|
||||
}
|
||||
|
||||
async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) {
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
msgRandom: parseInt(msgRandom),
|
||||
msgSeq: parseInt(msgSeq),
|
||||
};
|
||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
||||
}
|
||||
|
||||
async removeGroupEssence(GroupCode: string, msgId: string) {
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
guildId: '',
|
||||
peerUid: GroupCode,
|
||||
}, msgId, 1, false);
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||
};
|
||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
||||
}
|
||||
|
||||
async getSingleScreenNotifies(doubt: boolean, num: number) {
|
||||
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getSingleScreenNotifies',
|
||||
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
|
||||
[
|
||||
doubt,
|
||||
'',
|
||||
num,
|
||||
],
|
||||
);
|
||||
return notifies;
|
||||
}
|
||||
|
||||
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
|
||||
const Listener = this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
(params, _, members) => params === GroupCode && members.size > 0,
|
||||
1,
|
||||
forced ? 5000 : 250,
|
||||
);
|
||||
const retData = await (
|
||||
this.core.eventWrapper
|
||||
.createEventFunction('NodeIKernelGroupService/getMemberInfo')
|
||||
)!(GroupCode, [uid], forced);
|
||||
if (retData.result !== 0) {
|
||||
throw new Error(`${retData.errMsg}`);
|
||||
}
|
||||
const result = await Listener as unknown;
|
||||
let member: GroupMember | undefined;
|
||||
if (Array.isArray(result) && result?.[2] instanceof Map) {
|
||||
const members = result[2] as Map<string, GroupMember>;
|
||||
member = members.get(uid);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
async searchGroup(groupCode: string) {
|
||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelSearchService/searchGroup',
|
||||
'NodeIKernelSearchListener/onSearchGroupResult',
|
||||
[{
|
||||
keyWords: groupCode,
|
||||
groupNum: 25,
|
||||
exactSearch: false,
|
||||
penetrate: ''
|
||||
}],
|
||||
(ret) => ret.result === 0,
|
||||
(params) => !!params.groupInfos.find(g => g.groupCode === groupCode),
|
||||
1,
|
||||
5000
|
||||
);
|
||||
return ret.groupInfos.find(g => g.groupCode === groupCode);
|
||||
}
|
||||
|
||||
async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
|
||||
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
|
||||
return eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getMemberInfo',
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
[GroupCode, [uid], forced],
|
||||
(ret) => ret.result === 0,
|
||||
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||
1,
|
||||
forced ? 2500 : 250
|
||||
);
|
||||
}, this.core.eventWrapper, GroupCode, uid, forced);
|
||||
if (data && data[3] instanceof Map && data[3].has(uid)) {
|
||||
return data[3].get(uid);
|
||||
}
|
||||
if (retry > 0) {
|
||||
const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
|
||||
if (trydata) return trydata;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (modeListener) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
};
|
||||
}
|
||||
|
||||
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
listenerMode: boolean;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (result.result.finish && result.result.infos.size === 0) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
listenerMode: resMode2?.hasNext !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
|
||||
if (no_cache) {
|
||||
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
|
||||
}
|
||||
let res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
let ret = res.infos;
|
||||
if (res.infos.size === 0 && !res.listenerMode) {
|
||||
res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
ret = res.infos;
|
||||
}
|
||||
if (res.infos.size === 0) {
|
||||
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
const groupService = this.context.session.getGroupService();
|
||||
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
||||
const result = await groupService.getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`);
|
||||
return result.result.infos;
|
||||
}
|
||||
|
||||
async getGroupFileCount(group_ids: Array<string>) {
|
||||
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
|
||||
}
|
||||
|
||||
async getArkJsonGroupShare(GroupCode: string) {
|
||||
const ret = await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
|
||||
GroupCode,
|
||||
) as GeneralCallResult & { arkJson: string };
|
||||
return ret.arkJson;
|
||||
}
|
||||
|
||||
//需要异常处理
|
||||
async uploadGroupBulletinPic(GroupCode: string, imageurl: string) {
|
||||
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||||
return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
|
||||
}
|
||||
|
||||
async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
|
||||
const flagitem = flag.split('|');
|
||||
const groupCode = flagitem[0];
|
||||
const seq = flagitem[1];
|
||||
const type = parseInt(flagitem[2]);
|
||||
|
||||
return this.context.session.getGroupService().operateSysNotify(
|
||||
false,
|
||||
{
|
||||
operateType: operateType,
|
||||
targetMsg: {
|
||||
seq: seq, // 通知序列号
|
||||
type: type,
|
||||
groupCode: groupCode,
|
||||
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async quitGroup(groupQQ: string) {
|
||||
return this.context.session.getGroupService().quitGroup(groupQQ);
|
||||
}
|
||||
|
||||
async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
|
||||
return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason);
|
||||
}
|
||||
|
||||
async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
|
||||
// timeStamp为秒数, 0为解除禁言
|
||||
return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList);
|
||||
}
|
||||
|
||||
async banGroup(groupQQ: string, shutUp: boolean) {
|
||||
return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp);
|
||||
}
|
||||
|
||||
async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
|
||||
return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
|
||||
}
|
||||
|
||||
async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
|
||||
return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
|
||||
}
|
||||
|
||||
async setGroupName(groupQQ: string, groupName: string) {
|
||||
return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
|
||||
}
|
||||
|
||||
async publishGroupBulletin(groupQQ: string, content: string, picInfo: {
|
||||
id: string,
|
||||
width: number,
|
||||
height: number
|
||||
} | undefined = undefined, pinned: number = 0, confirmRequired: number = 0) {
|
||||
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com');
|
||||
//text是content内容url编码
|
||||
const data = {
|
||||
text: encodeURI(content),
|
||||
picInfo: picInfo,
|
||||
oldFeedsId: '',
|
||||
pinned: pinned,
|
||||
confirmRequired: confirmRequired,
|
||||
};
|
||||
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
|
||||
}
|
||||
|
||||
async getGroupRemainAtTimes(GroupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
||||
}
|
||||
|
||||
async getMemberExtInfo(groupCode: string, uin: string) {
|
||||
return this.context.session.getGroupService().getMemberExtInfo(
|
||||
{
|
||||
groupCode: groupCode,
|
||||
sourceType: MemberExtSourceType.TITLETYPE,
|
||||
beginUin: '0',
|
||||
dataTime: '0',
|
||||
uinList: [uin],
|
||||
uinNum: '',
|
||||
seq: '',
|
||||
groupType: '',
|
||||
richCardNameVer: '',
|
||||
memberExtFilter: {
|
||||
memberLevelInfoUin: 1,
|
||||
memberLevelInfoPoint: 1,
|
||||
memberLevelInfoActiveDay: 1,
|
||||
memberLevelInfoLevel: 1,
|
||||
memberLevelInfoName: 1,
|
||||
levelName: 1,
|
||||
dataTime: 1,
|
||||
userShowFlag: 1,
|
||||
sysShowFlag: 1,
|
||||
timeToUpdate: 1,
|
||||
nickName: 1,
|
||||
specialTitle: 1,
|
||||
levelNameNew: 1,
|
||||
userShowFlagNew: 1,
|
||||
msgNeedField: 1,
|
||||
cmdUinFlagExt3Grocery: 1,
|
||||
memberIcon: 1,
|
||||
memberInfoSeq: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/core/apis/index.ts
Normal file
7
src/core/apis/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './file';
|
||||
export * from './friend';
|
||||
export * from './group';
|
||||
export * from './msg';
|
||||
export * from './user';
|
||||
export * from './webapi';
|
||||
export * from './system';
|
||||
285
src/core/apis/msg.ts
Normal file
285
src/core/apis/msg.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types';
|
||||
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore } from '@/core';
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
|
||||
export class NTQQMsgApi {
|
||||
|
||||
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
|
||||
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||
}
|
||||
async getAioFirstViewLatestMsgs(peer: Peer, MsgCount: number) {
|
||||
return this.context.session.getMsgService().getAioFirstViewLatestMsgs(peer, MsgCount);
|
||||
}
|
||||
|
||||
async sendShowInputStatusReq(peer: Peer, eventType: number) {
|
||||
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
||||
}
|
||||
|
||||
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
|
||||
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
||||
}
|
||||
|
||||
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
||||
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
||||
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
||||
}
|
||||
|
||||
async setEmojiLike(peer: Peer, msgSeq: string, emojiId: string, set: boolean = true) {
|
||||
emojiId = emojiId.toString();
|
||||
return this.context.session.getMsgService().setMsgEmojiLikes(peer, msgSeq, emojiId, emojiId.length > 3 ? '2' : '1', set);
|
||||
}
|
||||
|
||||
async getMultiMsg(peer: Peer, rootMsgId: string, parentMsgId: string): Promise<GeneralCallResult & {
|
||||
msgList: RawMessage[]
|
||||
} | undefined> {
|
||||
return this.context.session.getMsgService().getMultiMsg(peer, rootMsgId, parentMsgId);
|
||||
}
|
||||
|
||||
async ForwardMsg(peer: Peer, msgIds: string[]) {
|
||||
return this.context.session.getMsgService().forwardMsg(msgIds, peer, [peer], new Map());
|
||||
}
|
||||
|
||||
async getMsgsByMsgId(peer: Peer | undefined, msgIds: string[] | undefined) {
|
||||
if (!peer) throw new Error('peer is not allowed');
|
||||
if (!msgIds) throw new Error('msgIds is not allowed');
|
||||
//MliKiowa: 参数不合规会导致NC异常崩溃 原因是TX未对进入参数判断 对应Android标记@NotNull AndroidJADX分析可得
|
||||
return await this.context.session.getMsgService().getMsgsByMsgId(peer, msgIds);
|
||||
}
|
||||
|
||||
async getSingleMsg(peer: Peer, seq: string) {
|
||||
return await this.context.session.getMsgService().getSingleMsg(peer, seq);
|
||||
}
|
||||
|
||||
async fetchFavEmojiList(num: number) {
|
||||
return this.context.session.getMsgService().fetchFavEmojiList('', num, true, true);
|
||||
}
|
||||
|
||||
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: [],
|
||||
filterMsgToTime: '0',
|
||||
filterMsgFromTime: '0',
|
||||
isReverseOrder: false,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async queryMsgsWithFilterExWithSeqV2(peer: Peer, msgSeq: string, MsgTime: string, SendersUid: string[]) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: SendersUid,
|
||||
filterMsgToTime: MsgTime,
|
||||
filterMsgFromTime: MsgTime,
|
||||
isReverseOrder: false,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: SendersUid,
|
||||
filterMsgToTime: '0',
|
||||
filterMsgFromTime: '0',
|
||||
isReverseOrder: false,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,
|
||||
filterMsgType: [],
|
||||
filterSendersUid: [],
|
||||
filterMsgToTime: '0',
|
||||
filterMsgFromTime: '0',
|
||||
isReverseOrder: true,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 1,
|
||||
});
|
||||
}
|
||||
// 客户端还在用别慌
|
||||
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
|
||||
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
|
||||
}
|
||||
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
||||
const DateNow = Math.floor(Date.now() / 1000);
|
||||
const filterMsgFromTime = (DateNow - 300).toString();
|
||||
const filterMsgToTime = DateNow.toString();
|
||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
|
||||
filterMsgType: [],
|
||||
filterSendersUid: [],
|
||||
filterMsgToTime: filterMsgToTime,
|
||||
filterMsgFromTime: filterMsgFromTime,
|
||||
isReverseOrder: false,
|
||||
isIncludeCurrent: true,
|
||||
pageLimit: 100,
|
||||
});
|
||||
}
|
||||
|
||||
async setMsgRead(peer: Peer) {
|
||||
return this.context.session.getMsgService().setMsgRead(peer);
|
||||
}
|
||||
|
||||
async getGroupFileList(GroupCode: string, params: GetFileListParam) {
|
||||
const item: GroupFileInfoUpdateItem[] = [];
|
||||
let index = params.startIndex;
|
||||
while (true) {
|
||||
params.startIndex = index;
|
||||
const [, groupFileListResult] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelRichMediaService/getGroupFileList',
|
||||
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
|
||||
[
|
||||
GroupCode,
|
||||
params,
|
||||
],
|
||||
() => true,
|
||||
() => true, // 应当通过 groupFileListResult 判断
|
||||
1,
|
||||
5000,
|
||||
);
|
||||
if (!groupFileListResult?.item?.length) break;
|
||||
item.push(...groupFileListResult.item);
|
||||
if (groupFileListResult.isEnd) break;
|
||||
if (item.length === params.fileCount) break;
|
||||
index = groupFileListResult.nextIndex;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
|
||||
// 消息时间从旧到新
|
||||
return this.context.session.getMsgService().getMsgsIncludeSelf(peer, msgId, count, isReverseOrder);
|
||||
}
|
||||
|
||||
async recallMsg(peer: Peer, msgId: string) {
|
||||
await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/recallMsg',
|
||||
'NodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
[peer, [msgId]],
|
||||
() => true,
|
||||
(updatedList) => updatedList.find(m => m.msgId === msgId && m.recallTime !== '0') !== undefined,
|
||||
1,
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
async PrepareTempChat(toUserUid: string, GroupCode: string, nickname: string) {
|
||||
return this.context.session.getMsgService().prepareTempChat({
|
||||
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
|
||||
peerUid: toUserUid,
|
||||
peerNickname: nickname,
|
||||
fromGroupCode: GroupCode,
|
||||
sig: '',
|
||||
selfPhone: '',
|
||||
selfUid: this.core.selfInfo.uid,
|
||||
gameSession: {
|
||||
nickname: '',
|
||||
gameAppId: '',
|
||||
selfTinyId: '',
|
||||
peerRoleId: '',
|
||||
peerOpenId: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTempChatInfo(chatType: ChatType, peerUid: string) {
|
||||
return this.context.session.getMsgService().getTempChatInfo(chatType, peerUid);
|
||||
}
|
||||
|
||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
|
||||
//唉?!我有个想法
|
||||
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
|
||||
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid);
|
||||
if (member) {
|
||||
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
|
||||
}
|
||||
}
|
||||
const msgId = await this.generateMsgUniqueId(peer.chatType);
|
||||
peer.guildId = msgId;
|
||||
const [, msgList] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/sendMsg',
|
||||
'NodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
[
|
||||
'0',
|
||||
peer,
|
||||
msgElements,
|
||||
new Map(),
|
||||
],
|
||||
(ret) => ret.result === 0,
|
||||
msgRecords => {
|
||||
for (const msgRecord of msgRecords) {
|
||||
if (msgRecord.guildId === msgId && msgRecord.sendStatus === SendStatusType.KSEND_STATUS_SUCCESS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
1,
|
||||
timeout,
|
||||
);
|
||||
return msgList.find(msgRecord => msgRecord.guildId === msgId);
|
||||
}
|
||||
|
||||
async generateMsgUniqueId(chatType: number) {
|
||||
return this.context.session.getMsgService().generateMsgUniqueId(chatType, this.context.session.getMSFService().getServerTime());
|
||||
}
|
||||
|
||||
async forwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]) {
|
||||
return this.context.session.getMsgService().forwardMsg(msgIds, srcPeer, [destPeer], new Map());
|
||||
}
|
||||
|
||||
async multiForwardMsg(srcPeer: Peer, destPeer: Peer, msgIds: string[]): Promise<RawMessage> {
|
||||
const msgInfos = msgIds.map(id => {
|
||||
return { msgId: id, senderShowName: this.core.selfInfo.nick };
|
||||
});
|
||||
const [, msgList] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelMsgService/multiForwardMsgWithComment',
|
||||
'NodeIKernelMsgListener/onMsgInfoListUpdate',
|
||||
[
|
||||
msgInfos,
|
||||
srcPeer,
|
||||
destPeer,
|
||||
[],
|
||||
new Map(),
|
||||
],
|
||||
() => true,
|
||||
(msgRecords) => msgRecords.some(
|
||||
msgRecord => msgRecord.peerUid === destPeer.peerUid
|
||||
&& msgRecord.senderUid === this.core.selfInfo.uid
|
||||
),
|
||||
);
|
||||
for (const msg of msgList) {
|
||||
const arkElement = msg.elements.find(ele => ele.arkElement);
|
||||
if (!arkElement) {
|
||||
continue;
|
||||
}
|
||||
const forwardData: any = JSON.parse(arkElement.arkElement?.bytesData ?? '');
|
||||
if (forwardData.app != 'com.tencent.multimsg') {
|
||||
continue;
|
||||
}
|
||||
if (msg.peerUid == destPeer.peerUid && msg.senderUid == this.core.selfInfo.uid) {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
throw new Error('转发消息超时');
|
||||
}
|
||||
|
||||
async markAllMsgAsRead() {
|
||||
return this.context.session.getMsgService().setAllC2CAndGroupMsgRead();
|
||||
}
|
||||
}
|
||||
66
src/core/apis/packet.ts
Normal file
66
src/core/apis/packet.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as os from 'os';
|
||||
import offset from '@/core/external/offset.json';
|
||||
import { InstanceContext, NapCatCore } from "@/core";
|
||||
import { LogWrapper } from "@/common/log";
|
||||
import { PacketClientSession } from "@/core/packet/clientSession";
|
||||
import { napCatVersion } from "@/common/version";
|
||||
|
||||
interface OffsetType {
|
||||
[key: string]: {
|
||||
recv: string;
|
||||
send: string;
|
||||
};
|
||||
}
|
||||
|
||||
const typedOffset: OffsetType = offset;
|
||||
|
||||
export class NTQQPacketApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
logger: LogWrapper;
|
||||
qqVersion: string | undefined;
|
||||
pkt!: PacketClientSession;
|
||||
errStack: string[] = [];
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
this.logger = core.context.logger;
|
||||
}
|
||||
async initApi() {
|
||||
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||
.then()
|
||||
.catch((err) => {
|
||||
this.logger.logError.bind(this.core.context.logger);
|
||||
this.errStack.push(err);
|
||||
});
|
||||
}
|
||||
get available(): boolean {
|
||||
return this.pkt?.available ?? false;
|
||||
}
|
||||
|
||||
get clientLogStack() {
|
||||
return this.pkt?.clientLogStack + '\n' + this.errStack.join('\n');
|
||||
}
|
||||
|
||||
async InitSendPacket(qqVer: string) {
|
||||
this.qqVersion = qqVer;
|
||||
const table = typedOffset[qqVer + '-' + os.arch()];
|
||||
if (!table) {
|
||||
const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()},
|
||||
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`;
|
||||
this.logger.logError(err);
|
||||
this.errStack.push(err);
|
||||
return false;
|
||||
}
|
||||
if (this.core.configLoader.configData.packetBackend === 'disable') {
|
||||
const err = '[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!';
|
||||
this.logger.logError(err);
|
||||
this.errStack.push(err);
|
||||
return false;
|
||||
}
|
||||
this.pkt = new PacketClientSession(this.core);
|
||||
await this.pkt.init(process.pid, table.recv, table.send);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
src/core/apis/sign.ts
Normal file
25
src/core/apis/sign.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { InstanceContext, NapCatCore } from '..';
|
||||
|
||||
export class NTQQMusicSignApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
||||
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
||||
|
||||
//外域名不行得走qgroup中转
|
||||
//https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg
|
||||
|
||||
//可外域名
|
||||
//https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1
|
||||
|
||||
//QQ音乐gtimg接口
|
||||
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
|
||||
|
||||
//还有一处公告上传可以上传高质量图片 持久为qq域名
|
||||
}
|
||||
|
||||
36
src/core/apis/system.ts
Normal file
36
src/core/apis/system.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { InstanceContext, NapCatCore } from '@/core';
|
||||
|
||||
export class NTQQSystemApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async hasOtherRunningQQProcess() {
|
||||
return this.core.util.hasOtherRunningQQProcess();
|
||||
}
|
||||
|
||||
async ocrImage(filePath: string) {
|
||||
return this.context.session.getNodeMiscService().wantWinScreenOCR(filePath);
|
||||
}
|
||||
|
||||
async translateEnWordToZn(words: string[]) {
|
||||
return this.context.session.getRichMediaService().translateEnWordToZn(words);
|
||||
}
|
||||
|
||||
async getOnlineDev() {
|
||||
this.context.session.getMsgService().getOnLineDev();
|
||||
}
|
||||
|
||||
async getArkJsonCollection(cid: string) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent('NodeIKernelCollectionService/collectionArkShare', '1717662698058');
|
||||
}
|
||||
|
||||
async bootMiniApp(appFile: string, params: string) {
|
||||
await this.context.session.getNodeMiscService().setMiniAppVersion('2.16.4');
|
||||
return this.context.session.getNodeMiscService().startNewMiniApp(appFile, params);
|
||||
}
|
||||
}
|
||||
221
src/core/apis/user.ts
Normal file
221
src/core/apis/user.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
||||
import { solveAsyncProblem } from '@/common/helper';
|
||||
|
||||
export class NTQQUserApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
//self_tind格式
|
||||
async createUidFromTinyId(tinyId: string) {
|
||||
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
|
||||
}
|
||||
async getStatusByUid(uid: string) {
|
||||
return this.context.session.getProfileService().getStatus(uid);
|
||||
}
|
||||
// 默认获取自己的 type = 2 获取别人 type = 1
|
||||
async getProfileLike(uid: string, start: number, count: number, type: number = 2) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 1,
|
||||
type: type,
|
||||
start: start,
|
||||
limit: count,
|
||||
});
|
||||
}
|
||||
async setLongNick(longNick: string) {
|
||||
return this.context.session.getProfileService().setLongNick(longNick);
|
||||
}
|
||||
|
||||
async setSelfOnlineStatus(status: number, extStatus: number, batteryStatus: number) {
|
||||
return this.context.session.getMsgService().setStatus({
|
||||
status: status,
|
||||
extStatus: extStatus,
|
||||
batteryStatus: batteryStatus,
|
||||
});
|
||||
}
|
||||
|
||||
async getBuddyRecommendContactArkJson(uin: string, sencenID = '') {
|
||||
return this.context.session.getBuddyService().getBuddyRecommendContactArkJson(uin, sencenID);
|
||||
}
|
||||
|
||||
async like(uid: string, count = 1): Promise<{ result: number, errMsg: string, succCounts: number }> {
|
||||
return this.context.session.getProfileLikeService().setBuddyProfileLike({
|
||||
friendUid: uid,
|
||||
sourceId: 71,
|
||||
doLikeCount: count,
|
||||
doLikeTollCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async setQQAvatar(filePath: string) {
|
||||
const ret = await this.context.session.getProfileService().setHeader(filePath);
|
||||
return { result: ret?.result, errMsg: ret?.errMsg };
|
||||
}
|
||||
|
||||
async setGroupAvatar(gc: string, filePath: string) {
|
||||
return this.context.session.getGroupService().setHeader(gc, filePath);
|
||||
}
|
||||
|
||||
async fetchUserDetailInfo(uid: string, mode: UserDetailSource = UserDetailSource.KDB) {
|
||||
const [_retData, profile] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelProfileService/fetchUserDetailInfo',
|
||||
'NodeIKernelProfileListener/onUserDetailInfoChanged',
|
||||
[
|
||||
'BuddyProfileStore',
|
||||
[uid],
|
||||
mode,
|
||||
[ProfileBizType.KALL],
|
||||
],
|
||||
() => true,
|
||||
(profile) => profile.uid === uid,
|
||||
);
|
||||
const RetUser: User = {
|
||||
...profile.simpleInfo.status,
|
||||
...profile.simpleInfo.vasInfo,
|
||||
...profile.commonExt,
|
||||
...profile.simpleInfo.baseInfo,
|
||||
qqLevel: profile.commonExt?.qqLevel,
|
||||
age: profile.simpleInfo.baseInfo.age,
|
||||
pendantId: '',
|
||||
...profile.simpleInfo.coreInfo
|
||||
};
|
||||
return RetUser;
|
||||
}
|
||||
|
||||
async getUserDetailInfo(uid: string): Promise<User> {
|
||||
let retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(uid, UserDetailSource.KDB), uid);
|
||||
if (retUser && retUser.uin !== '0') {
|
||||
return retUser;
|
||||
}
|
||||
this.context.logger.logDebug('[NapCat] [Mark] getUserDetailInfo Mode1 Failed.');
|
||||
retUser = await this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER);
|
||||
if (retUser && retUser.uin === '0') {
|
||||
retUser.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
|
||||
}
|
||||
return retUser;
|
||||
}
|
||||
|
||||
async modifySelfProfile(param: ModifyProfileParams) {
|
||||
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
|
||||
}
|
||||
|
||||
async getCookies(domain: string) {
|
||||
const ClientKeyData = await this.forceFetchClientKey();
|
||||
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin +
|
||||
'&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2F' + domain + '%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27';
|
||||
const data = await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
if (!data.p_skey || data.p_skey.length == 0) {
|
||||
try {
|
||||
const pskey = (await this.getPSkey([domain])).domainPskeyMap.get(domain);
|
||||
if (pskey) data.p_skey = pskey;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async getPSkey(domainList: string[]) {
|
||||
return await this.context.session.getTipOffService().getPskey(domainList, true);
|
||||
}
|
||||
|
||||
async getRobotUinRange(): Promise<Array<any>> {
|
||||
const robotUinRanges = await this.context.session.getRobotService().getRobotUinRange({
|
||||
justFetchMsgConfig: '1',
|
||||
type: 1,
|
||||
version: 0,
|
||||
aioKeywordVersion: 0,
|
||||
});
|
||||
return robotUinRanges?.response?.robotUinRanges;
|
||||
}
|
||||
|
||||
//需要异常处理
|
||||
|
||||
async getQzoneCookies() {
|
||||
const ClientKeyData = await this.forceFetchClientKey();
|
||||
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin + '&clientkey=' + ClientKeyData.clientKey + '&u1=https%3A%2F%2Fuser.qzone.qq.com%2F' + this.core.selfInfo.uin + '%2Finfocenter&keyindex=19%27';
|
||||
return await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
}
|
||||
|
||||
//需要异常处理
|
||||
|
||||
async getSKey(): Promise<string | undefined> {
|
||||
const ClientKeyData = await this.forceFetchClientKey();
|
||||
if (ClientKeyData.result !== 0) {
|
||||
throw new Error('getClientKey Error');
|
||||
}
|
||||
const clientKey = ClientKeyData.clientKey;
|
||||
// const keyIndex = ClientKeyData.keyIndex;
|
||||
const requestUrl = 'https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=' + this.core.selfInfo.uin + '&clientkey=' + clientKey + '&u1=https%3A%2F%2Fh5.qzone.qq.com%2Fqqnt%2Fqzoneinpcqq%2Ffriend%3Frefresh%3D0%26clientuin%3D0%26darkMode%3D0&keyindex=19%27';
|
||||
const cookies: { [key: string]: string; } = await RequestUtil.HttpsGetCookies(requestUrl);
|
||||
const skey = cookies['skey'];
|
||||
if (!skey) {
|
||||
throw new Error('SKey is Empty');
|
||||
}
|
||||
return skey;
|
||||
}
|
||||
|
||||
//后期改成流水线处理
|
||||
async getUidByUinV2(Uin: string) {
|
||||
let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin);
|
||||
if (uid) return uid;
|
||||
uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
|
||||
if (uid) return uid;
|
||||
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
|
||||
if (uid) return uid;
|
||||
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
|
||||
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
|
||||
//if (uid) return uid;
|
||||
return uid;
|
||||
}
|
||||
|
||||
//后期改成流水线处理
|
||||
async getUinByUidV2(Uid: string) {
|
||||
let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
|
||||
return uin;
|
||||
}
|
||||
|
||||
async getRecentContactListSnapShot(count: number) {
|
||||
return await this.context.session.getRecentContactService().getRecentContactListSnapShot(count);
|
||||
}
|
||||
|
||||
async getRecentContactListSyncLimit(count: number) {
|
||||
return await this.context.session.getRecentContactService().getRecentContactListSyncLimit(count);
|
||||
}
|
||||
|
||||
async getRecentContactListSync() {
|
||||
return await this.context.session.getRecentContactService().getRecentContactListSync();
|
||||
}
|
||||
|
||||
async getRecentContactList() {
|
||||
return await this.context.session.getRecentContactService().getRecentContactList();
|
||||
}
|
||||
|
||||
async getUserDetailInfoByUin(Uin: string) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getUserDetailInfoByUin',
|
||||
Uin
|
||||
);
|
||||
}
|
||||
|
||||
async forceFetchClientKey() {
|
||||
return await this.context.session.getTicketService().forceFetchClientKey('');
|
||||
}
|
||||
}
|
||||
415
src/core/apis/webapi.ts
Normal file
415
src/core/apis/webapi.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import {
|
||||
GroupEssenceMsgRet,
|
||||
InstanceContext,
|
||||
WebApiGroupMember,
|
||||
WebApiGroupMemberRet,
|
||||
WebApiGroupNoticeRet,
|
||||
WebHonorType,
|
||||
} from '@/core';
|
||||
import { NapCatCore } from '..';
|
||||
import { createReadStream, readFileSync, statSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
export class NTQQWebApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
async shareDigest(groupCode: string, msgSeq: string, msgRandom: string, targetGroupCode: string) {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const url = `https://qun.qq.com/cgi-bin/group_digest/share_digest?${new URLSearchParams({
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
group_code: groupCode,
|
||||
msg_seq: msgSeq,
|
||||
msg_random: msgRandom,
|
||||
target_group_code: targetGroupCode,
|
||||
}).toString()}`;
|
||||
try {
|
||||
return RequestUtil.HttpGetText(url, 'GET', '', { 'Cookie': this.cookieToString(cookieObject) });
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
async getGroupEssenceMsgAll(GroupCode: string) {
|
||||
const ret: GroupEssenceMsgRet[] = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const data = await this.getGroupEssenceMsg(GroupCode, i, 50);
|
||||
if (!data) break;
|
||||
ret.push(data);
|
||||
if (data.data.is_end) break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
async getGroupEssenceMsg(GroupCode: string, page_start: number = 0, page_limit: number = 50) {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?${new URLSearchParams({
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
page_start: page_start.toString(),
|
||||
page_limit: page_limit.toString(),
|
||||
group_code: GroupCode,
|
||||
}).toString()}`;
|
||||
try {
|
||||
const ret = await RequestUtil.HttpGetJson<GroupEssenceMsgRet>(
|
||||
url,
|
||||
'GET',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
return ret.retcode === 0 ? ret : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupMembers(GroupCode: string, cached: boolean = true): Promise<WebApiGroupMember[]> {
|
||||
//logDebug('webapi 获取群成员', GroupCode);
|
||||
const memberData: Array<WebApiGroupMember> = new Array<WebApiGroupMember>();
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const retList: Promise<WebApiGroupMemberRet>[] = [];
|
||||
const fastRet = await RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||||
st: '0',
|
||||
end: '40',
|
||||
sort: '1',
|
||||
gc: GroupCode,
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
}).toString()}`,
|
||||
'POST',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
if (!fastRet?.count || fastRet?.errcode !== 0 || !fastRet?.mems) {
|
||||
return [];
|
||||
} else {
|
||||
for (const key in fastRet.mems) {
|
||||
memberData.push(fastRet.mems[key]);
|
||||
}
|
||||
}
|
||||
//初始化获取PageNum
|
||||
const PageNum = Math.ceil(fastRet.count / 40);
|
||||
//遍历批量请求
|
||||
for (let i = 2; i <= PageNum; i++) {
|
||||
const ret = RequestUtil.HttpGetJson<WebApiGroupMemberRet>(
|
||||
`https://qun.qq.com/cgi-bin/qun_mgr/search_group_members?${new URLSearchParams({
|
||||
st: ((i - 1) * 40).toString(),
|
||||
end: (i * 40).toString(),
|
||||
sort: '1',
|
||||
gc: GroupCode,
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
}).toString()}`,
|
||||
'POST',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
retList.push(ret);
|
||||
}
|
||||
//批量等待
|
||||
for (let i = 1; i <= PageNum; i++) {
|
||||
const ret = await (retList[i]);
|
||||
if (!ret?.count || ret?.errcode !== 0 || !ret?.mems) {
|
||||
continue;
|
||||
}
|
||||
for (const key in ret.mems) {
|
||||
memberData.push(ret.mems[key]);
|
||||
}
|
||||
}
|
||||
return memberData;
|
||||
}
|
||||
|
||||
// public async addGroupDigest(groupCode: string, msgSeq: string) {
|
||||
// const url = `https://qun.qq.com/cgi-bin/group_digest/cancel_digest?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&msg_seq=${msgSeq}&msg_random=444021292`;
|
||||
// const res = await this.request(url);
|
||||
// return await res.json();
|
||||
// }
|
||||
|
||||
// public async getGroupDigest(groupCode: string) {
|
||||
// const url = `https://qun.qq.com/cgi-bin/group_digest/digest_list?random=665&X-CROSS-ORIGIN=fetch&group_code=${groupCode}&page_start=0&page_limit=20`;
|
||||
// const res = await this.request(url);
|
||||
// return await res.json();
|
||||
// }
|
||||
|
||||
async setGroupNotice(
|
||||
GroupCode: string,
|
||||
Content: string,
|
||||
pinned: number = 0,
|
||||
type: number = 1,
|
||||
is_show_edit_card: number = 1,
|
||||
tip_window_type: number = 1,
|
||||
confirm_required: number = 1,
|
||||
picId: string = '',
|
||||
imgWidth: number = 540,
|
||||
imgHeight: number = 300,
|
||||
) {
|
||||
interface SetNoticeRetSuccess {
|
||||
ec: number;
|
||||
em: string;
|
||||
id: number;
|
||||
ltsm: number;
|
||||
new_fid: string;
|
||||
read_only: number;
|
||||
role: number;
|
||||
srv_code: number;
|
||||
}
|
||||
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
|
||||
try {
|
||||
const settings = JSON.stringify({
|
||||
is_show_edit_card: is_show_edit_card,
|
||||
tip_window_type: tip_window_type,
|
||||
confirm_required: confirm_required
|
||||
});
|
||||
const externalParam = {
|
||||
pic: picId,
|
||||
imgWidth: imgWidth.toString(),
|
||||
imgHeight: imgHeight.toString(),
|
||||
};
|
||||
const ret: SetNoticeRetSuccess = await RequestUtil.HttpGetJson<SetNoticeRetSuccess>(
|
||||
`https://web.qun.qq.com/cgi-bin/announce/add_qun_notice?${new URLSearchParams({
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
qid: GroupCode,
|
||||
text: Content,
|
||||
pinned: pinned.toString(),
|
||||
type: type.toString(),
|
||||
settings: settings,
|
||||
...(picId === '' ? {} : externalParam)
|
||||
}).toString()}`,
|
||||
'POST',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
return ret;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async getGroupNotice(GroupCode: string): Promise<undefined | WebApiGroupNoticeRet> {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
try {
|
||||
const ret = await RequestUtil.HttpGetJson<WebApiGroupNoticeRet>(
|
||||
`https://web.qun.qq.com/cgi-bin/announce/get_t_list?${new URLSearchParams({
|
||||
bkn: this.getBknFromCookie(cookieObject),
|
||||
qid: GroupCode,
|
||||
ft: '23',
|
||||
ni: '1',
|
||||
n: '1',
|
||||
i: '1',
|
||||
log_read: '1',
|
||||
platform: '1',
|
||||
s: '-1',
|
||||
}).toString()}&n=20`,
|
||||
'GET',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
return ret?.ec === 0 ? ret : undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDataInternal(cookieObject: any, groupCode: string, type: number) {
|
||||
let resJson;
|
||||
try {
|
||||
const res = await RequestUtil.HttpGetText(
|
||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||
gc: groupCode,
|
||||
type: type.toString(),
|
||||
}).toString()}`,
|
||||
'GET',
|
||||
'',
|
||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||
);
|
||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||
if (match) {
|
||||
resJson = JSON.parse(match[1].trim());
|
||||
}
|
||||
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||||
} catch (e) {
|
||||
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async getHonorList(cookieObject: any, groupCode: string, type: number) {
|
||||
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||||
if (!data) {
|
||||
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||||
return [];
|
||||
}
|
||||
return data.map((item: any) => ({
|
||||
user_id: item?.uin,
|
||||
nickname: item?.name,
|
||||
avatar: item?.avatar,
|
||||
description: item?.desc,
|
||||
}));
|
||||
}
|
||||
|
||||
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||
const HonorInfo: any = { group_id: groupCode };
|
||||
|
||||
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
||||
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
|
||||
if (talkativeList.length > 0) {
|
||||
HonorInfo.current_talkative = talkativeList[0];
|
||||
HonorInfo.talkative_list = talkativeList;
|
||||
}
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||||
HonorInfo.performer_list = await this.getHonorList(cookieObject, groupCode, 2);
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
|
||||
HonorInfo.legend_list = await this.getHonorList(cookieObject, groupCode, 3);
|
||||
}
|
||||
|
||||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||||
HonorInfo.emotion_list = await this.getHonorList(cookieObject, groupCode, 6);
|
||||
}
|
||||
|
||||
// 冒尖小春笋好像已经被tx扬了 R.I.P.
|
||||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||||
HonorInfo.strong_newbie_list = [];
|
||||
}
|
||||
|
||||
return HonorInfo;
|
||||
}
|
||||
|
||||
private cookieToString(cookieObject: any) {
|
||||
return Object.entries(cookieObject).map(([key, value]) => `${key}=${value}`).join('; ');
|
||||
}
|
||||
|
||||
public getBknFromCookie(cookieObject: any) {
|
||||
const sKey = cookieObject.skey as string;
|
||||
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < sKey.length; i++) {
|
||||
const code = sKey.charCodeAt(i);
|
||||
hash = hash + (hash << 5) + code;
|
||||
}
|
||||
return (hash & 0x7FFFFFFF).toString();
|
||||
}
|
||||
public getBknFromSKey(sKey: string) {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < sKey.length; i++) {
|
||||
const code = sKey.charCodeAt(i);
|
||||
hash = hash + (hash << 5) + code;
|
||||
}
|
||||
return (hash & 0x7FFFFFFF).toString();
|
||||
}
|
||||
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) {
|
||||
const img = readFileSync(path);
|
||||
const img_md5 = createHash('md5').update(img).digest('hex');
|
||||
const img_size = img.length;
|
||||
const img_name = basename(path);
|
||||
const time = Math.floor(Date.now() / 1000);
|
||||
const GTK = this.getBknFromSKey(pskey);
|
||||
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
|
||||
const body = {
|
||||
control_req: [{
|
||||
uin: uin,
|
||||
token: {
|
||||
type: 4,
|
||||
data: pskey,
|
||||
appid: 5
|
||||
},
|
||||
appid: "qun",
|
||||
checksum: img_md5,
|
||||
check_type: 0,
|
||||
file_len: img_size,
|
||||
env: {
|
||||
refer: "qzone",
|
||||
deviceInfo: "h5"
|
||||
},
|
||||
model: 0,
|
||||
biz_req: {
|
||||
sPicTitle: img_name,
|
||||
sPicDesc: "",
|
||||
sAlbumName: sAlbumName,
|
||||
sAlbumID: sAlbumID,
|
||||
iAlbumTypeID: 0,
|
||||
iBitmap: 0,
|
||||
iUploadType: 0,
|
||||
iUpPicType: 0,
|
||||
iBatchID: time,
|
||||
sPicPath: "",
|
||||
iPicWidth: 0,
|
||||
iPicHight: 0,
|
||||
iWaterType: 0,
|
||||
iDistinctUse: 0,
|
||||
iNeedFeeds: 1,
|
||||
iUploadTime: time,
|
||||
mapExt: {
|
||||
appid: "qun",
|
||||
userid: gc
|
||||
}
|
||||
},
|
||||
session: "",
|
||||
asy_upload: 0,
|
||||
cmd: "FileUpload"
|
||||
}]
|
||||
};
|
||||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
||||
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
|
||||
"Cookie": cookie,
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
|
||||
return post;
|
||||
}
|
||||
|
||||
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
|
||||
const img_size = statSync(path).size;
|
||||
const img_name = basename(path);
|
||||
let seq = 0;
|
||||
let offset = 0;
|
||||
const GTK = this.getBknFromSKey(pskey);
|
||||
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
|
||||
|
||||
const stream = createReadStream(path, { highWaterMark: slice_size });
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const end = Math.min(offset + chunk.length, img_size);
|
||||
const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`;
|
||||
const formData = await RequestUtil.createFormData(boundary, path);
|
||||
|
||||
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
|
||||
const body = {
|
||||
uin: uin,
|
||||
appid: "qun",
|
||||
session: session,
|
||||
offset: offset,
|
||||
data: formData,
|
||||
checksum: "",
|
||||
check_type: 0,
|
||||
retry: 0,
|
||||
seq: seq,
|
||||
end: end,
|
||||
cmd: "FileUpload",
|
||||
slice_size: slice_size,
|
||||
"biz_req.iUploadType": 0
|
||||
};
|
||||
|
||||
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
|
||||
"Cookie": cookie,
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`
|
||||
});
|
||||
|
||||
offset += chunk.length;
|
||||
seq++;
|
||||
}
|
||||
}
|
||||
async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) {
|
||||
const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session;
|
||||
return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024);
|
||||
}
|
||||
}
|
||||
102
src/core/external/appid.json
vendored
Normal file
102
src/core/external/appid.json
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"9.9.15-28060": {
|
||||
"appid": 537246092,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28060_GW_B"
|
||||
},
|
||||
"9.9.15-28131": {
|
||||
"appid": 537246092,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28131_GW_B"
|
||||
},
|
||||
"3.2.12-28060": {
|
||||
"appid": 537246140,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28060_GW_B"
|
||||
},
|
||||
"3.2.12-28131": {
|
||||
"appid": 537246140,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28131_GW_B"
|
||||
},
|
||||
"6.9.55-28131": {
|
||||
"appid": 537246115,
|
||||
"qua": "V1_MAC_NQ_6.9.55_28131_GW_B"
|
||||
},
|
||||
"9.9.15-28327": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28327_GW_B"
|
||||
},
|
||||
"3.2.12-28327": {
|
||||
"appid": 537249393,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28327_GW_B"
|
||||
},
|
||||
"9.9.15-28418": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28418_GW_B"
|
||||
},
|
||||
"3.2.12-28418": {
|
||||
"appid": 537249393,
|
||||
"qua": "V1_LNX_NQ_3.2.12_28418_GW_B"
|
||||
},
|
||||
"6.9.56-28418": {
|
||||
"appid": 537249367,
|
||||
"qua": "V1_MAC_NQ_6.9.56_28418_GW_B"
|
||||
},
|
||||
"9.9.15-28498": {
|
||||
"appid": 537249321,
|
||||
"qua": "V1_WIN_NQ_9.9.15_28498_GW_B"
|
||||
},
|
||||
"3.2.13-28788": {
|
||||
"appid": 537249787,
|
||||
"qua": "V1_LNX_NQ_3.2.13_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28788": {
|
||||
"appid": 537249739,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||
},
|
||||
"9.9.16-28971": {
|
||||
"appid": 537249775,
|
||||
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||
},
|
||||
"3.2.13-28971": {
|
||||
"appid": 537249848,
|
||||
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
|
||||
},
|
||||
"6.9.58-28971": {
|
||||
"appid": 537249826,
|
||||
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
||||
},
|
||||
"9.9.16-29271": {
|
||||
"appid": 537249813,
|
||||
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
|
||||
},
|
||||
"3.2.13-29271": {
|
||||
"appid": 537249913,
|
||||
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
|
||||
},
|
||||
"6.9.59-29271": {
|
||||
"appid": 537249863,
|
||||
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
|
||||
},
|
||||
"9.9.16-29456": {
|
||||
"appid": 537249875,
|
||||
"qua": "V1_WIN_NQ_9.9.16_29456_GW_B"
|
||||
},
|
||||
"3.2.13-29456": {
|
||||
"appid": 537249996,
|
||||
"qua": "V1_LNX_NQ_3.2.13_29456_GW_B"
|
||||
},
|
||||
"6.9.59-29456": {
|
||||
"appid": 537249961,
|
||||
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
|
||||
},
|
||||
"9.9.16-29927": {
|
||||
"appid": 537255812,
|
||||
"qua": "V1_WIN_NQ_9.9.16_29927_GW_B"
|
||||
},
|
||||
"3.2.13-29927": {
|
||||
"appid": 537255847,
|
||||
"qua": "V1_LNX_NQ_3.2.13_29927_GW_B"
|
||||
},
|
||||
"6.9.61-29927": {
|
||||
"appid": 537255836,
|
||||
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B"
|
||||
}
|
||||
}
|
||||
3665
src/core/external/face_config.json
vendored
Normal file
3665
src/core/external/face_config.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
8
src/core/external/napcat.json
vendored
Normal file
8
src/core/external/napcat.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"fileLog": false,
|
||||
"consoleLog": true,
|
||||
"fileLogLevel": "debug",
|
||||
"consoleLogLevel": "info",
|
||||
"packetBackend": "auto",
|
||||
"packetServer": ""
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user