mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-07 05:20:23 +00:00
Compare commits
3066 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 ..
|
||||
npm run build:framework && npm run depend
|
||||
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 ..
|
||||
npm run build:shell && npm run depend
|
||||
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"
|
||||
}
|
||||
343
LICENSE
Normal file
343
LICENSE
Normal file
@@ -0,0 +1,343 @@
|
||||
GNU GENERAL PUBLIC Without Social media promotion LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
d)You may use this software in accordance with the above terms,
|
||||
but you are not allowed to promote this project or your projects
|
||||
based on this project on any public social media.
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
63
README.md
63
README.md
@@ -1,14 +1,57 @@
|
||||
# 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/haLGHixZ74)
|
||||
|
||||
## 感谢他们
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
||||
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
|
||||
|
||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 延缓Native模块与NapCat对新版QQ适配
|
||||
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
|
||||
|
||||
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
|
||||
|
||||
因此此时推荐使用release指定版本
|
||||
|
||||
## 开源附加
|
||||
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./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.1.8",
|
||||
"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="./vite.svg" />
|
||||
<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>
|
||||
31
napcat.webui/package.json
Normal file
31
napcat.webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"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": "vue-tsc -b && vite build",
|
||||
"webui:preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"tdesign-vue-next": "^1.10.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.31.0",
|
||||
"globals": "^15.12.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 |
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 |
7
napcat.webui/src/App.vue
Normal file
7
napcat.webui/src/App.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
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 |
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 |
185
napcat.webui/src/backend/shell.ts
Normal file
185
napcat.webui/src/backend/shell.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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 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: '接口异常' };
|
||||
}
|
||||
}
|
||||
55
napcat.webui/src/components/Dashboard.vue
Normal file
55
napcat.webui/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
||||
<div class="content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import SidebarMenu from './webui/Nav.vue';
|
||||
|
||||
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' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
/* padding: 20px; */
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
napcat.webui/src/components/QQLogin.vue
Normal file
167
napcat.webui/src/components/QQLogin.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">QQ Login</h2>
|
||||
<div class="login-methods">
|
||||
<t-button
|
||||
id="quick-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'quick' }"
|
||||
@click="loginMethod = 'quick'"
|
||||
>Quick Login</t-button
|
||||
>
|
||||
<t-button
|
||||
id="qrcode-login"
|
||||
class="login-method"
|
||||
:class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="loginMethod = 'qrcode'"
|
||||
>QR Code</t-button
|
||||
>
|
||||
</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>
|
||||
</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;
|
||||
|
||||
const selectAccount = async (accountName: string): Promise<void> => {
|
||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||
if (result) {
|
||||
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.checkQQLoginStatus();
|
||||
if (isLogined) {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
}
|
||||
};
|
||||
|
||||
const InitPages = async (): Promise<void> => {
|
||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
|
||||
generateQrCode(qrcodeData, qrcodeCanvas.value);
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
InitPages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 0 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;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
151
napcat.webui/src/components/WebUiLogin.vue
Normal file
151
napcat.webui/src/components/WebUiLogin.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<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>
|
||||
<div class="footer">Power By NapCat.WebUi</div>
|
||||
</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>
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 0 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;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
71
napcat.webui/src/components/webui/Nav.vue
Normal file
71
napcat.webui/src/components/webui/Nav.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
||||
<template #logo> </template>
|
||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||
<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>
|
||||
</router-link>
|
||||
<template #operations>
|
||||
<t-button 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 } from 'vue';
|
||||
|
||||
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 changeCollapsed = (): void => {
|
||||
collapsed.value = !collapsed.value;
|
||||
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||
};
|
||||
</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-text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
62
napcat.webui/src/main.ts
Normal file
62
napcat.webui/src/main.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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,
|
||||
ListItem as TListItem,
|
||||
Tabs as TTabs,
|
||||
TabPanel as TTabPanel,
|
||||
Space as TSpace,
|
||||
Checkbox as TCheckbox,
|
||||
Popup as TPopup,
|
||||
Dialog as TDialog,
|
||||
Switch as TSwitch,
|
||||
} 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(TListItem);
|
||||
app.use(TTabs);
|
||||
app.use(TTabPanel);
|
||||
app.use(TSpace);
|
||||
app.use(TCheckbox);
|
||||
app.use(TPopup);
|
||||
app.use(TDialog);
|
||||
app.use(TSwitch);
|
||||
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>
|
||||
249
napcat.webui/src/pages/NetWork.vue
Normal file
249
napcat.webui/src/pages/NetWork.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<t-space class="full-space">
|
||||
<template v-if="clientPanelData.length > 0">
|
||||
<t-tabs
|
||||
v-model="activeTab"
|
||||
:addable="true"
|
||||
theme="card"
|
||||
@add="showAddTabDialog"
|
||||
@remove="removeTab"
|
||||
class="full-tabs"
|
||||
>
|
||||
<t-tab-panel
|
||||
v-for="(config, idx) in clientPanelData"
|
||||
:key="idx"
|
||||
:label="config.name"
|
||||
:removable="true"
|
||||
:value="idx"
|
||||
class="full-tab-panel"
|
||||
>
|
||||
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
|
||||
<div class="button-container">
|
||||
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
|
||||
</div>
|
||||
</t-tab-panel>
|
||||
</t-tabs>
|
||||
</template>
|
||||
<template v-else>
|
||||
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
|
||||
</template>
|
||||
<t-dialog
|
||||
v-model:visible="isDialogVisible"
|
||||
header="添加网络配置"
|
||||
@close="isDialogVisible = false"
|
||||
@confirm="addTab"
|
||||
>
|
||||
<t-form ref="form" :model="newTab">
|
||||
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
|
||||
<t-input v-model="newTab.name" />
|
||||
</t-form-item>
|
||||
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
|
||||
<t-select v-model="newTab.type">
|
||||
<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>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</t-space>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import {
|
||||
httpServerDefaultConfigs,
|
||||
httpClientDefaultConfigs,
|
||||
websocketServerDefaultConfigs,
|
||||
websocketClientDefaultConfigs,
|
||||
HttpClientConfig,
|
||||
HttpServerConfig,
|
||||
WebsocketClientConfig,
|
||||
WebsocketServerConfig,
|
||||
NetworkConfig,
|
||||
OneBotConfig,
|
||||
mergeOneBotConfigs,
|
||||
} from '../../../src/onebot/config/config';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
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 EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
|
||||
|
||||
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
|
||||
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
|
||||
type ComponentUnion =
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent;
|
||||
|
||||
const componentMap: Record<ConfigKey, ComponentUnion> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
};
|
||||
|
||||
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
|
||||
httpServers: httpServerDefaultConfigs,
|
||||
httpClients: httpClientDefaultConfigs,
|
||||
websocketServers: websocketServerDefaultConfigs,
|
||||
websocketClients: websocketClientDefaultConfigs,
|
||||
};
|
||||
|
||||
interface ConfigMap {
|
||||
httpServers: HttpServerConfig;
|
||||
httpClients: HttpClientConfig;
|
||||
websocketServers: WebsocketServerConfig;
|
||||
websocketClients: WebsocketClientConfig;
|
||||
}
|
||||
|
||||
interface ClientPanel<K extends ConfigKey = ConfigKey> {
|
||||
name: string;
|
||||
key: K;
|
||||
data: ConfigMap[K];
|
||||
}
|
||||
|
||||
const activeTab = ref<number>(0);
|
||||
const isDialogVisible = ref(false);
|
||||
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
|
||||
const clientPanelData: Ref<ClientPanel[]> = ref([]);
|
||||
|
||||
const getComponent = (type: ConfigKey) => {
|
||||
return componentMap[type];
|
||||
};
|
||||
|
||||
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 addToPanel = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => {
|
||||
configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key }));
|
||||
};
|
||||
|
||||
const addConfigDataToPanel = (data: NetworkConfig) => {
|
||||
(Object.keys(data) as ConfigKey[]).forEach((key) => {
|
||||
addToPanel(data[key], key);
|
||||
});
|
||||
};
|
||||
|
||||
const parsePanelData = (): NetworkConfig => {
|
||||
const result: NetworkConfig = {
|
||||
httpServers: [],
|
||||
httpClients: [],
|
||||
websocketServers: [],
|
||||
websocketClients: [],
|
||||
};
|
||||
clientPanelData.value.forEach((panel) => {
|
||||
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||
addConfigDataToPanel(mergedConfig.network);
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async () => {
|
||||
const config = parsePanelData();
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) {
|
||||
await MessagePlugin.error('无法获取配置!');
|
||||
return;
|
||||
}
|
||||
userConfig.network = config;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
await MessagePlugin.success('配置保存成功');
|
||||
} else {
|
||||
await MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showAddTabDialog = () => {
|
||||
newTab.value = { name: '', type: 'httpServers' };
|
||||
isDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const addTab = async () => {
|
||||
const { name, type } = newTab.value;
|
||||
if (clientPanelData.value.some((panel) => panel.name === name)) {
|
||||
await MessagePlugin.error('选项卡名称已存在');
|
||||
return;
|
||||
}
|
||||
const defaultConfig = structuredClone(defaultConfigMap[type]);
|
||||
defaultConfig.name = name;
|
||||
clientPanelData.value.push({ name, data: defaultConfig, key: type });
|
||||
isDialogVisible.value = false;
|
||||
await nextTick();
|
||||
activeTab.value = clientPanelData.value.length - 1;
|
||||
await MessagePlugin.success('选项卡添加成功');
|
||||
};
|
||||
|
||||
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
|
||||
clientPanelData.value.splice(payload.index, 1);
|
||||
activeTab.value = Math.max(0, activeTab.value - 1);
|
||||
await saveConfig();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.full-space {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.full-tabs {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.full-tab-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
134
napcat.webui/src/pages/OtherConfig.vue
Normal file
134
napcat.webui/src/pages/OtherConfig.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-divider content="其余配置" align="left" />
|
||||
</div>
|
||||
<div class="other-config-container">
|
||||
<div class="other-config">
|
||||
<t-form ref="form" :model="otherConfig" class="form">
|
||||
<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>
|
||||
<div class="button-container">
|
||||
<t-button @click="saveConfig">保存</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
} 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;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
MessagePlugin.success('配置保存成功');
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.other-config-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.other-config {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.form-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-item t-input,
|
||||
.form-item t-switch {
|
||||
flex: 1;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
napcat.webui/src/pages/network/EmptyStateComponent.vue
Normal file
22
napcat.webui/src/pages/network/EmptyStateComponent.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<p>当前没有网络配置</p>
|
||||
<t-button @click="showAddTabDialog">添加网络配置</t-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
defineProps<{ showAddTabDialog: () => void }>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
68
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
68
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<h3>HTTP Client 配置</h3>
|
||||
<t-form>
|
||||
<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>
|
||||
</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>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
74
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
74
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<h3>HTTP Server 配置</h3>
|
||||
<t-form>
|
||||
<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>
|
||||
</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>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
71
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
71
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<h3>WebSocket Client 配置</h3>
|
||||
<t-form>
|
||||
<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>
|
||||
</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>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
77
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
77
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="form-container">
|
||||
<h3>WebSocket Server 配置</h3>
|
||||
<t-form>
|
||||
<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>
|
||||
</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>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</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,
|
||||
});
|
||||
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"]
|
||||
}
|
||||
30
napcat.webui/vite.config.ts
Normal file
30
napcat.webui/vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import path from 'path';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': 'http://localhost:6099',
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
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.1.8",
|
||||
"scripts": {
|
||||
"build:framework": "npm run build:webui && vite build --mode framework",
|
||||
"build:shell": "npm run build:webui && vite build --mode shell",
|
||||
"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": "^5.2.6",
|
||||
"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",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"piscina": "^4.7.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 './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);
|
||||
|
||||
let 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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
293
src/common/file.ts
Normal file
293
src/common/file.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
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 './helper';
|
||||
|
||||
export function isGIF(path: string) {
|
||||
const buffer = Buffer.alloc(4);
|
||||
const fd = fs.openSync(path, 'r');
|
||||
fs.readSync(fd, buffer, 0, 4, 0);
|
||||
fs.closeSync(fd);
|
||||
return buffer.toString() === 'GIF8';
|
||||
}
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export function checkFileReceived(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 checkFileReceived2(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 {
|
||||
// 读取文件内容
|
||||
// if (!fs.existsSync(path)){
|
||||
// path = path.replace("\\Ori\\", "\\Thumb\\");
|
||||
// }
|
||||
try {
|
||||
await checkFileReceived(path, 5000);
|
||||
} catch (e: any) {
|
||||
result.err = e.toString();
|
||||
return result;
|
||||
}
|
||||
const data = await readFile(path);
|
||||
// 转换为Base64编码
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
url: string;
|
||||
headers?: Record<string, string> | string;
|
||||
}
|
||||
|
||||
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||
// const chunks: Buffer[] = [];
|
||||
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);
|
||||
}
|
||||
|
||||
type Uri2LocalRes = {
|
||||
success: boolean,
|
||||
errMsg: string,
|
||||
fileName: string,
|
||||
ext: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
if (uri.startsWith('file://')) {
|
||||
let filePath: string;
|
||||
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
|
||||
if (process.platform === 'win32') {
|
||||
filePath = pathname.slice(1);
|
||||
} else {
|
||||
filePath = pathname;
|
||||
}
|
||||
|
||||
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 { AtType, 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 === AtType.notAt) {
|
||||
const originalContentLines = textElement.content.split('\n');
|
||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||
} else if (textElement.atType === AtType.atAll) {
|
||||
return `@全体成员`;
|
||||
} else if (textElement.atType === AtType.atUser) {
|
||||
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 './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 './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.1.8';
|
||||
63
src/common/video.ts
Normal file
63
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 "../entities/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 '..';
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
489
src/core/apis/file.ts
Normal file
489
src/core/apis/file.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
import {
|
||||
ChatType,
|
||||
ElementType,
|
||||
IMAGE_HTTP_HOST,
|
||||
IMAGE_HTTP_HOST_NT,
|
||||
Peer,
|
||||
PicElement,
|
||||
PicType,
|
||||
RawMessage,
|
||||
SendFileElement,
|
||||
SendPicElement,
|
||||
SendPttElement,
|
||||
SendVideoElement,
|
||||
} from '@/core/entities';
|
||||
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 '../helper/rkey';
|
||||
import { calculateFileMD5, isGIF } 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 { MessageContext } from '@/onebot/api';
|
||||
|
||||
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', 'https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], 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: MessageContext, 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: MessageContext, picPath: string, summary: string = '', subType: 0 | 1 = 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: isGIF(picPath) ? PicType.gif : PicType.jpg,
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
thumbFileSize: 0,
|
||||
summary,
|
||||
} as PicElement,
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(context: MessageContext, 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;
|
||||
|
||||
if (url) {
|
||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||
const rkeyData = await this.getRkeyData();
|
||||
return this.getImageUrlFromParsedUrl(parsedUrl, 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(parsedUrl: URL, rkeyData: any): string {
|
||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||
if (isNTV2 && rkeyData.online_rkey) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST_NT + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
} else if (isNTV2 && imageFileId) {
|
||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
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/entities';
|
||||
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 getBuddyV2(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,
|
||||
});
|
||||
}
|
||||
}
|
||||
528
src/core/apis/group.ts
Normal file
528
src/core/apis/group.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
import {
|
||||
GeneralCallResult,
|
||||
Group,
|
||||
GroupMember,
|
||||
GroupMemberRole,
|
||||
GroupRequestOperateTypes,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// log('getGroupMember', members);
|
||||
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);
|
||||
//console.log('GetGroupMembersV3 len :', result.result.infos.size, resMode2?.infos.size, groupQQ);
|
||||
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: GroupRequestOperateTypes, 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, // 2 拒绝
|
||||
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: GroupMemberRole) {
|
||||
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';
|
||||
287
src/core/apis/msg.ts
Normal file
287
src/core/apis/msg.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/entities';
|
||||
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore } from '@/core';
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
|
||||
export class NTQQMsgApi {
|
||||
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||
}
|
||||
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
||||
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
||||
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
||||
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
232
src/core/apis/user.ts
Normal file
232
src/core/apis/user.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { ModifyProfileParams, User, UserDetailSource } from '@/core/entities';
|
||||
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);
|
||||
}
|
||||
async getProfileLike(uid: string, start: number, count: number) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 1,
|
||||
type: 2,
|
||||
start: start,
|
||||
limit: count,
|
||||
});
|
||||
}
|
||||
async fetchOtherProfileLike(uid: string) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 0,
|
||||
type: 1,
|
||||
start: 0,
|
||||
limit: 20,
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
11
src/core/entities/adapter.ts
Normal file
11
src/core/entities/adapter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum MsfStatusType {
|
||||
KUNKNOWN,
|
||||
KDISCONNECTED,
|
||||
KCONNECTED
|
||||
}
|
||||
export enum MsfChangeReasonType {
|
||||
KUNKNOWN,
|
||||
KUSERLOGININ,
|
||||
KUSERLOGINOUT,
|
||||
KAUTO
|
||||
}
|
||||
65
src/core/entities/cache.ts
Normal file
65
src/core/entities/cache.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ChatType } from './msg';
|
||||
|
||||
export interface CacheScanResult {
|
||||
result: number;
|
||||
size: [ // 单位为字节
|
||||
string, // 系统总存储空间
|
||||
string, // 系统可用存储空间
|
||||
string, // 系统已用存储空间
|
||||
string, // QQ总大小
|
||||
string, // 「聊天与文件」大小
|
||||
string, // 未知
|
||||
string, // 「缓存数据」大小
|
||||
string, // 「其他数据」大小
|
||||
string, // 未知
|
||||
];
|
||||
}
|
||||
|
||||
export interface ChatCacheList {
|
||||
pageCount: number;
|
||||
infos: ChatCacheListItem[];
|
||||
}
|
||||
|
||||
export interface ChatCacheListItem {
|
||||
chatType: ChatType;
|
||||
basicChatCacheInfo: ChatCacheListItemBasic;
|
||||
guildChatCacheInfo: unknown[]; // TODO: 没用过频道所以不知道这里边的详细内容
|
||||
}
|
||||
|
||||
export interface ChatCacheListItemBasic {
|
||||
chatSize: string;
|
||||
chatTime: string;
|
||||
uid: string;
|
||||
uin: string;
|
||||
remarkName: string;
|
||||
nickName: string;
|
||||
chatType?: ChatType;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
|
||||
export enum CacheFileType {
|
||||
IMAGE = 0,
|
||||
VIDEO = 1,
|
||||
AUDIO = 2,
|
||||
DOCUMENT = 3,
|
||||
OTHER = 4,
|
||||
}
|
||||
|
||||
export interface CacheFileList {
|
||||
infos: CacheFileListItem[],
|
||||
}
|
||||
|
||||
export interface CacheFileListItem {
|
||||
fileSize: string;
|
||||
fileTime: string;
|
||||
fileKey: string;
|
||||
elementId: string;
|
||||
elementIdStr: string;
|
||||
fileType: CacheFileType;
|
||||
path: string;
|
||||
fileName: string;
|
||||
senderId: string;
|
||||
previewPath: string;
|
||||
senderName: string;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
12
src/core/entities/contact.ts
Normal file
12
src/core/entities/contact.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export interface FSABRecentContactParams {
|
||||
anchorPointContact: {
|
||||
contactId: string;
|
||||
sortField: string;
|
||||
pos: number;
|
||||
};
|
||||
relativeMoveCount: number;
|
||||
listType: number;
|
||||
count: number;
|
||||
fetchOld: boolean;
|
||||
}
|
||||
140
src/core/entities/group.ts
Normal file
140
src/core/entities/group.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { QQLevel, Sex } from './user';
|
||||
|
||||
export interface KickMemberInfo {
|
||||
optFlag: number,
|
||||
optOperate: number,
|
||||
optMemberUid: string,
|
||||
optBytesMsg: string,
|
||||
}
|
||||
//getGroupDetailInfo GroupCode,GroupInfoSource
|
||||
export enum GroupInfoSource {
|
||||
KUNSPECIFIED,
|
||||
KBIGDATACARD,
|
||||
KDATACARD,
|
||||
KNOTICE,
|
||||
KAIO,
|
||||
KRECENTCONTACT,
|
||||
KMOREPANEL
|
||||
}
|
||||
export interface GroupExt0xEF0InfoFilter {
|
||||
bindGuildId: number;
|
||||
blacklistExpireTime: number;
|
||||
companyId: number;
|
||||
essentialMsgPrivilege: number;
|
||||
essentialMsgSwitch: number;
|
||||
fullGroupExpansionSeq: number;
|
||||
fullGroupExpansionSwitch: number;
|
||||
gangUpId: number;
|
||||
groupAioBindGuildId: number;
|
||||
groupBindGuildIds: number;
|
||||
groupBindGuildSwitch: number;
|
||||
groupExcludeGuildIds: number;
|
||||
groupExtFlameData: number;
|
||||
groupFlagPro1: number;
|
||||
groupInfoExtSeq: number;
|
||||
groupOwnerId: number;
|
||||
groupSquareSwitch: number;
|
||||
hasGroupCustomPortrait: number;
|
||||
inviteRobotMemberExamine: number;
|
||||
inviteRobotMemberSwitch: number;
|
||||
inviteRobotSwitch: number;
|
||||
isLimitGroupRtc: number;
|
||||
lightCharNum: number;
|
||||
luckyWord: number;
|
||||
luckyWordId: number;
|
||||
msgEventSeq: number;
|
||||
qqMusicMedalSwitch: number;
|
||||
reserve: number;
|
||||
showPlayTogetherSwitch: number;
|
||||
starId: number;
|
||||
todoSeq: number;
|
||||
viewedMsgDisappearTime: number;
|
||||
}
|
||||
|
||||
export interface KickMemberV2Req {
|
||||
groupCode: string,
|
||||
kickFlag: number,
|
||||
kickList: Array<KickMemberInfo>,
|
||||
kickListUids: Array<string>,
|
||||
kickMsg: string
|
||||
}
|
||||
|
||||
export enum DataSource {
|
||||
LOCAL,
|
||||
REMOTE
|
||||
}
|
||||
|
||||
export enum GroupListUpdateType {
|
||||
REFRESHALL,
|
||||
GETALL,
|
||||
MODIFIED,
|
||||
REMOVE
|
||||
}
|
||||
|
||||
export interface GroupMemberCache {
|
||||
group: {
|
||||
data: GroupMember[];
|
||||
isExpired: boolean;
|
||||
};
|
||||
isExpired: boolean;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
groupCode: string,
|
||||
createTime?: string,//高版本才有
|
||||
maxMember: number,
|
||||
memberCount: number,
|
||||
groupName: string,
|
||||
groupStatus: number,
|
||||
memberRole: number,
|
||||
isTop: boolean,
|
||||
toppedTimestamp: string,
|
||||
privilegeFlag: number, //65760
|
||||
isConf: boolean,
|
||||
hasModifyConfGroupFace: boolean,
|
||||
hasModifyConfGroupName: boolean,
|
||||
remarkName: string,
|
||||
hasMemo: boolean,
|
||||
groupShutupExpireTime: string, //"0",
|
||||
personShutupExpireTime: string, //"0",
|
||||
discussToGroupUin: string, //"0",
|
||||
discussToGroupMaxMsgSeq: number,
|
||||
discussToGroupTime: number,
|
||||
groupFlagExt: number, //1073938496,
|
||||
authGroupType: number, //0,
|
||||
groupCreditLevel: number, //0,
|
||||
groupFlagExt3: number, //0,
|
||||
groupOwnerId: {
|
||||
memberUin: string, //"0",
|
||||
memberUid: string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
|
||||
}
|
||||
}
|
||||
|
||||
export enum GroupMemberRole {
|
||||
normal = 2,
|
||||
admin = 3,
|
||||
owner = 4
|
||||
}
|
||||
|
||||
export interface GroupMember {
|
||||
memberRealLevel: number | undefined;
|
||||
memberSpecialTitle?: string;
|
||||
avatarPath: string;
|
||||
cardName: string;
|
||||
cardType: number;
|
||||
isDelete: boolean;
|
||||
nick: string;
|
||||
qid: string;
|
||||
remark: string;
|
||||
role: GroupMemberRole; // 群主:4, 管理员:3,群员:2
|
||||
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚
|
||||
uid: string; // 加密的字符串
|
||||
uin: string; // QQ号
|
||||
isRobot: boolean;
|
||||
sex?: Sex;
|
||||
age?: number;
|
||||
qqLevel?: QQLevel;
|
||||
isChangeRole: boolean;
|
||||
joinTime: string;
|
||||
lastSpeakTime: string;
|
||||
}
|
||||
8
src/core/entities/index.ts
Normal file
8
src/core/entities/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './user';
|
||||
export * from './group';
|
||||
export * from './msg';
|
||||
export * from './notify';
|
||||
export * from './cache';
|
||||
export * from './system';
|
||||
export * from './webapi';
|
||||
export * from './sign';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user