mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-02 16:50:27 +00:00
Compare commits
3024 Commits
dependabot
...
v4.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -12,10 +12,10 @@ insert_final_newline = true
|
||||
# Set default charset
|
||||
charset = utf-8
|
||||
|
||||
# 4 space indentation
|
||||
# 2 space indentation
|
||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
[*.bat]
|
||||
charset = latin1
|
||||
|
||||
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
|
||||
19
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
19
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug 反馈
|
||||
description: 报告可能的 NapCat 异常行为
|
||||
title: "[BUG] "
|
||||
title: '[BUG] '
|
||||
labels: bug
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -10,16 +10,13 @@ body:
|
||||
在提交新的 Bug 反馈前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||
* 不与现有的某一 issue 重复
|
||||
* **不接受因发送不当内容而导致的问题报告**
|
||||
- 包括但不限于:多媒体发送失败、转发消息失败、消息被拦截等因 18+ 内容、违规内容或触发风控的问题
|
||||
- 提交 issue 前,请确认您发送的多媒体内容、链接、文本等均为正常合规内容,不会触发平台风控机制
|
||||
- 因违规内容导致的问题,一律不予受理
|
||||
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 运行 QQNT 的系统版本
|
||||
placeholder: Windows 11 24H2
|
||||
placeholder: Windows 10 Pro Workstation 22H2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -27,14 +24,14 @@ body:
|
||||
attributes:
|
||||
label: QQNT 版本
|
||||
description: 可在 QQNT 的「关于」的设置页中找到
|
||||
placeholder: 9.9.16-29927
|
||||
placeholder: 9.9.7-21804
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: napcat-version
|
||||
attributes:
|
||||
label: NapCat 版本
|
||||
description: 可在 WebUI 的「系统信息」页中找到
|
||||
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
|
||||
placeholder: 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
@@ -43,21 +40,21 @@ body:
|
||||
attributes:
|
||||
label: OneBot 客户端
|
||||
description: 连接至 NapCat 的客户端版本信息
|
||||
placeholder: Karin 1.0.0
|
||||
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: 发生了什么?
|
||||
description: 填写你认为的 NapCat 的异常行为
|
||||
description: 填写你认为的 NapCat 的不正常行为
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-reproduce
|
||||
attributes:
|
||||
label: 如何复现
|
||||
description: 填写应当如何操作才能触发这个异常行为
|
||||
description: 填写应当如何操作才能触发这个不正常行为
|
||||
placeholder: |
|
||||
1. xxx
|
||||
2. xxx
|
||||
|
||||
60
.github/ISSUE_TEMPLATE/feat_request.yml
vendored
60
.github/ISSUE_TEMPLATE/feat_request.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Feat 请求
|
||||
description: 提交新的 NapCat 功能或改进建议
|
||||
title: '[FEAT] '
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
欢迎来到 NapCat 的 Issue Tracker!请填写以下表格来提交功能请求。
|
||||
在提交新的功能请求前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到类似的建议
|
||||
* 不与现有的某一 issue 重复
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 运行 QQNT 的系统版本
|
||||
placeholder: Windows 11 24H2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: qqnt-version
|
||||
attributes:
|
||||
label: QQNT 版本
|
||||
description: 可在 QQNT 的「关于」的设置页中找到
|
||||
placeholder: 9.9.16-29927
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: napcat-version
|
||||
attributes:
|
||||
label: NapCat 版本
|
||||
description: 可在 WebUI 的「系统信息」页中找到
|
||||
placeholder: 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: 功能描述
|
||||
description: 请详细描述你希望添加的功能或改进
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-reason
|
||||
attributes:
|
||||
label: 需求背景与理由
|
||||
description: 请说明为什么需要这个功能,解决了什么问题
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-expected
|
||||
attributes:
|
||||
label: 期望的实现方式或效果
|
||||
description: 请描述你期望的功能实现方式或最终效果
|
||||
- type: textarea
|
||||
id: other-info
|
||||
attributes:
|
||||
label: 其他补充信息
|
||||
description: 你还想补充什么?
|
||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,6 +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"
|
||||
directory: "/"
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "daily"
|
||||
|
||||
43
.github/prompt/default.md
vendored
43
.github/prompt/default.md
vendored
@@ -1,43 +0,0 @@
|
||||
# {VERSION}
|
||||
[使用文档](https://napneko.github.io/)
|
||||
|
||||
## Windows 一键包
|
||||
我们提供了轻量化的一键部署方案
|
||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||
|
||||
你可以下载
|
||||
|
||||
NapCat.Shell.Windows.OneKey.zip (无头)
|
||||
|
||||
启动后可自动化部署一键包,教程参考使用文档安装部分
|
||||
|
||||
## 警告
|
||||
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
|
||||
**默认WebUi密钥为随机密码 控制台查看**
|
||||
|
||||
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
|
||||
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
|
||||
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
|
||||
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
|
||||
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
|
||||
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
|
||||
## 如果WinX64缺少运行库或者xxx.dll?
|
||||
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
## 更新
|
||||
|
||||
### 🐛 修复
|
||||
1. 修复 WebUI 主题配置在有未保存更改时卸载组件导致字体重置的问题 (ae42eed6)
|
||||
|
||||
### ✨ 新增
|
||||
1. 文件上传相关接口(UploadGroupFile/UploadPrivateFile)新增 `upload_file` 参数支持 (91e0839e)
|
||||
2. 消息发送逻辑支持 PTT(语音)元素过滤,确保语音消息正确独立发送 (47983e29)
|
||||
|
||||
### 🔧 优化
|
||||
1. 优化合并转发消息(GetForwardMsg)的获取与解析逻辑,提高兼容性 (334c4233)
|
||||
2. 改进消息发送方法中发送者 UIN 的处理逻辑 (71bb4f68)
|
||||
3. 增强 WebUI 系统信息界面中对构建产物的处理与展示 (cb061890)
|
||||
|
||||
---
|
||||
|
||||
**完整更新日志**: [v4.10.6...v4.10.7](https://github.com/NapNeko/NapCatQQ/compare/v4.10.6...v4.10.7)
|
||||
111
.github/prompt/release_note_prompt.txt
vendored
111
.github/prompt/release_note_prompt.txt
vendored
@@ -1,111 +0,0 @@
|
||||
# NapCat Release Note Generator
|
||||
|
||||
你是 NapCat 项目的发布说明生成器。请根据提供的 commit 列表生成标准格式的发布说明。
|
||||
|
||||
## 核心规则
|
||||
|
||||
1. **版本号**:第一行必须是 `# {VERSION}`,使用用户提供的版本号,如果版本号是小写 v 开头(如 v4.10.2),必须转换为大写 V(如 V4.10.2)
|
||||
2. **语言**:全部使用简体中文
|
||||
3. **格式**:严格按照下方模板输出,不要添加额外的 markdown 格式
|
||||
|
||||
## Commit 分析规则
|
||||
|
||||
将 commit 分类为以下类型:
|
||||
- 🐛 **修复**:bug fix、修复、fix 相关
|
||||
- ✨ **新增**:新功能、feat、add 相关
|
||||
- 🔧 **优化**:优化、重构、refactor、improve、perf 相关
|
||||
- 📦 **依赖**:deps、依赖更新(通常可以忽略或合并)
|
||||
- 🔨 **构建**:ci、build、workflow 相关(通常可以忽略)
|
||||
|
||||
## 合并和筛选
|
||||
|
||||
- **合并相似项**:同一功能的多个 commit 合并为一条
|
||||
- **忽略琐碎项**:合并冲突、格式化、typo 等可忽略
|
||||
- **控制数量**:最终保持 5-15 条更新要点
|
||||
- **保留 commit hash**:每条末尾附上短 hash,格式 `(a1b2c3d)`
|
||||
|
||||
## 输出模板 - 必须严格遵守以下格式
|
||||
|
||||
```
|
||||
# {VERSION}
|
||||
[使用文档](https://napneko.github.io/)
|
||||
|
||||
## Windows 一键包
|
||||
我们提供了轻量化的一键部署方案
|
||||
相对于普通需要安装QQ的方案,下面已内置QQ和Napcat 阅读使用文档参考
|
||||
|
||||
你可以下载
|
||||
|
||||
NapCat.Shell.Windows.OneKey.zip (无头)
|
||||
|
||||
启动后可自动化部署一键包,教程参考使用文档安装部分
|
||||
|
||||
## 警告
|
||||
**注意QQ版本推荐使用 40768+ 版本 最低可以使用40768版本**
|
||||
**默认WebUi密钥为随机密码 控制台查看**
|
||||
|
||||
**[9.9.26-44343 X64 Win](https://dldir1.qq.com/qqfile/qq/QQNT/40d6045a/QQ9.9.26.44343_x64.exe)**
|
||||
[LinuxX64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_amd64.deb)
|
||||
[LinuxX64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.rpm)
|
||||
[LinuxArm64 DEB 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.deb)
|
||||
[LinuxArm64 RPM 44343 ](https://dldir1.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_aarch64.rpm)
|
||||
[MAC DMG 40990 ](https://dldir1v6.qq.com/qqfile/qq/QQNT/c6cb0f5d/QQ_v6.9.82.40990.dmg)
|
||||
## 如果WinX64缺少运行库或者xxx.dll?
|
||||
[安装运行库](https://aka.ms/vs/17/release/vc_redist.x64.exe)
|
||||
|
||||
## 更新
|
||||
|
||||
### 🐛 修复
|
||||
1. 修复 xxx 问题 (a1b2c3d)
|
||||
2. 修复 yyy 崩溃 (b2c3d4e)
|
||||
|
||||
### ✨ 新增
|
||||
1. 新增 xxx 功能 (c3d4e5f)
|
||||
2. 支持 yyy 特性 (d4e5f6g)
|
||||
|
||||
### 🔧 优化
|
||||
1. 优化 xxx 性能 (e5f6g7h)
|
||||
2. 重构 yyy 模块 (f6g7h8i)
|
||||
|
||||
---
|
||||
|
||||
**完整更新日志**: [{PREV_VERSION}...{VERSION}](https://github.com/NapNeko/NapCatQQ/compare/{PREV_VERSION}...{VERSION})
|
||||
```
|
||||
|
||||
**格式要求 - 务必严格遵守:**
|
||||
- "Windows 一键包"部分的文本必须完全一致,不要修改任何措辞
|
||||
- "警告"部分必须包含所有 QQ 版本下载链接,保持原有格式
|
||||
- "如果WinX64缺少运行库或者xxx.dll?"这一行必须保持原样
|
||||
- QQ 版本号和下载链接保持不变(40990 版本)
|
||||
- 只有"## 更新"部分下面的内容需要根据实际 commit 生成
|
||||
|
||||
## 重要约束
|
||||
|
||||
1. 如果某个分类没有内容,则完全省略该分类
|
||||
2. 不要编造不存在的更新
|
||||
3. 保持简洁,每条更新控制在一行内
|
||||
4. 使用用户友好的语言,避免过于技术化的描述
|
||||
5. 重大变更(Breaking Changes)需要在注意事项中加粗提示
|
||||
|
||||
## 文件变化分析
|
||||
|
||||
用户会提供文件变化统计和具体代码diff,帮助你理解变更内容:
|
||||
|
||||
### 目录含义
|
||||
- `packages/napcat-core/` → 核心功能、消息处理、QQ接口
|
||||
- `packages/napcat-onebot/` → OneBot 协议实现、API、事件
|
||||
- `packages/napcat-webui-backend/` → WebUI 后端接口
|
||||
- `packages/napcat-webui-frontend/` → WebUI 前端界面
|
||||
- `packages/napcat-shell/` → Shell 启动器
|
||||
|
||||
### 代码diff阅读指南
|
||||
- `+` 开头的行是新增代码
|
||||
- `-` 开头的行是删除代码
|
||||
- 关注函数名、类名的变化来理解功能变更
|
||||
- 关注 `fix`、`bug`、`error` 等关键词识别修复项
|
||||
- 关注 `add`、`new`、`feature` 等关键词识别新功能
|
||||
- 忽略纯重构(代码移动但功能不变)和格式化变更
|
||||
|
||||
### 截断说明
|
||||
- 如果看到 `[... 已截断 ...]`,表示内容过长被截断
|
||||
- 根据已有信息推断完整变更意图即可
|
||||
231
.github/scripts/lib/comment.ts
vendored
231
.github/scripts/lib/comment.ts
vendored
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* 构建状态评论模板
|
||||
*/
|
||||
|
||||
export const COMMENT_MARKER = '<!-- napcat-pr-build -->';
|
||||
|
||||
export type BuildStatus = 'success' | 'failure' | 'cancelled' | 'pending' | 'unknown';
|
||||
|
||||
export interface BuildTarget {
|
||||
name: string;
|
||||
status: BuildStatus;
|
||||
error?: string;
|
||||
downloadUrl?: string; // Artifact 直接下载链接
|
||||
}
|
||||
|
||||
// ============== 辅助函数 ==============
|
||||
|
||||
function formatSha (sha: string): string {
|
||||
return sha && sha.length >= 7 ? sha.substring(0, 7) : sha || 'unknown';
|
||||
}
|
||||
|
||||
function escapeCodeBlock (text: string): string {
|
||||
// 替换 ``` 为转义形式,避免破坏 Markdown 代码块
|
||||
return text.replace(/```/g, '\\`\\`\\`');
|
||||
}
|
||||
|
||||
function getTimeString (): string {
|
||||
return new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
}
|
||||
|
||||
// ============== 状态图标 ==============
|
||||
|
||||
export function getStatusIcon (status: BuildStatus): string {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return '✅ 成功';
|
||||
case 'pending':
|
||||
return '⏳ 构建中...';
|
||||
case 'cancelled':
|
||||
return '⚪ 已取消';
|
||||
case 'failure':
|
||||
return '❌ 失败';
|
||||
default:
|
||||
return '❓ 未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusEmoji (status: BuildStatus): string {
|
||||
switch (status) {
|
||||
case 'success': return '✅';
|
||||
case 'pending': return '⏳';
|
||||
case 'cancelled': return '⚪';
|
||||
case 'failure': return '❌';
|
||||
default: return '❓';
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 构建中评论 ==============
|
||||
|
||||
export function generateBuildingComment (prSha: string, targets: string[]): string {
|
||||
const time = getTimeString();
|
||||
const shortSha = formatSha(prSha);
|
||||
|
||||
const lines: string[] = [
|
||||
COMMENT_MARKER,
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
'# 🔨 NapCat 构建中',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'</div>',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📦 构建目标',
|
||||
'',
|
||||
'| 包名 | 状态 | 说明 |',
|
||||
'| :--- | :---: | :--- |',
|
||||
...targets.map(name => `| \`${name}\` | ⏳ | 正在构建... |`),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📋 构建信息',
|
||||
'',
|
||||
`| 项目 | 值 |`,
|
||||
`| :--- | :--- |`,
|
||||
`| 📝 提交 | \`${shortSha}\` |`,
|
||||
`| 🕐 开始时间 | ${time} |`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⏳ **构建进行中,请稍候...**',
|
||||
'>',
|
||||
'> 构建完成后将自动更新此评论',
|
||||
'',
|
||||
'</div>',
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ============== 构建结果评论 ==============
|
||||
|
||||
export function generateResultComment (
|
||||
targets: BuildTarget[],
|
||||
prSha: string,
|
||||
runId: string,
|
||||
repository: string,
|
||||
version?: string
|
||||
): string {
|
||||
const runUrl = `https://github.com/${repository}/actions/runs/${runId}`;
|
||||
const shortSha = formatSha(prSha);
|
||||
const time = getTimeString();
|
||||
|
||||
const allSuccess = targets.every(t => t.status === 'success');
|
||||
const anyCancelled = targets.some(t => t.status === 'cancelled');
|
||||
const anyFailure = targets.some(t => t.status === 'failure');
|
||||
|
||||
// 状态徽章
|
||||
let statusBadge: string;
|
||||
let headerTitle: string;
|
||||
if (allSuccess) {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ✅ NapCat 构建成功';
|
||||
} else if (anyCancelled && !anyFailure) {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ⚪ NapCat 构建已取消';
|
||||
} else {
|
||||
statusBadge = '';
|
||||
headerTitle = '# ❌ NapCat 构建失败';
|
||||
}
|
||||
|
||||
const downloadLink = (target: BuildTarget) => {
|
||||
if (target.status !== 'success') return '—';
|
||||
if (target.downloadUrl) {
|
||||
return `[📥 下载](${target.downloadUrl})`;
|
||||
}
|
||||
return `[📥 下载](${runUrl}#artifacts)`;
|
||||
};
|
||||
|
||||
const lines: string[] = [
|
||||
COMMENT_MARKER,
|
||||
'',
|
||||
'<div align="center">',
|
||||
'',
|
||||
headerTitle,
|
||||
'',
|
||||
statusBadge,
|
||||
'',
|
||||
'</div>',
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📦 构建产物',
|
||||
'',
|
||||
'| 包名 | 状态 | 下载 |',
|
||||
'| :--- | :---: | :---: |',
|
||||
...targets.map(t => `| \`${t.name}\` | ${getStatusEmoji(t.status)} ${t.status === 'success' ? '成功' : t.status === 'failure' ? '失败' : t.status === 'cancelled' ? '已取消' : '未知'} | ${downloadLink(t)} |`),
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
'## 📋 构建信息',
|
||||
'',
|
||||
`| 项目 | 值 |`,
|
||||
`| :--- | :--- |`,
|
||||
...(version ? [`| 🏷️ 版本号 | \`${version}\` |`] : []),
|
||||
`| 📝 提交 | \`${shortSha}\` |`,
|
||||
`| 🔗 构建日志 | [查看详情](${runUrl}) |`,
|
||||
`| 🕐 完成时间 | ${time} |`,
|
||||
];
|
||||
|
||||
// 添加错误详情
|
||||
const failedTargets = targets.filter(t => t.status === 'failure' && t.error);
|
||||
if (failedTargets.length > 0) {
|
||||
lines.push('', '---', '', '## ⚠️ 错误详情', '');
|
||||
for (const target of failedTargets) {
|
||||
lines.push(
|
||||
`<details>`,
|
||||
`<summary>🔴 <b>${target.name}</b> 构建错误</summary>`,
|
||||
'',
|
||||
'```',
|
||||
escapeCodeBlock(target.error!),
|
||||
'```',
|
||||
'',
|
||||
'</details>',
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加底部提示
|
||||
lines.push('---', '');
|
||||
if (allSuccess) {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> 🎉 **所有构建均已成功完成!**',
|
||||
'>',
|
||||
'> 点击上方下载链接获取构建产物进行测试',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
} else if (anyCancelled && !anyFailure) {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⚪ **构建已被取消**',
|
||||
'>',
|
||||
'> 可能是由于新的提交触发了新的构建',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
'<div align="center">',
|
||||
'',
|
||||
'> ⚠️ **部分构建失败**',
|
||||
'>',
|
||||
'> 请查看上方错误详情或点击构建日志查看完整输出',
|
||||
'',
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
189
.github/scripts/lib/github.ts
vendored
189
.github/scripts/lib/github.ts
vendored
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* GitHub API 工具库
|
||||
*/
|
||||
|
||||
import { appendFileSync } from 'node:fs';
|
||||
|
||||
// ============== 类型定义 ==============
|
||||
|
||||
export interface PullRequest {
|
||||
number: number;
|
||||
state: string;
|
||||
head: {
|
||||
sha: string;
|
||||
ref: string;
|
||||
repo: {
|
||||
full_name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
owner: {
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
id: number;
|
||||
name: string;
|
||||
size_in_bytes: number;
|
||||
archive_download_url: string;
|
||||
}
|
||||
|
||||
// ============== GitHub API Client ==========================
|
||||
|
||||
export class GitHubAPI {
|
||||
private token: string;
|
||||
private baseUrl = 'https://api.github.com';
|
||||
|
||||
constructor (token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
private async request<T> (endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async getPullRequest (owner: string, repo: string, pullNumber: number): Promise<PullRequest> {
|
||||
return this.request<PullRequest>(`/repos/${owner}/${repo}/pulls/${pullNumber}`);
|
||||
}
|
||||
|
||||
async getCollaboratorPermission (owner: string, repo: string, username: string): Promise<string> {
|
||||
const data = await this.request<{ permission: string; }>(
|
||||
`/repos/${owner}/${repo}/collaborators/${username}/permission`
|
||||
);
|
||||
return data.permission;
|
||||
}
|
||||
|
||||
async getRepository (owner: string, repo: string): Promise<Repository> {
|
||||
return this.request(`/repos/${owner}/${repo}`);
|
||||
}
|
||||
|
||||
async checkOrgMembership (org: string, username: string): Promise<boolean> {
|
||||
try {
|
||||
await this.request(`/orgs/${org}/members/${username}`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getRunArtifacts (owner: string, repo: string, runId: string): Promise<Artifact[]> {
|
||||
const data = await this.request<{ artifacts: Artifact[]; }>(
|
||||
`/repos/${owner}/${repo}/actions/runs/${runId}/artifacts`
|
||||
);
|
||||
return data.artifacts;
|
||||
}
|
||||
|
||||
async createComment (owner: string, repo: string, issueNumber: number, body: string): Promise<void> {
|
||||
await this.request(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ body }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async findComment (owner: string, repo: string, issueNumber: number, marker: string): Promise<number | null> {
|
||||
let page = 1;
|
||||
const perPage = 100;
|
||||
|
||||
while (page <= 10) { // 最多检查 1000 条评论
|
||||
const comments = await this.request<Array<{ id: number, body: string; }>>(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=${perPage}&page=${page}`
|
||||
);
|
||||
|
||||
if (comments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const found = comments.find(c => c.body.includes(marker));
|
||||
if (found) {
|
||||
return found.id;
|
||||
}
|
||||
|
||||
if (comments.length < perPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async updateComment (owner: string, repo: string, commentId: number, body: string): Promise<void> {
|
||||
await this.request(`/repos/${owner}/${repo}/issues/comments/${commentId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ body }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async createOrUpdateComment (
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
body: string,
|
||||
marker: string
|
||||
): Promise<void> {
|
||||
const existingId = await this.findComment(owner, repo, issueNumber, marker);
|
||||
if (existingId) {
|
||||
await this.updateComment(owner, repo, existingId, body);
|
||||
console.log(`✓ Updated comment #${existingId}`);
|
||||
} else {
|
||||
await this.createComment(owner, repo, issueNumber, body);
|
||||
console.log('✓ Created new comment');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Output 工具 ==============
|
||||
|
||||
export function setOutput (name: string, value: string): void {
|
||||
const outputFile = process.env.GITHUB_OUTPUT;
|
||||
if (outputFile) {
|
||||
appendFileSync(outputFile, `${name}=${value}\n`);
|
||||
}
|
||||
console.log(` ${name}=${value}`);
|
||||
}
|
||||
|
||||
export function setMultilineOutput (name: string, value: string): void {
|
||||
const outputFile = process.env.GITHUB_OUTPUT;
|
||||
if (outputFile) {
|
||||
const delimiter = `EOF_${Date.now()}`;
|
||||
appendFileSync(outputFile, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 环境变量工具 ==============
|
||||
|
||||
export function getEnv (name: string, required: true): string;
|
||||
export function getEnv (name: string, required?: false): string | undefined;
|
||||
export function getEnv (name: string, required = false): string | undefined {
|
||||
const value = process.env[name];
|
||||
if (required && !value) {
|
||||
throw new Error(`Environment variable ${name} is required`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getRepository (): { owner: string, repo: string; } {
|
||||
const repository = getEnv('GITHUB_REPOSITORY', true);
|
||||
const [owner, repo] = repository.split('/');
|
||||
return { owner, repo };
|
||||
}
|
||||
36
.github/scripts/pr-build-building.ts
vendored
36
.github/scripts/pr-build-building.ts
vendored
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* PR Build - 更新构建中状态评论
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - PR_NUMBER: PR 编号
|
||||
* - PR_SHA: PR 提交 SHA
|
||||
*/
|
||||
|
||||
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||
import { generateBuildingComment, COMMENT_MARKER } from './lib/comment.ts';
|
||||
|
||||
const BUILD_TARGETS = ['NapCat.Framework', 'NapCat.Shell'];
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('🔨 Updating building status comment\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||
const prSha = getEnv('PR_SHA', true);
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
console.log(`PR: #${prNumber}`);
|
||||
console.log(`SHA: ${prSha}`);
|
||||
console.log(`Repo: ${owner}/${repo}\n`);
|
||||
|
||||
const github = new GitHubAPI(token);
|
||||
const comment = generateBuildingComment(prSha, BUILD_TARGETS);
|
||||
|
||||
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
206
.github/scripts/pr-build-check.ts
vendored
206
.github/scripts/pr-build-check.ts
vendored
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* PR Build Check Script
|
||||
* 检查 PR 构建触发条件和用户权限
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - GITHUB_EVENT_NAME: 事件名称
|
||||
* - GITHUB_EVENT_PATH: 事件 payload 文件路径
|
||||
* - GITHUB_REPOSITORY: 仓库名称 (owner/repo)
|
||||
* - GITHUB_OUTPUT: 输出文件路径
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { GitHubAPI, getEnv, getRepository, setOutput } from './lib/github.ts';
|
||||
import type { PullRequest } from './lib/github.ts';
|
||||
|
||||
// ============== 类型定义 ==============
|
||||
|
||||
interface GitHubPayload {
|
||||
pull_request?: PullRequest;
|
||||
issue?: {
|
||||
number: number;
|
||||
pull_request?: object;
|
||||
};
|
||||
comment?: {
|
||||
body: string;
|
||||
user: { login: string; };
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckResult {
|
||||
should_build: boolean;
|
||||
pr_number?: number;
|
||||
pr_sha?: string;
|
||||
pr_head_repo?: string;
|
||||
pr_head_ref?: string;
|
||||
}
|
||||
|
||||
// ============== 权限检查 ==============
|
||||
|
||||
async function checkUserPermission (
|
||||
github: GitHubAPI,
|
||||
owner: string,
|
||||
repo: string,
|
||||
username: string
|
||||
): Promise<boolean> {
|
||||
// 方法1:检查仓库协作者权限
|
||||
try {
|
||||
const permission = await github.getCollaboratorPermission(owner, repo, username);
|
||||
if (['admin', 'write', 'maintain'].includes(permission)) {
|
||||
console.log(`✓ User ${username} has ${permission} permission`);
|
||||
return true;
|
||||
}
|
||||
console.log(`✗ User ${username} has ${permission} permission (insufficient)`);
|
||||
} catch (e) {
|
||||
console.log(`✗ Failed to get collaborator permission: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
// 方法2:检查组织成员身份
|
||||
try {
|
||||
const repoInfo = await github.getRepository(owner, repo);
|
||||
if (repoInfo.owner.type === 'Organization') {
|
||||
const isMember = await github.checkOrgMembership(owner, username);
|
||||
if (isMember) {
|
||||
console.log(`✓ User ${username} is organization member`);
|
||||
return true;
|
||||
}
|
||||
console.log(`✗ User ${username} is not organization member`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`✗ Failed to check org membership: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============== 事件处理 ==============
|
||||
|
||||
function handlePullRequestTarget (payload: GitHubPayload): CheckResult {
|
||||
const pr = payload.pull_request;
|
||||
|
||||
if (!pr) {
|
||||
console.log('✗ No pull_request in payload');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
if (pr.state !== 'open') {
|
||||
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`✓ PR #${pr.number} is open, triggering build`);
|
||||
return {
|
||||
should_build: true,
|
||||
pr_number: pr.number,
|
||||
pr_sha: pr.head.sha,
|
||||
pr_head_repo: pr.head.repo.full_name,
|
||||
pr_head_ref: pr.head.ref,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleIssueComment (
|
||||
payload: GitHubPayload,
|
||||
github: GitHubAPI,
|
||||
owner: string,
|
||||
repo: string
|
||||
): Promise<CheckResult> {
|
||||
const { issue, comment } = payload;
|
||||
|
||||
if (!issue || !comment) {
|
||||
console.log('✗ No issue or comment in payload');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查是否是 PR 的评论
|
||||
if (!issue.pull_request) {
|
||||
console.log('✗ Comment is not on a PR');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查是否是 /build 命令
|
||||
if (!comment.body.trim().startsWith('/build')) {
|
||||
console.log('✗ Comment is not a /build command');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`→ /build command from @${comment.user.login}`);
|
||||
|
||||
// 获取 PR 详情
|
||||
const pr = await github.getPullRequest(owner, repo, issue.number);
|
||||
|
||||
// 检查 PR 状态
|
||||
if (pr.state !== 'open') {
|
||||
console.log(`✗ PR is not open (state: ${pr.state})`);
|
||||
await github.createComment(owner, repo, issue.number, '⚠️ 此 PR 已关闭,无法触发构建。');
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
// 检查用户权限
|
||||
const username = comment.user.login;
|
||||
const hasPermission = await checkUserPermission(github, owner, repo, username);
|
||||
|
||||
if (!hasPermission) {
|
||||
console.log(`✗ User ${username} has no permission`);
|
||||
await github.createComment(
|
||||
owner,
|
||||
repo,
|
||||
issue.number,
|
||||
`⚠️ @${username} 您没有权限使用 \`/build\` 命令,仅仓库协作者或组织成员可使用。`
|
||||
);
|
||||
return { should_build: false };
|
||||
}
|
||||
|
||||
console.log(`✓ Build triggered by @${username}`);
|
||||
return {
|
||||
should_build: true,
|
||||
pr_number: issue.number,
|
||||
pr_sha: pr.head.sha,
|
||||
pr_head_repo: pr.head.repo.full_name,
|
||||
pr_head_ref: pr.head.ref,
|
||||
};
|
||||
}
|
||||
|
||||
// ============== 主函数 ==============
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('🔍 PR Build Check\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const eventName = getEnv('GITHUB_EVENT_NAME', true);
|
||||
const eventPath = getEnv('GITHUB_EVENT_PATH', true);
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
console.log(`Event: ${eventName}`);
|
||||
console.log(`Repository: ${owner}/${repo}\n`);
|
||||
|
||||
const payload = JSON.parse(readFileSync(eventPath, 'utf-8')) as GitHubPayload;
|
||||
const github = new GitHubAPI(token);
|
||||
|
||||
let result: CheckResult;
|
||||
|
||||
switch (eventName) {
|
||||
case 'pull_request_target':
|
||||
result = handlePullRequestTarget(payload);
|
||||
break;
|
||||
case 'issue_comment':
|
||||
result = await handleIssueComment(payload, github, owner, repo);
|
||||
break;
|
||||
default:
|
||||
console.log(`✗ Unsupported event: ${eventName}`);
|
||||
result = { should_build: false };
|
||||
}
|
||||
|
||||
// 输出结果
|
||||
console.log('\n=== Outputs ===');
|
||||
setOutput('should_build', String(result.should_build));
|
||||
setOutput('pr_number', String(result.pr_number ?? ''));
|
||||
setOutput('pr_sha', result.pr_sha ?? '');
|
||||
setOutput('pr_head_repo', result.pr_head_repo ?? '');
|
||||
setOutput('pr_head_ref', result.pr_head_ref ?? '');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
90
.github/scripts/pr-build-result.ts
vendored
90
.github/scripts/pr-build-result.ts
vendored
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* PR Build - 更新构建结果评论
|
||||
*
|
||||
* 环境变量:
|
||||
* - GITHUB_TOKEN: GitHub API Token
|
||||
* - PR_NUMBER: PR 编号
|
||||
* - PR_SHA: PR 提交 SHA
|
||||
* - RUN_ID: GitHub Actions Run ID
|
||||
* - NAPCAT_VERSION: 构建版本号
|
||||
* - FRAMEWORK_STATUS: Framework 构建状态
|
||||
* - FRAMEWORK_ERROR: Framework 构建错误信息
|
||||
* - SHELL_STATUS: Shell 构建状态
|
||||
* - SHELL_ERROR: Shell 构建错误信息
|
||||
*/
|
||||
|
||||
import { GitHubAPI, getEnv, getRepository } from './lib/github.ts';
|
||||
import { generateResultComment, COMMENT_MARKER } from './lib/comment.ts';
|
||||
import type { BuildTarget, BuildStatus } from './lib/comment.ts';
|
||||
|
||||
function parseStatus (value: string | undefined): BuildStatus {
|
||||
if (value === 'success' || value === 'failure' || value === 'cancelled') {
|
||||
return value;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
async function main (): Promise<void> {
|
||||
console.log('📝 Updating build result comment\n');
|
||||
|
||||
const token = getEnv('GITHUB_TOKEN', true);
|
||||
const prNumber = parseInt(getEnv('PR_NUMBER', true), 10);
|
||||
const prSha = getEnv('PR_SHA') || 'unknown';
|
||||
const runId = getEnv('RUN_ID', true);
|
||||
const version = getEnv('NAPCAT_VERSION') || '';
|
||||
const { owner, repo } = getRepository();
|
||||
|
||||
const frameworkStatus = parseStatus(getEnv('FRAMEWORK_STATUS'));
|
||||
const frameworkError = getEnv('FRAMEWORK_ERROR');
|
||||
const shellStatus = parseStatus(getEnv('SHELL_STATUS'));
|
||||
const shellError = getEnv('SHELL_ERROR');
|
||||
|
||||
console.log(`PR: #${prNumber}`);
|
||||
console.log(`SHA: ${prSha}`);
|
||||
console.log(`Version: ${version}`);
|
||||
console.log(`Run: ${runId}`);
|
||||
console.log(`Framework: ${frameworkStatus}${frameworkError ? ` (${frameworkError})` : ''}`);
|
||||
console.log(`Shell: ${shellStatus}${shellError ? ` (${shellError})` : ''}\n`);
|
||||
|
||||
const github = new GitHubAPI(token);
|
||||
const repository = `${owner}/${repo}`;
|
||||
|
||||
// 获取 artifacts 列表,生成直接下载链接
|
||||
const artifactMap: Record<string, string> = {};
|
||||
try {
|
||||
const artifacts = await github.getRunArtifacts(owner, repo, runId);
|
||||
console.log(`Found ${artifacts.length} artifacts`);
|
||||
for (const artifact of artifacts) {
|
||||
// 生成直接下载链接:https://github.com/{owner}/{repo}/actions/runs/{run_id}/artifacts/{artifact_id}
|
||||
const downloadUrl = `https://github.com/${repository}/actions/runs/${runId}/artifacts/${artifact.id}`;
|
||||
artifactMap[artifact.name] = downloadUrl;
|
||||
console.log(` - ${artifact.name}: ${downloadUrl}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Warning: Failed to get artifacts: ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
const targets: BuildTarget[] = [
|
||||
{
|
||||
name: 'NapCat.Framework',
|
||||
status: frameworkStatus,
|
||||
error: frameworkError,
|
||||
downloadUrl: artifactMap['NapCat.Framework'],
|
||||
},
|
||||
{
|
||||
name: 'NapCat.Shell',
|
||||
status: shellStatus,
|
||||
error: shellError,
|
||||
downloadUrl: artifactMap['NapCat.Shell'],
|
||||
},
|
||||
];
|
||||
|
||||
const comment = generateResultComment(targets, prSha, runId, repository, version);
|
||||
|
||||
await github.createOrUpdateComment(owner, repo, prNumber, comment, COMMENT_MARKER);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('❌ Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
149
.github/scripts/pr-build-run.ts
vendored
149
.github/scripts/pr-build-run.ts
vendored
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* PR Build Runner
|
||||
* 执行构建步骤
|
||||
*
|
||||
* 用法: node pr-build-run.ts <target>
|
||||
* target: framework | shell
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, renameSync, unlinkSync } from 'node:fs';
|
||||
import { setOutput } from './lib/github.ts';
|
||||
|
||||
type BuildTarget = 'framework' | 'shell';
|
||||
|
||||
interface BuildStep {
|
||||
name: string;
|
||||
command: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
// ============== 构建步骤 ==============
|
||||
|
||||
function getCommonSteps (): BuildStep[] {
|
||||
return [
|
||||
{
|
||||
name: 'Install pnpm',
|
||||
command: 'npm i -g pnpm',
|
||||
errorMessage: 'Failed to install pnpm',
|
||||
},
|
||||
{
|
||||
name: 'Install dependencies',
|
||||
command: 'pnpm i',
|
||||
errorMessage: 'Failed to install dependencies',
|
||||
},
|
||||
{
|
||||
name: 'Type check',
|
||||
command: 'pnpm run typecheck',
|
||||
errorMessage: 'Type check failed',
|
||||
},
|
||||
{
|
||||
name: 'Test',
|
||||
command: 'pnpm test',
|
||||
errorMessage: 'Tests failed',
|
||||
},
|
||||
{
|
||||
name: 'Build WebUI',
|
||||
command: 'pnpm --filter napcat-webui-frontend run build',
|
||||
errorMessage: 'WebUI build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getTargetSteps (target: BuildTarget): BuildStep[] {
|
||||
if (target === 'framework') {
|
||||
return [
|
||||
{
|
||||
name: 'Build Framework',
|
||||
command: 'pnpm run build:framework',
|
||||
errorMessage: 'Framework build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: 'Build Shell',
|
||||
command: 'pnpm run build:shell',
|
||||
errorMessage: 'Shell build failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ============== 执行器 ==============
|
||||
|
||||
function runStep (step: BuildStep): boolean {
|
||||
console.log(`\n::group::${step.name}`);
|
||||
console.log(`> ${step.command}\n`);
|
||||
|
||||
try {
|
||||
execSync(step.command, {
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||
});
|
||||
console.log('::endgroup::');
|
||||
console.log(`✓ ${step.name}`);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
console.log('::endgroup::');
|
||||
console.log(`✗ ${step.name}`);
|
||||
setOutput('error', step.errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function postBuild (target: BuildTarget): void {
|
||||
const srcDir = target === 'framework'
|
||||
? 'packages/napcat-framework/dist'
|
||||
: 'packages/napcat-shell/dist';
|
||||
const destDir = target === 'framework' ? 'framework-dist' : 'shell-dist';
|
||||
|
||||
console.log(`\n→ Moving ${srcDir} to ${destDir}`);
|
||||
|
||||
if (!existsSync(srcDir)) {
|
||||
throw new Error(`Build output not found: ${srcDir}`);
|
||||
}
|
||||
|
||||
renameSync(srcDir, destDir);
|
||||
|
||||
// Install production dependencies
|
||||
console.log('→ Installing production dependencies');
|
||||
execSync('npm install --omit=dev', {
|
||||
cwd: destDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/bash',
|
||||
});
|
||||
|
||||
// Remove package-lock.json
|
||||
const lockFile = `${destDir}/package-lock.json`;
|
||||
if (existsSync(lockFile)) {
|
||||
unlinkSync(lockFile);
|
||||
}
|
||||
|
||||
console.log(`✓ Build output ready at ${destDir}`);
|
||||
}
|
||||
|
||||
// ============== 主函数 ==============
|
||||
|
||||
function main (): void {
|
||||
const target = process.argv[2] as BuildTarget;
|
||||
|
||||
if (!target || !['framework', 'shell'].includes(target)) {
|
||||
console.error('Usage: node pr-build-run.ts <framework|shell>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔨 Building NapCat.${target === 'framework' ? 'Framework' : 'Shell'}\n`);
|
||||
|
||||
const steps = [...getCommonSteps(), ...getTargetSteps(target)];
|
||||
|
||||
for (const step of steps) {
|
||||
if (!runStep(step)) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
postBuild(target);
|
||||
console.log('\n✅ Build completed successfully!');
|
||||
}
|
||||
|
||||
main();
|
||||
153
.github/workflows/auto-release.yml
vendored
153
.github/workflows/auto-release.yml
vendored
@@ -1,153 +0,0 @@
|
||||
name: Auto Release Docker
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-schema:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get Version
|
||||
id: get_version
|
||||
run: |
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
version=${latest_tag#v}
|
||||
echo "version=${version}" >> $GITHUB_ENV
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
echo "Debug: Version is ${version}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build napcat-schema
|
||||
run: |
|
||||
cd packages/napcat-schema
|
||||
pnpm run build:openapi
|
||||
|
||||
- name: Checkout NapCatDocs
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: NapNeko/NapCatDocs
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
path: napcat-docs
|
||||
|
||||
- name: Copy OpenAPI Schema
|
||||
run: |
|
||||
mkdir -p napcat-docs/src/api/${{ env.version }}
|
||||
cp packages/napcat-schema/dist/openapi.json napcat-docs/src/api/${{ env.version }}/openapi.json
|
||||
echo "OpenAPI schema copied to napcat-docs/src/api/${{ env.version }}/openapi.json"
|
||||
|
||||
- name: Commit and Push
|
||||
run: |
|
||||
cd napcat-docs
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add src/api/${{ env.version }}/openapi.json
|
||||
git commit -m "chore: update OpenAPI schema for version ${{ env.version }}" || echo "No changes to commit"
|
||||
git push
|
||||
|
||||
shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger NapCat-Docker docker-publish workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCat-Docker/actions/workflows/docker-publish.yml/dispatches \
|
||||
-d '{"ref":"main"}'
|
||||
framework-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger NapCat-Framework-Docker docker-publish workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCat.Docker.Framework/actions/workflows/docker-image.yml/dispatches \
|
||||
-d '{"ref":"main"}'
|
||||
appimage-shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Get Latest NapCat Version
|
||||
id: get_version
|
||||
run: |
|
||||
# 获取当前仓库的最新 tag
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# 输出调试信息
|
||||
echo "Debug: Latest NapCat Version is ${latest_tag}"
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
- name: Trigger Release NapCat AppImage Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||
run: |
|
||||
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_version_x86_64=${QQ_VERSION_X86_64}, qq_version_arm64=${QQ_VERSION_ARM64}"
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCatAppImageBuild/actions/workflows/release.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_version_x86_64\":\"${QQ_VERSION_X86_64}\",\"qq_version_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||
node-shell-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Get Latest NapCat Version
|
||||
id: get_version
|
||||
run: |
|
||||
# 获取当前仓库的最新 tag
|
||||
latest_tag=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# 输出调试信息
|
||||
echo "Debug: Latest NapCat Version is ${latest_tag}"
|
||||
echo "latest_tag=${latest_tag}" >> $GITHUB_ENV
|
||||
- name: Trigger Release NapCat AppImage Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||
run: |
|
||||
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/release.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||
- name: Trigger Release NapCat AppImage Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NAPCAT_BUILD }}
|
||||
NAPCAT_VERSION: ${{ env.latest_tag }}
|
||||
QQ_VERSION_X86_64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_x86_64.AppImage' # 写死 QQ 版本
|
||||
QQ_VERSION_ARM64: 'https://dldir1v6.qq.com/qqfile/qq/QQNT/94704804/linuxqq_3.2.23-44343_arm64.AppImage' # 写死 QQ 版本
|
||||
run: |
|
||||
echo "Debug: Triggering Release NapCat AppImage with napcat_version=${NAPCAT_VERSION}, qq_url_amd64=${QQ_VERSION_X86_64}, qq_url_arm64=${QQ_VERSION_ARM64}"
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
https://api.github.com/repos/NapNeko/NapCatLinuxNodeLoader/actions/workflows/docker-publish.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"napcat_version\":\"${NAPCAT_VERSION}\",\"qq_url_amd64\":\"${QQ_VERSION_X86_64}\",\"qq_url_arm64\":\"${QQ_VERSION_ARM64}\"}}"
|
||||
140
.github/workflows/build.yml
vendored
140
.github/workflows/build.yml
vendored
@@ -1,96 +1,70 @@
|
||||
name: Build NapCat Artifacts
|
||||
name: "Build Action"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
Build-Framework:
|
||||
Build-LiteLoader:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Generate Version
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
- name: Build NapCat.Framework
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm run typecheck || exit 1
|
||||
pnpm test || exit 1
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:framework
|
||||
pnpm --filter napcat-plugin-builtin run build || exit 1
|
||||
mv packages/napcat-framework/dist framework-dist
|
||||
cd framework-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: framework-dist
|
||||
- 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
|
||||
rm package-lock.json
|
||||
cd ..
|
||||
- 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
|
||||
with:
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- name: Generate Version
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-main.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
- name: Build NapCat.Shell
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm run typecheck || exit 1
|
||||
pnpm test || exit 1
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:shell
|
||||
pnpm --filter napcat-plugin-builtin run build || exit 1
|
||||
mv packages/napcat-shell/dist shell-dist
|
||||
cd shell-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: shell-dist
|
||||
- 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 LiteLoader
|
||||
run: |
|
||||
npm i
|
||||
cd napcat.webui
|
||||
npm i
|
||||
cd ..
|
||||
npm run build:shell
|
||||
cd dist
|
||||
npm i --omit=dev
|
||||
rm package-lock.json
|
||||
cd ..
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: dist
|
||||
|
||||
303
.github/workflows/pr-build.yml
vendored
303
.github/workflows/pr-build.yml
vendored
@@ -1,303 +0,0 @@
|
||||
# =============================================================================
|
||||
# PR 构建工作流
|
||||
# =============================================================================
|
||||
# 功能:
|
||||
# 1. 在 PR 提交时自动构建 Framework 和 Shell 包
|
||||
# 2. 支持通过 /build 命令手动触发构建(仅协作者/组织成员)
|
||||
# 3. 在 PR 中发布构建状态评论,并持续更新(不会重复创建)
|
||||
# 4. 支持 Fork PR 的构建(使用 pull_request_target 获取写权限)
|
||||
#
|
||||
# 安全说明:
|
||||
# - 使用 pull_request_target 事件,在 base 分支上下文运行
|
||||
# - 构建脚本始终从 base 分支 checkout,避免恶意 PR 篡改脚本
|
||||
# - PR 代码单独 checkout 到 workspace 目录
|
||||
# =============================================================================
|
||||
|
||||
name: PR Build
|
||||
|
||||
# =============================================================================
|
||||
# 触发条件
|
||||
# =============================================================================
|
||||
on:
|
||||
# PR 事件:打开、同步(新推送)、重新打开时触发
|
||||
# 注意:使用 pull_request_target 而非 pull_request,以便对 Fork PR 有写权限
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
# Issue 评论事件:用于响应 /build 命令
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# =============================================================================
|
||||
# 权限配置
|
||||
# =============================================================================
|
||||
permissions:
|
||||
contents: read # 读取仓库内容
|
||||
pull-requests: write # 写入 PR 评论
|
||||
issues: write # 写入 Issue 评论(/build 命令响应)
|
||||
actions: read # 读取 Actions 信息(获取构建日志链接)
|
||||
|
||||
# =============================================================================
|
||||
# 并发控制
|
||||
# =============================================================================
|
||||
# 同一 PR 的多次构建会取消之前未完成的构建,避免资源浪费
|
||||
# 注意:只有在 should_build=true 时才会进入实际构建流程,
|
||||
# issue_comment 事件如果不是 /build 命令,会在 check-build 阶段快速退出,
|
||||
# 不会取消正在进行的构建(因为 cancel-in-progress 只影响同 group 的后续任务)
|
||||
concurrency:
|
||||
# 使用不同的 group 策略:
|
||||
# - pull_request_target: 使用 PR 号
|
||||
# - issue_comment: 只有确认是 /build 命令时才使用 PR 号,否则使用 run_id(不冲突)
|
||||
group: pr-build-${{ github.event_name == 'pull_request_target' && github.event.pull_request.number || github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/build') && github.event.issue.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# =============================================================================
|
||||
# 任务定义
|
||||
# =============================================================================
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: 检查构建条件
|
||||
# ---------------------------------------------------------------------------
|
||||
# 判断是否应该触发构建:
|
||||
# - pull_request_target 事件:总是触发
|
||||
# - issue_comment 事件:检查是否为 /build 命令,且用户有权限
|
||||
# ---------------------------------------------------------------------------
|
||||
check-build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }} # 是否应该构建
|
||||
pr_number: ${{ steps.check.outputs.pr_number }} # PR 编号
|
||||
pr_sha: ${{ steps.check.outputs.pr_sha }} # PR 最新提交 SHA
|
||||
pr_head_repo: ${{ steps.check.outputs.pr_head_repo }} # PR 源仓库(用于 Fork)
|
||||
pr_head_ref: ${{ steps.check.outputs.pr_head_ref }} # PR 源分支
|
||||
steps:
|
||||
# 仅 checkout 脚本目录,加快速度
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
# 使用 Node.js 24 以支持原生 TypeScript 执行
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 执行检查脚本,判断是否触发构建
|
||||
- name: Check trigger condition
|
||||
id: check
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-check.ts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2: 更新评论为"构建中"状态
|
||||
# ---------------------------------------------------------------------------
|
||||
# 在 PR 中创建或更新评论,显示构建正在进行中
|
||||
# ---------------------------------------------------------------------------
|
||||
update-comment-building:
|
||||
needs: check-build
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 更新 PR 评论,显示构建中状态
|
||||
- name: Update building comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-building.ts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 3: 构建 Framework 包
|
||||
# ---------------------------------------------------------------------------
|
||||
# 执行 napcat-framework 的构建流程
|
||||
# ---------------------------------------------------------------------------
|
||||
build-framework:
|
||||
needs: [check-build, update-comment-building]
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||
version: ${{ steps.version.outputs.version }} # 构建版本号
|
||||
steps:
|
||||
# 【安全】先从 base 分支 checkout 构建脚本
|
||||
# 这样即使 PR 中修改了脚本,也不会被执行
|
||||
- name: Checkout scripts from base
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
path: _scripts
|
||||
|
||||
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||
path: workspace
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 获取最新 release tag 并生成版本号
|
||||
- name: Generate Version
|
||||
id: version
|
||||
working-directory: workspace
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
|
||||
SHORT_SHA="${SHORT_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
|
||||
# 执行构建,使用 base 分支的脚本处理 workspace 中的代码
|
||||
- name: Build
|
||||
id: build
|
||||
working-directory: workspace
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts framework
|
||||
continue-on-error: true # 允许失败,后续更新评论时处理
|
||||
|
||||
# 构建成功时上传产物
|
||||
- name: Upload Artifact
|
||||
if: steps.build.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: workspace/framework-dist
|
||||
retention-days: 7 # 保留 7 天
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 4: 构建 Shell 包
|
||||
# ---------------------------------------------------------------------------
|
||||
# 执行 napcat-shell 的构建流程(与 Framework 并行执行)
|
||||
# ---------------------------------------------------------------------------
|
||||
build-shell:
|
||||
needs: [check-build, update-comment-building]
|
||||
if: needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
status: ${{ steps.build.outcome }} # 构建结果:success/failure
|
||||
error: ${{ steps.build.outputs.error }} # 错误信息(如有)
|
||||
version: ${{ steps.version.outputs.version }} # 构建版本号
|
||||
steps:
|
||||
# 【安全】先从 base 分支 checkout 构建脚本
|
||||
- name: Checkout scripts from base
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
path: _scripts
|
||||
|
||||
# 将 PR 代码 checkout 到单独的 workspace 目录
|
||||
- name: Checkout PR code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-build.outputs.pr_head_repo }}
|
||||
ref: ${{ needs.check-build.outputs.pr_sha }}
|
||||
path: workspace
|
||||
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 获取最新 release tag 并生成版本号
|
||||
- name: Generate Version
|
||||
id: version
|
||||
working-directory: workspace
|
||||
run: |
|
||||
# 获取最近的 release tag (格式: vX.X.X)
|
||||
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||
# 去掉 v 前缀
|
||||
BASE_VERSION="${LATEST_TAG#v}"
|
||||
SHORT_SHA="${{ needs.check-build.outputs.pr_sha }}"
|
||||
SHORT_SHA="${SHORT_SHA::7}"
|
||||
VERSION="${BASE_VERSION}-pr.${{ needs.check-build.outputs.pr_number }}.${{ github.run_number }}+${SHORT_SHA}"
|
||||
echo "NAPCAT_VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "Latest tag: ${LATEST_TAG}"
|
||||
echo "Build version: ${VERSION}"
|
||||
|
||||
# 执行构建
|
||||
- name: Build
|
||||
id: build
|
||||
working-directory: workspace
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NAPCAT_VERSION: ${{ env.NAPCAT_VERSION }}
|
||||
run: node --experimental-strip-types ../_scripts/.github/scripts/pr-build-run.ts shell
|
||||
continue-on-error: true
|
||||
|
||||
# 构建成功时上传产物
|
||||
- name: Upload Artifact
|
||||
if: steps.build.outcome == 'success'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: workspace/shell-dist
|
||||
retention-days: 7 # 保留 7 天
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 5: 更新评论为构建结果
|
||||
# ---------------------------------------------------------------------------
|
||||
# 汇总所有构建结果,更新 PR 评论显示最终状态
|
||||
# 使用 always() 确保即使构建失败/取消也会执行
|
||||
# ---------------------------------------------------------------------------
|
||||
update-comment-result:
|
||||
needs: [check-build, update-comment-building, build-framework, build-shell]
|
||||
if: always() && needs.check-build.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout scripts
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Setup Node.js 24
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
# 更新评论,显示构建结果和下载链接
|
||||
- name: Update result comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ needs.check-build.outputs.pr_number }}
|
||||
PR_SHA: ${{ needs.check-build.outputs.pr_sha }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
# 构建版本号
|
||||
NAPCAT_VERSION: ${{ needs.build-framework.outputs.version || needs.build-shell.outputs.version || '' }}
|
||||
# 获取构建状态,如果 job 被跳过则标记为 cancelled
|
||||
FRAMEWORK_STATUS: ${{ needs.build-framework.outputs.status || 'cancelled' }}
|
||||
FRAMEWORK_ERROR: ${{ needs.build-framework.outputs.error }}
|
||||
SHELL_STATUS: ${{ needs.build-shell.outputs.status || 'cancelled' }}
|
||||
SHELL_ERROR: ${{ needs.build-shell.outputs.error }}
|
||||
run: node --experimental-strip-types .github/scripts/pr-build-result.ts
|
||||
572
.github/workflows/release.yml
vendored
572
.github/workflows/release.yml
vendored
@@ -1,480 +1,152 @@
|
||||
name: Release NapCat
|
||||
name: "Build Release"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- "v*"
|
||||
|
||||
permissions: write-all
|
||||
|
||||
env:
|
||||
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
|
||||
OPENROUTER_MODEL: "deepseek-v3.2-chat"
|
||||
RELEASE_NAME: "NapCat"
|
||||
|
||||
jobs:
|
||||
# 验证版本号格式
|
||||
validate-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
valid: ${{ steps.check.outputs.valid }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- name: Validate semantic version
|
||||
id: check
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Checking tag: $TAG"
|
||||
|
||||
# 语义化版本正则表达式
|
||||
# 支持: v1.0.0, v1.0.0-beta, v1.0.0-rc.1, v1.0.0-alpha.1+build.123
|
||||
SEMVER_REGEX="^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"
|
||||
|
||||
if [[ "$TAG" =~ $SEMVER_REGEX ]]; then
|
||||
echo "✅ Valid semantic version: $TAG"
|
||||
echo "valid=true" >> $GITHUB_OUTPUT
|
||||
echo "version=$TAG" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "❌ Invalid version format: $TAG"
|
||||
echo "Expected format: vX.Y.Z or vX.Y.Z-prerelease"
|
||||
echo "Examples: v1.0.0, v1.2.3-beta, v2.0.0-rc.1"
|
||||
echo "valid=false" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Build-Framework:
|
||||
needs: validate-version
|
||||
if: needs.validate-version.outputs.valid == 'true'
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone Main Repository
|
||||
- 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: Build NapCat.Framework
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:framework
|
||||
pnpm --filter napcat-plugin-builtin run build || exit 1
|
||||
mv packages/napcat-framework/dist framework-dist
|
||||
cd framework-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Framework
|
||||
path: framework-dist
|
||||
|
||||
- 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:
|
||||
needs: validate-version
|
||||
if: needs.validate-version.outputs.valid == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check-version]
|
||||
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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm --filter napcat-webui-frontend run build || exit 1
|
||||
pnpm run build:shell
|
||||
pnpm --filter napcat-plugin-builtin run build || exit 1
|
||||
mv packages/napcat-shell/dist shell-dist
|
||||
cd shell-dist
|
||||
npm install --omit=dev
|
||||
rm ./package-lock.json || exit 0
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: shell-dist
|
||||
Download-QNX64:
|
||||
needs: Build-Shell
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Clone Main Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'NapNeko/NapCatQQ'
|
||||
submodules: true
|
||||
ref: main
|
||||
token: ${{ secrets.NAPCAT_BUILD }}
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Setup tools
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y aria2 unzip zip p7zip-full curl jq
|
||||
|
||||
- name: Download and Assemble NapCat.Shell.Windows.Node.zip
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TMPDIR=$(mktemp -d)
|
||||
cd "$TMPDIR"
|
||||
|
||||
# -----------------------------
|
||||
# 1) 下载 QQ x64
|
||||
# -----------------------------
|
||||
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/32876254/QQ9.9.27.45627_x64.exe"
|
||||
QQ_ZIP="$(basename "$NT_URL")"
|
||||
echo "Downloading QQ installer: $QQ_ZIP"
|
||||
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
|
||||
- name: Use Node.js 20.X
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
QQ_EXTRACT="$TMPDIR/qq_extracted"
|
||||
mkdir -p "$QQ_EXTRACT"
|
||||
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
|
||||
- name: Build NuCat Shell
|
||||
run: |
|
||||
npm i
|
||||
cd napcat.webui
|
||||
npm i
|
||||
cd ..
|
||||
npm run build:shell
|
||||
cd dist
|
||||
npm i --omit=dev
|
||||
cd ..
|
||||
|
||||
# -----------------------------
|
||||
# 2) 下载 Node.js Windows x64 zip 22.11.0
|
||||
# -----------------------------
|
||||
NODE_VER="22.11.0"
|
||||
NODE_URL="https://nodejs.org/dist/v$NODE_VER/node-v$NODE_VER-win-x64.zip"
|
||||
NODE_ZIP="node-v$NODE_VER-win-x64.zip"
|
||||
aria2c -x1 -s1 -k1M -o "$NODE_ZIP" "$NODE_URL"
|
||||
|
||||
NODE_EXTRACT="$TMPDIR/node_extracted"
|
||||
mkdir -p "$NODE_EXTRACT"
|
||||
unzip -q "$NODE_ZIP" -d "$NODE_EXTRACT"
|
||||
|
||||
# -----------------------------
|
||||
# 3) 创建输出目录
|
||||
# -----------------------------
|
||||
OUT_DIR="$GITHUB_WORKSPACE/NapCat.Shell.Windows.Node"
|
||||
mkdir -p "$OUT_DIR/NapCat.Shell.Windows.Node"
|
||||
|
||||
# -----------------------------
|
||||
# 4) 解压 NapCat.Shell.zip 到 napcat
|
||||
# -----------------------------
|
||||
cp -a "$GITHUB_WORKSPACE/artifacts/NapCat.Shell/." "$OUT_DIR/napcat/"
|
||||
|
||||
# -----------------------------
|
||||
# 5) 拷贝 QQ 文件到 NapCat.Shell.Windows.Node
|
||||
# -----------------------------
|
||||
QQ_TARGETS=("avif_convert.dll" "broadcast_ipc.dll" "config.json" "libglib-2.0-0.dll" "libgobject-2.0-0.dll" "libvips-42.dll" "ncnn.dll" "opencv.dll" "package.json" "QBar.dll" "wrapper.node" "LightQuic.dll")
|
||||
for name in "${QQ_TARGETS[@]}"; do
|
||||
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true
|
||||
done
|
||||
|
||||
# -----------------------------
|
||||
# 5.1) 拷贝 win64 目录下的文件
|
||||
# -----------------------------
|
||||
mkdir -p "$OUT_DIR/win64"
|
||||
find "$QQ_EXTRACT" -ipath "*/win64/SSOShareInfoHelper64.dll" -exec cp -a {} "$OUT_DIR/win64/" \; || true
|
||||
find "$QQ_EXTRACT" -ipath "*/win64/parent-ipc-core-x64.dll" -exec cp -a {} "$OUT_DIR/win64/" \; || true
|
||||
|
||||
# -----------------------------
|
||||
# 6) 拷贝仓库文件 napcat.bat 和 index.js
|
||||
# -----------------------------
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/napcat.bat" "$OUT_DIR/" || true
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/index.js" "$OUT_DIR/" || true
|
||||
cp -a "$GITHUB_WORKSPACE/packages/napcat-develop/QQNT.dll" "$OUT_DIR/" || true
|
||||
# -----------------------------
|
||||
# 7) 拷贝 Node.exe 到 NapCat.Shell.Windows.Node
|
||||
# -----------------------------
|
||||
NODE_VER="22.11.0"
|
||||
cp -a "$NODE_EXTRACT/node-v$NODE_VER-win-x64/node.exe" "$OUT_DIR/" || true
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell.Windows.Node
|
||||
path: NapCat.Shell.Windows.Node
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NapCat.Shell
|
||||
path: dist
|
||||
|
||||
release-napcat:
|
||||
needs: [Build-Framework, Build-Shell, Download-QNX64]
|
||||
needs: [Build-LiteLoader,Build-Shell]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Download NapCat.Shell.Windows.OneKey.zip
|
||||
run: |
|
||||
curl -L -o NapCat.Shell.Windows.OneKey.zip https://github.com/NapNeko/NapCatResource/raw/main/NapCat.Shell.Windows.OneKey.zip
|
||||
|
||||
- name: Zip Artifacts
|
||||
run: |
|
||||
cd artifacts
|
||||
[ -d NapCat.Framework ] && (cd NapCat.Framework && zip -qr ../../NapCat.Framework.zip .)
|
||||
[ -d NapCat.Shell ] && (cd NapCat.Shell && zip -qr ../../NapCat.Shell.zip .)
|
||||
[ -d NapCat.Shell.Windows.Node ] && (cd NapCat.Shell.Windows.Node && zip -qr ../../NapCat.Shell.Windows.Node.zip .)
|
||||
cd ..
|
||||
|
||||
- name: Generate release note via OpenRouter
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }}
|
||||
OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }}
|
||||
GITHUB_OWNER: "NapNeko" # 替换成你的 repo owner
|
||||
GITHUB_REPO: "NapCatQQ" # 替换成你的 repo 名
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# 当前 tag
|
||||
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
||||
echo "Current tag: $CURRENT_TAG"
|
||||
|
||||
# 从 GitHub API 获取 tag 列表
|
||||
TAGS_JSON=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/tags?per_page=100")
|
||||
TAGS=( $(echo "$TAGS_JSON" | jq -r '.[].name' | sort -V) )
|
||||
|
||||
# 找到上一个 tag
|
||||
PREV_TAG=""
|
||||
for i in "${!TAGS[@]}"; do
|
||||
if [ "${TAGS[$i]}" = "$CURRENT_TAG" ]; then
|
||||
if [ $i -gt 0 ]; then
|
||||
PREV_TAG="${TAGS[$((i-1))]}"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
echo "⚠️ Could not find previous tag for $CURRENT_TAG, using first commit"
|
||||
PREV_TAG=$(git rev-list --max-parents=0 HEAD | head -1)
|
||||
fi
|
||||
|
||||
echo "Previous tag: $PREV_TAG"
|
||||
|
||||
# 强制拉取上一个 tag 和当前 tag
|
||||
git fetch origin "refs/tags/$PREV_TAG:refs/tags/$PREV_TAG" --force || true
|
||||
git fetch origin "refs/tags/$CURRENT_TAG:refs/tags/$CURRENT_TAG" --force || true
|
||||
|
||||
# 获取 commit,使用更清晰的格式
|
||||
# 格式: <type>: <subject> (<hash>)
|
||||
COMMITS=$(git log --pretty=format:'- %s (%h)' "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || git log --pretty=format:'- %s (%h)' -20)
|
||||
|
||||
echo "Commit list from $PREV_TAG to $CURRENT_TAG:"
|
||||
echo "$COMMITS"
|
||||
|
||||
# 获取文件变化统计
|
||||
echo "Getting file change statistics..."
|
||||
FILE_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null || echo "")
|
||||
|
||||
# 获取总体统计(最后一行)
|
||||
SUMMARY_LINE=$(echo "$FILE_STATS" | tail -1)
|
||||
echo "Summary: $SUMMARY_LINE"
|
||||
|
||||
# 获取每个文件的变化(去掉最后一行汇总)
|
||||
# 截断过长的输出(最多50个文件,每行最多80字符)
|
||||
FILE_CHANGES=$(echo "$FILE_STATS" | head -n -1 | head -50 | cut -c1-80)
|
||||
|
||||
# 如果文件变化太多,进一步精简:只保留主要目录的变化
|
||||
FILE_COUNT=$(echo "$FILE_STATS" | head -n -1 | wc -l)
|
||||
if [ "$FILE_COUNT" -gt 50 ]; then
|
||||
echo "Too many files ($FILE_COUNT), grouping by directory..."
|
||||
# 按目录分组统计
|
||||
DIR_STATS=$(git diff --stat "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | head -n -1 | \
|
||||
sed 's/|.*//g' | \
|
||||
awk -F'/' '{if(NF>1) print $1"/"$2; else print $1}' | \
|
||||
sort | uniq -c | sort -rn | head -20)
|
||||
FILE_CHANGES="[按目录分组统计 - 共 $FILE_COUNT 个文件变更]
|
||||
$DIR_STATS"
|
||||
fi
|
||||
|
||||
echo "File changes:"
|
||||
echo "$FILE_CHANGES"
|
||||
|
||||
# 获取具体代码变化(关键文件的diff)
|
||||
echo "Getting code diff for key files..."
|
||||
|
||||
# 定义关键目录(优先展示这些目录的变化)
|
||||
KEY_DIRS="packages/napcat-core packages/napcat-onebot packages/napcat-webui-backend"
|
||||
|
||||
# 获取变更的关键文件列表(排除测试、配置等)
|
||||
# 使用 || true 防止 grep 无匹配时返回非零退出码
|
||||
KEY_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
|
||||
grep -E "^packages/napcat-(core|onebot|webui-backend|shell)/" || true | \
|
||||
grep -E "\.(ts|js)$" || true | \
|
||||
grep -v -E "(test|spec|\.d\.ts|config)" || true | \
|
||||
head -15) || true
|
||||
|
||||
CODE_DIFF=""
|
||||
DIFF_CHAR_LIMIT=6000 # 总diff字符限制
|
||||
CURRENT_CHARS=0
|
||||
|
||||
if [ -n "$KEY_FILES" ]; then
|
||||
for file in $KEY_FILES; do
|
||||
if [ "$CURRENT_CHARS" -ge "$DIFF_CHAR_LIMIT" ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
[... 更多文件变化已截断 ...]"
|
||||
break
|
||||
fi
|
||||
|
||||
# 获取单个文件的diff,限制每个文件最多50行
|
||||
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -50) || true
|
||||
FILE_DIFF_LEN=${#FILE_DIFF}
|
||||
|
||||
# 如果单个文件diff超过1500字符,截断
|
||||
if [ "$FILE_DIFF_LEN" -gt 1500 ]; then
|
||||
FILE_DIFF=$(echo "$FILE_DIFF" | head -c 1500)
|
||||
FILE_DIFF="$FILE_DIFF
|
||||
[... 文件 $file 变化已截断 ...]"
|
||||
fi
|
||||
|
||||
if [ -n "$FILE_DIFF" ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
|
||||
### $file
|
||||
\`\`\`diff
|
||||
$FILE_DIFF
|
||||
\`\`\`"
|
||||
CURRENT_CHARS=$((CURRENT_CHARS + FILE_DIFF_LEN))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 如果没有关键文件变化,获取前5个变更文件的diff
|
||||
if [ -z "$CODE_DIFF" ]; then
|
||||
echo "No key files changed, getting top changed files..."
|
||||
TOP_FILES=$(git diff --name-only "$PREV_TAG".."$CURRENT_TAG" 2>/dev/null | \
|
||||
grep -E "\.(ts|js|yml|md)$" | head -5) || true
|
||||
|
||||
if [ -n "$TOP_FILES" ]; then
|
||||
for file in $TOP_FILES; do
|
||||
FILE_DIFF=$(git diff "$PREV_TAG".."$CURRENT_TAG" -- "$file" 2>/dev/null | head -30) || true
|
||||
if [ -n "$FILE_DIFF" ] && [ ${#FILE_DIFF} -lt 1000 ]; then
|
||||
CODE_DIFF="$CODE_DIFF
|
||||
|
||||
### $file
|
||||
\`\`\`diff
|
||||
$FILE_DIFF
|
||||
\`\`\`"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 如果仍然没有代码变化,添加说明
|
||||
if [ -z "$CODE_DIFF" ]; then
|
||||
CODE_DIFF="[本次更新主要涉及配置文件和文档变更,无核心代码变化]"
|
||||
fi
|
||||
|
||||
echo "Code diff preview:"
|
||||
echo "$CODE_DIFF" | head -50
|
||||
|
||||
# 读取 prompt
|
||||
PROMPT_FILE=".github/prompt/release_note_prompt.txt"
|
||||
SYSTEM_PROMPT=$(<"$PROMPT_FILE")
|
||||
|
||||
# 构建用户内容,传递更多上下文(包含文件变化和代码diff)
|
||||
USER_CONTENT="当前版本: $CURRENT_TAG
|
||||
上一版本: $PREV_TAG
|
||||
|
||||
## 提交列表
|
||||
$COMMITS
|
||||
|
||||
## 文件变化统计
|
||||
$SUMMARY_LINE
|
||||
|
||||
## 变更文件列表
|
||||
$FILE_CHANGES
|
||||
|
||||
## 关键代码变化
|
||||
$CODE_DIFF"
|
||||
|
||||
# 构建请求 JSON,增加 max_tokens 以获取更完整的输出
|
||||
BODY=$(jq -n \
|
||||
--arg system "$SYSTEM_PROMPT" \
|
||||
--arg user "$USER_CONTENT" \
|
||||
--arg model "$OPENROUTER_MODEL" \
|
||||
'{model: $model, messages:[{role:"system", content:$system},{role:"user", content:$user}], temperature:0.2, max_tokens:5000}')
|
||||
|
||||
echo "=== OpenRouter request body ==="
|
||||
echo "$BODY" | jq .
|
||||
|
||||
# 调用 OpenRouter
|
||||
if RESPONSE=$(curl -s -X POST "$OPENROUTER_API_URL" \
|
||||
-H "Authorization: Bearer $OPENAI_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY"); then
|
||||
echo "=== raw response ==="
|
||||
echo "$RESPONSE"
|
||||
echo "=== OpenRouter raw response ==="
|
||||
if echo "$RESPONSE" | jq . >/dev/null 2>&1; then
|
||||
echo "$RESPONSE" | jq .
|
||||
else
|
||||
echo "jq failed to parse response"
|
||||
fi
|
||||
|
||||
# 提取生成内容
|
||||
RELEASE_BODY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // .choices[0].text // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RELEASE_BODY" ]; then
|
||||
echo "❌ OpenRouter failed to generate release note, using default.md"
|
||||
# 替换默认模板中的版本占位符
|
||||
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
|
||||
else
|
||||
# 后处理:确保版本号正确,并添加比较链接
|
||||
echo -e "$RELEASE_BODY" > CHANGELOG.md
|
||||
# 替换可能的占位符
|
||||
sed -i "s/{VERSION}/$CURRENT_TAG/g" CHANGELOG.md
|
||||
sed -i "s/{PREV_VERSION}/$PREV_TAG/g" CHANGELOG.md
|
||||
fi
|
||||
else
|
||||
echo "❌ Curl failed, using default.md"
|
||||
sed "s/{VERSION}/$CURRENT_TAG/g" .github/prompt/default.md > CHANGELOG.md
|
||||
fi
|
||||
echo "=== generated release note ==="
|
||||
cat CHANGELOG.md
|
||||
|
||||
- name: Create Release Draft and Upload Artifacts
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: NapCat ${{ github.ref_name }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
body_path: CHANGELOG.md
|
||||
files: |
|
||||
NapCat.Shell.Windows.Node.zip
|
||||
NapCat.Framework.zip
|
||||
NapCat.Shell.zip
|
||||
NapCat.Shell.Windows.OneKey.zip
|
||||
draft: true
|
||||
- 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: Update apifox
|
||||
env:
|
||||
APIFOX_TOKEN: ${{ secrets.APIFOX_TOKEN }}
|
||||
run: |
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
pnpm run build:openapi
|
||||
|
||||
# 使用 jq 安全地构建大型 JSON 数据并保存到文件
|
||||
jq -n --rawfile input packages/napcat-schema/dist/openapi.json \
|
||||
'{
|
||||
input: $input,
|
||||
options: {
|
||||
"endpointOverwriteBehavior": "OVERWRITE_EXISTING",
|
||||
"schemaOverwriteBehavior": "OVERWRITE_EXISTING",
|
||||
"updateFolderOfChangedEndpoint": true,
|
||||
"moduleId": 1140714,
|
||||
"deleteUnmatchedResources": true
|
||||
}
|
||||
}' > apifox_payload.json
|
||||
|
||||
# 通过文件形式发送数据,避免命令行长度限制
|
||||
curl --location -g --request POST 'https://api.apifox.com/v1/projects/5348325/import-openapi?locale=zh-CN' \
|
||||
--header 'X-Apifox-Api-Version: 2024-03-28' \
|
||||
--header "Authorization: Bearer $APIFOX_TOKEN" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-binary @apifox_payload.json
|
||||
- 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
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,23 +1,18 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
devconfig/*
|
||||
/localdebug/
|
||||
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Build
|
||||
*.db
|
||||
checkVersion.sh
|
||||
bun.lockb
|
||||
tests/run/
|
||||
guild1.db-wal
|
||||
guild1.db-shm
|
||||
packages/napcat-develop/config/.env
|
||||
|
||||
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"
|
||||
}
|
||||
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"name": "调试程序",
|
||||
"command": "pnpm run dev:shell",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
.vscode/settings.json
vendored
37
.vscode/settings.json
vendored
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
".env.universal": ".env.*",
|
||||
"vite.config.ts": "vite*.ts",
|
||||
"README.md": "CODE_OF_CONDUCT.md, RELEASES.md, CONTRIBUTING.md, CHANGELOG.md, SECURITY.md",
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts",
|
||||
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
||||
},
|
||||
"css.customData": [
|
||||
".vscode/tailwindcss.json"
|
||||
],
|
||||
"editor.detectIndentation": false,
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "always"
|
||||
},
|
||||
"files.autoSave": "onFocusChange",
|
||||
"javascript.preferences.quoteStyle": "single",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"javascript.format.semicolons": "insert",
|
||||
"typescript.format.semicolons": "insert",
|
||||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"typescript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||
"typescript.format.insertSpaceAfterConstructor": true,
|
||||
"javascript.format.insertSpaceAfterConstructor": true,
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.disableAutomaticTypeAcquisition": true
|
||||
}
|
||||
55
.vscode/tailwindcss.json
vendored
55
.vscode/tailwindcss.json
vendored
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
nanaeonn@outlook.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
352
LICENSE
352
LICENSE
@@ -1,19 +1,343 @@
|
||||
Limited Redistribution License for NapCat
|
||||
GNU GENERAL PUBLIC Without Social media promotion LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright © 2024 Mlikiowa
|
||||
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.
|
||||
|
||||
1. Usage and Reproduction:
|
||||
- Unauthorized use, reproduction, modification, or distribution of this code is prohibited without explicit permission from the main author of the NapCat repository.
|
||||
|
||||
2. Redistribution:
|
||||
- Redistribution of this code is permitted, provided that the full text of this license is included, and the source and copyright information is clearly stated.
|
||||
- Minor modifications and extensions are allowed for redistribution purposes, but the modified code must not be publicly released.
|
||||
Preamble
|
||||
|
||||
3. Non-Commercial Use:
|
||||
- This code is not to be used for any commercial purposes.
|
||||
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.
|
||||
|
||||
4. Additional Permissions:
|
||||
- Any rights not explicitly addressed in this license must be requested from and granted by the main author of the NapCat repository.
|
||||
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.
|
||||
|
||||
5. Disclaimer:
|
||||
- This code is provided "as is," without any express or implied warranties, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. In no event shall the author be liable for any damages or other liability arising from, out of, or in connection with the use or distribution of this code.
|
||||
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.
|
||||
|
||||
84
README.md
84
README.md
@@ -1,77 +1,51 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_Modern protocol-side framework implemented based on NTQQ._
|
||||
|
||||
> Where clouds rise and winds are born, the heart yearns for distant lands yet untraveled.
|
||||
|
||||
English | [简体中文](./docs/README_zh-CN.md) | [繁體中文](./docs/README_zh-TW.md) | [日本語](./docs/README_ja.md) | [한국어](./docs/README_ko.md) | [Русский](./docs/README_ru.md) | [العربية](./docs/README_ar.md)
|
||||
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
## 欢迎回家
|
||||
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
## Notice
|
||||
NapCat is currently looking for a new primary maintainer. Please email nanaeonn@outlook.com if you are interested. No public community groups will be established during this period, but NapCat will continue to receive regular updates.
|
||||
## 碎碎叨叨
|
||||
- [x] **安装简单**:就算是笨蛋也能使用
|
||||
- [x] **性能友好**:就算是低内存也能使用
|
||||
- [x] **接口丰富**:就算是没有也能使用
|
||||
- [x] **稳定好用**:就算是被捉也能使用
|
||||
|
||||
## Welcome
|
||||
## 使用框架
|
||||
|
||||
- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||
|
||||
## Features
|
||||
**首次使用**请务必查看如下文档看使用教程
|
||||
|
||||
- **Easy to Use**
|
||||
- Beginners can get started effortlessly.
|
||||
- **Quick and Efficient**
|
||||
- Runs for extended periods on low-memory systems.
|
||||
- **Rich API Interface**
|
||||
- Fully implements most standard interfaces.
|
||||
- **Stable and Reliable**
|
||||
- Continuous and stable development and maintenance.
|
||||
### 文档地址
|
||||
|
||||
## Quick Start
|
||||
[Cloudflare.Worker](https://doc.napneko.icu/)
|
||||
|
||||
Download the latest version from the [Release](https://github.com/NapNeko/NapCatQQ/releases/) page.
|
||||
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
||||
|
||||
**First-time users** should make sure to read the documentation below for a usage guide.
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
> This is a non-profit project. For integration issues, basic questions, or underlying framework issues, please search for solutions on your own — this project's community does not provide such support.
|
||||
[Server.China](https://napneko.com/)
|
||||
|
||||
## Links
|
||||
[Server.Other](https://napcat.cyou/)
|
||||
|
||||
| Docs | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
[Github.IO](https://napneko.github.io/)
|
||||
## 回家旅途
|
||||
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
|
||||
|
||||
| Docs | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
## 感谢他们
|
||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
||||
> Please do not mention this project in other communities (including other protocol-side or related application-side projects) to avoid disputes. If you have suggestions, please discuss them in the official group or submit a PR.
|
||||
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
|
||||
|
||||
## Thanks
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — Great support for this project; some code was referenced (with permission)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — An LLM Bot framework that perfectly integrates with this project
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — A cyber companion Bot framework
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — A message export tool based on NapCat
|
||||
|
||||
- And most importantly, thank you — the person reading this right now~
|
||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
## 开源附加
|
||||
|
||||
## License
|
||||
|
||||
This project is open-sourced under a hybrid license. When using this project, please note the following:
|
||||
|
||||
1. Third-party library code or modified portions follow their original open-source licenses.
|
||||
2. This project has obtained authorization from certain projects and is not subject to some restrictions.
|
||||
3. The remaining logic code of the project is governed by the [repository license](./LICENSE).
|
||||
|
||||
**This repository is solely intended to improve usability and implement message push features. Furthermore, developing any project based on NapCat code without prior authorization from the repository's main author is prohibited. Please comply with local laws and regulations when using this project. Any issues arising from misuse are the responsibility of the user and anyone providing tutorials for unauthorized use.**
|
||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
||||
|
||||
11
SECURITY.md
11
SECURITY.md
@@ -1,11 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| > 4.0 | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
you should open an issue
|
||||
@@ -1,81 +0,0 @@
|
||||
<div dir="rtl">
|
||||
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=left />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_إطار عمل حديث على جانب البروتوكول مبني على NTQQ._
|
||||
|
||||
> حيث تنهض السحب وتولد الرياح، يتوق القلب إلى أراضٍ بعيدة لم تُسلك بعد.
|
||||
|
||||
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | العربية
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## إعلان
|
||||
يبحث NapCat حالياً عن مشرف رئيسي جديد. يرجى التواصل عبر البريد الإلكتروني nanaeonn@outlook.com. لن يتم إنشاء مجموعات مجتمع عامة خلال هذه الفترة، لكن NapCat يضمن استمرار التحديثات المنتظمة.
|
||||
|
||||
## مرحباً
|
||||
|
||||
- NapCatQQ هو تطبيق حديث لبروتوكول Bot مبني على NTQQ
|
||||
|
||||
## المميزات
|
||||
|
||||
- **سهل الاستخدام**
|
||||
- يمكن للمبتدئين البدء بسهولة.
|
||||
- **سريع وفعال**
|
||||
- يعمل لفترات طويلة على أنظمة ذات ذاكرة منخفضة.
|
||||
- **واجهة API غنية**
|
||||
- تنفيذ كامل لمعظم الواجهات القياسية.
|
||||
- **مستقر وموثوق**
|
||||
- تطوير وصيانة مستمرة ومستقرة.
|
||||
|
||||
## البداية السريعة
|
||||
|
||||
قم بتنزيل أحدث إصدار من صفحة [Release](https://github.com/NapNeko/NapCatQQ/releases/)
|
||||
|
||||
**للمستخدمين لأول مرة** تأكد من قراءة الوثائق أدناه للحصول على دليل الاستخدام
|
||||
|
||||
> هذا مشروع غير ربحي. للمشاكل المتعلقة بالتكامل والأسئلة الأساسية ومشاكل إطار العمل الأساسي، يرجى البحث عن الحلول بنفسك — مجتمع هذا المشروع لا يقدم مثل هذا الدعم.
|
||||
|
||||
## الروابط
|
||||
|
||||
| الوثائق | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| الوثائق | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> يرجى عدم ذكر هذا المشروع في مجتمعات أخرى (بما في ذلك مشاريع البروتوكول الأخرى / مشاريع التطبيقات ذات الصلة) لتجنب الخلافات. إذا كانت لديك اقتراحات، يرجى مناقشتها في المجموعة الرسمية أو تقديم PR.
|
||||
|
||||
## شكر وتقدير
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — دعم كبير لهذا المشروع؛ تمت الإشارة إلى بعض الأكواد (بإذن)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — إطار عمل LLM Bot متوافق تماماً مع هذا المشروع
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — إطار عمل Bot رفيق سيبراني
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — أداة تصدير الرسائل مبنية على NapCat
|
||||
|
||||
- والأهم من ذلك كله، شكراً لك — أنت الذي تقرأ هذا الآن~
|
||||
|
||||
---
|
||||
|
||||
## الرخصة
|
||||
|
||||
هذا المشروع مفتوح المصدر بموجب رخصة هجينة. عند استخدام هذا المشروع، يرجى ملاحظة ما يلي:
|
||||
|
||||
1. كود المكتبات الخارجية أو الأجزاء المعدلة تخضع لرخصها مفتوحة المصدر الأصلية.
|
||||
2. حصل هذا المشروع على إذن من بعض المشاريع ولا يخضع لبعض القيود.
|
||||
3. باقي الكود المنطقي للمشروع يخضع [لرخصة هذا المستودع](../LICENSE).
|
||||
|
||||
**هذا المستودع مخصص فقط لتحسين سهولة الاستخدام وتنفيذ ميزات دفع الرسائل. علاوة على ذلك، يُحظر تطوير أي مشروع بناءً على كود NapCat دون إذن مسبق من المؤلف الرئيسي للمستودع. يرجى الامتثال للقوانين واللوائح المحلية عند الاستخدام. أي مشاكل ناتجة عن سوء الاستخدام تقع على عاتق المستخدم ومن يقدم تعليمات الاستخدام غير المصرح به.**
|
||||
|
||||
</div>
|
||||
@@ -1,77 +0,0 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_NTQQをベースにした最新のプロトコル側フレームワーク._
|
||||
|
||||
> 雲起きて風生じ、心は遠方を向けど路は未だ至らず.
|
||||
|
||||
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | 日本語 | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## お知らせ
|
||||
NapCat は現在、新しいメインメンテナーを募集しています。nanaeonn@outlook.com までメールでご連絡ください。この期間中、公開コミュニティグループは設立しませんが、NapCat は通常通りの更新を保証します。
|
||||
|
||||
## ようこそ
|
||||
|
||||
- NapCatQQ は NTQQ をベースにした最新の Bot プロトコル側実装です
|
||||
|
||||
## 特徴
|
||||
|
||||
- **簡単に使える**
|
||||
- 初心者でも簡単に使用できます.
|
||||
- **高速で効率的**
|
||||
- 低メモリ環境のOSで長時間稼働できます.
|
||||
- **豊富な API インターフェース**
|
||||
- ほとんどの標準インターフェースを完全に実装しています.
|
||||
- **安定して信頼できる**
|
||||
- 継続的で安定した開発とメンテナンスを行っています.
|
||||
|
||||
## クイックスタート
|
||||
|
||||
最新バージョンは [Release](https://github.com/NapNeko/NapCatQQ/releases/) ページからダウンロードできます
|
||||
|
||||
**初めてご使用の方**は、以下のドキュメントで使用ガイドを必ずご確認ください
|
||||
|
||||
> このプロジェクトは非営利です。連携に関する問題/基本的な問題/下位フレームワークの問題については、ご自身で検索して解決してください。本プロジェクトのコミュニティではそのような回答は提供していません。
|
||||
|
||||
## リンク
|
||||
|
||||
| ドキュメント | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| ドキュメント | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> 他のコミュニティでこのプロジェクト(他のプロトコル側/関連アプリケーション側のプロジェクトを含む)に言及して論争を引き起こさないでください。提案がある場合は、公式交流グループで議論するか、PRを提出してください。
|
||||
|
||||
## 謝辞
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 本プロジェクトへの多大なサポート、一部コードを参考にしています(許可済み)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 本プロジェクトに完璧に対応した LLM Bot フレームワークです
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) サイバー仲間の 麦麦 Bot フレームワークです
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) NapCat ベースのメッセージエクスポートツールです
|
||||
|
||||
- そして何より大切なのは、画面の前のあなたに感謝することです~
|
||||
|
||||
---
|
||||
|
||||
## ライセンス
|
||||
|
||||
本プロジェクトはハイブリッドライセンスの下でオープンソース化されています。本プロジェクトを使用する際は、以下の点にご注意ください:
|
||||
|
||||
1. サードパーティライブラリのコードまたは修正部分は、元のオープンソースライセンスに従います.
|
||||
2. 本プロジェクトは一部のプロジェクトから許可を得ており、一部の制約を受けません.
|
||||
3. プロジェクトのその他のロジックコードは[本リポジトリのオープンソースライセンス](../LICENSE)に従います.
|
||||
|
||||
**本リポジトリは使いやすさの향上とメッセージプッシュ機能の実装のみを目的としています。また、リポジトリの主要作者の事前許可なく NapCat のコードに基づいてプロジェクトを開発することは禁止されています。使用する際は現地の法律と規制を遵守してください。これに起因する問題は、使用者および違反使用チュートリアルの提供者の責任となります。**
|
||||
@@ -1,77 +0,0 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_NTQQ 기반으로 구현된 최신 프로토콜 사이드 프레임워크._
|
||||
|
||||
> 구름이 일고 바람이 불어, 마음은 먼 곳을 향하나 길은 아직 닿지 못했네.
|
||||
|
||||
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | 한국어 | [Русский](./README_ru.md) | [العربية](./README_ar.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 공지
|
||||
NapCat은 현재 새로운 주요 메인테이너를 찾고 있습니다. nanaeonn@outlook.com 으로 이메일을 보내주세요. 이 기간 동안 공개 커뮤니티 그룹은 만들어지지 않지만, NapCat은 정기적인 업데이트를 보장합니다.
|
||||
|
||||
## 환영합니다
|
||||
|
||||
- NapCatQQ는 NTQQ를 기반으로 한 최신 Bot 프로토콜 구현체입니다
|
||||
|
||||
## 특징
|
||||
|
||||
- **사용하기 쉬움**
|
||||
- 초보자도 쉽게 사용할 수 있습니다.
|
||||
- **빠르고 효율적**
|
||||
- 저메모리 시스템에서도 장시간 안정적으로 실행됩니다.
|
||||
- **풍부한 API 인터페이스**
|
||||
- 대부분의 표준 인터페이스를 완전히 구현했습니다.
|
||||
- **안정적이고 신뢰할 수 있음**
|
||||
- 지속적이고 안정적인 개발과 유지보수를 하고 있습니다.
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
[Release](https://github.com/NapNeko/NapCatQQ/releases/) 페이지에서 최신 버전을 다운로드할 수 있습니다
|
||||
|
||||
**처음 사용하시는 분**은 아래 문서에서 사용 가이드를 반드시 확인해 주세요
|
||||
|
||||
> 이 프로젝트는 비영리입니다. 연동 문제/기본 문제/하위 프레임워크 문제는 직접 검색하여 해결해 주세요. 본 프로젝트 커뮤니티에서는 이러한 지원을 제공하지 않습니다.
|
||||
|
||||
## 링크
|
||||
|
||||
| 문서 | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| 문서 | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> 다른 커뮤니티에서 이 프로젝트(다른 프로토콜 측/관련 애플리케이션 측 프로젝트 포함)를 언급하여 논쟁을 일으키지 마세요. 제안이 있으시면 공식 그룹에서 토론하거나 PR을 제출해 주세요.
|
||||
|
||||
## 감사의 말
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — 본 프로젝트에 대한 큰 지원, 일부 코드 참고 (허가 완료)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — 본 프로젝트와 완벽하게 호환되는 LLM Bot 프레임워크
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — 사이버 친구 麦麦 Bot 프레임워크
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — NapCat 기반의 메시지 내보내기 도구
|
||||
|
||||
- 그리고 가장 중요한 것은, 지금 이 글을 읽고 있는 당신에게 감사드립니다~
|
||||
|
||||
---
|
||||
|
||||
## 라이선스
|
||||
|
||||
이 프로젝트는 하이브리드 라이선스로 오픈소스화되어 있습니다. 이 프로젝트를 사용할 때 다음 사항에 유의해 주세요:
|
||||
|
||||
1. 서드파티 라이브러리 코드 또는 수정된 부분은 원래의 오픈소스 라이선스를 따릅니다.
|
||||
2. 이 프로젝트는 일부 프로젝트로부터 허가를 받아 일부 제한을 받지 않습니다.
|
||||
3. 프로젝트의 나머지 로직 코드는 [본 리포지토리 라이선스](../LICENSE)를 따릅니다.
|
||||
|
||||
**이 리포지토리는 사용 편의성 향상과 메시지 푸시 기능 구현만을 목적으로 합니다. 또한, 리포지토리 주요 작성자의 사전 승인 없이 NapCat 코드를 기반으로 프로젝트를 개발하는 것은 금지되어 있습니다. 사용 시 현지 법률 및 규정을 준수해 주세요. 이로 인해 발생하는 문제는 사용자 및 위반 사용 튜토리얼 제공자의 책임입니다.**
|
||||
@@ -1,77 +0,0 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_Современный фреймворк на стороне протокола, реализованный на базе NTQQ._
|
||||
|
||||
> Облака поднимаются, ветер рождается, сердце стремится вдаль, но путь ещё не пройден.
|
||||
|
||||
[English](../README.md) | [简体中文](./README_zh-CN.md) | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | Русский | [العربية](./README_ar.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Объявление
|
||||
NapCat в настоящее время ищет нового основного мейнтейнера. Пожалуйста, свяжитесь с нами по электронной почте nanaeonn@outlook.com. В этот период публичные группы сообщества создаваться не будут, но NapCat гарантирует регулярные обновления.
|
||||
|
||||
## Добро пожаловать
|
||||
|
||||
- NapCatQQ — это современная реализация протокола Bot на базе NTQQ
|
||||
|
||||
## Особенности
|
||||
|
||||
- **Простота использования**
|
||||
- Начинающие пользователи смогут легко освоить работу.
|
||||
- **Скорость и эффективность**
|
||||
- Длительная работа на системах с ограниченным объёмом памяти.
|
||||
- **Богатый API-интерфейс**
|
||||
- Полная реализация большинства стандартных интерфейсов.
|
||||
- **Стабильность и надёжность**
|
||||
- Постоянная стабильная разработка и поддержка.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
Скачайте последнюю версию на странице [Release](https://github.com/NapNeko/NapCatQQ/releases/)
|
||||
|
||||
**При первом использовании** обязательно ознакомьтесь с руководством в документации ниже
|
||||
|
||||
> Проект является некоммерческим. Вопросы интеграции, базовые вопросы и вопросы по нижележащему фреймворку решайте самостоятельно — сообщество данного проекта не предоставляет такой поддержки.
|
||||
|
||||
## Ссылки
|
||||
|
||||
| Документация | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| Документация | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> Пожалуйста, не упоминайте данный проект (включая другие протокольные/прикладные проекты) в других сообществах во избежание споров. Если у вас есть предложения, обсуждайте их в официальной группе или отправляйте PR.
|
||||
|
||||
## Благодарности
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) — большая поддержка данного проекта, частично использован код (с разрешения)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) — фреймворк LLM Bot, идеально совместимый с данным проектом
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) — кибер-друг 麦麦, Bot-фреймворк
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) — инструмент экспорта сообщений на базе NapCat
|
||||
|
||||
- И самое главное — спасибо вам, читающему это~
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
Данный проект распространяется с открытым исходным кодом под гибридной лицензией. При использовании данного проекта обратите внимание на следующее:
|
||||
|
||||
1. Код сторонних библиотек или модифицированные части подчиняются их исходным лицензиям с открытым исходным кодом.
|
||||
2. Данный проект получил разрешение от ряда проектов и не подчиняется некоторым ограничениям.
|
||||
3. Остальной логический код проекта распространяется по [лицензии данного репозитория](../LICENSE).
|
||||
|
||||
**Данный репозиторий предназначен исключительно для повышения удобства использования и реализации функций отправки сообщений. Кроме того, запрещается разработка проектов на основе кода NapCat без предварительного разрешения главного автора репозитория. При использовании соблюдайте местное законодательство. За любые проблемы, вызванные нарушением, несут ответственность пользователь и лицо, предоставившее инструкции по незаконному использованию.**
|
||||
@@ -1,77 +0,0 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_基于 NTQQ 实现的现代化协议端框架._
|
||||
|
||||
> 云起兮风生,心向远方兮路未曾至.
|
||||
|
||||
[English](../README.md) | 简体中文 | [繁體中文](./README_zh-TW.md) | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 公告
|
||||
NapCat 当前正在寻找新的主要维护者,欢迎email到 nanaeonn@outlook.com ,在此期不会建立任何公开社区交流群,Napcat会保证此期间的正常更新。
|
||||
|
||||
## 欢迎
|
||||
|
||||
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
## 特性
|
||||
|
||||
- **易于使用**
|
||||
- 作为初学者能够轻松使用.
|
||||
- **快速高效**
|
||||
- 在低内存操作系统长时运行.
|
||||
- **丰富的 API 接口**
|
||||
- 完整实现了大部分标准接口.
|
||||
- **稳定可靠**
|
||||
- 持续稳定的开发与维护.
|
||||
|
||||
## 快速开始
|
||||
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||
|
||||
**首次使用**请务必查看如下文档看使用教程
|
||||
|
||||
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
|
||||
|
||||
## 链接
|
||||
|
||||
| 文档 | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| 文档 | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR。
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
|
||||
|
||||
- 不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
|
||||
|
||||
1. 第三方库代码或修改部分遵循其原始开源许可.
|
||||
2. 本项目获取部分项目授权而不受部分约束
|
||||
2. 项目其余逻辑代码采用[本仓库开源许可](../LICENSE).
|
||||
|
||||
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
||||
@@ -1,77 +0,0 @@
|
||||
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||
<div align="center">
|
||||
|
||||
# NapCat
|
||||
|
||||
_基於 NTQQ 實現的現代化協議端框架._
|
||||
|
||||
> 雲起兮風生,心向遠方兮路未曾至.
|
||||
|
||||
[English](../README.md) | [简体中文](./README_zh-CN.md) | 繁體中文 | [日本語](./README_ja.md) | [한국어](./README_ko.md) | [Русский](./README_ru.md) | [العربية](./README_ar.md)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 公告
|
||||
NapCat 目前正在尋找新的主要維護者,歡迎透過 email 聯繫 nanaeonn@outlook.com。在此期間不會建立任何公開社群交流群,NapCat 會保證此期間的正常更新。
|
||||
|
||||
## 歡迎
|
||||
|
||||
- NapCatQQ 是現代化的基於 NTQQ 的 Bot 協議端實現
|
||||
|
||||
## 特性
|
||||
|
||||
- **易於使用**
|
||||
- 初學者也能輕鬆上手.
|
||||
- **快速高效**
|
||||
- 在低記憶體系統長時間穩定運行.
|
||||
- **豐富的 API 介面**
|
||||
- 完整實現了大部分標準介面.
|
||||
- **穩定可靠**
|
||||
- 持續穩定的開發與維護.
|
||||
|
||||
## 快速開始
|
||||
|
||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 頁面下載最新版本
|
||||
|
||||
**首次使用**請務必查看以下文件中的使用教學
|
||||
|
||||
> 專案為非營利性質,涉及 對接問題/基礎問題/底層框架問題 請自行搜尋解決,本專案社群不提供此類解答。
|
||||
|
||||
## 連結
|
||||
|
||||
| 文件 | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| 文件 | [](https://napneko.pages.dev/) | [](https://napcat.top/) | [](https://napcat.top/) |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
|
||||
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||
|:-:|:-:|
|
||||
|
||||
> 請不要在其他社群提及本專案(包括其他協議端/相關應用端專案)以免引發爭論。如有建議,請到官方交流群討論或提交 PR。
|
||||
|
||||
## 致謝
|
||||
|
||||
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 對本專案的大力支持,參考部分程式碼(已獲授權)
|
||||
|
||||
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美適配本專案的 LLM Bot 框架,在此推薦
|
||||
|
||||
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一隻賽博群友 麥麥 Bot 框架,在此推薦
|
||||
|
||||
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基於 NapCat 的訊息匯出工具,在此推薦
|
||||
|
||||
- 不過最最重要的,還是要感謝螢幕前的你哦~
|
||||
|
||||
---
|
||||
|
||||
## 授權條款
|
||||
|
||||
本專案採用混合協議開源,因此使用本專案時,您需要注意以下幾點:
|
||||
|
||||
1. 第三方函式庫程式碼或修改部分遵循其原始開源授權.
|
||||
2. 本專案已獲取部分專案授權而不受部分約束.
|
||||
3. 專案其餘邏輯程式碼採用[本倉庫開源授權](../LICENSE).
|
||||
|
||||
**本倉庫僅用於提高易用性,實現訊息推送類功能,此外,禁止任何專案未經倉庫主作者授權基於 NapCat 程式碼開發。使用請遵守當地法律法規,由此造成的問題由使用者和提供違規使用教學者負責。**
|
||||
@@ -1,52 +0,0 @@
|
||||
import neostandard from 'neostandard';
|
||||
|
||||
/** 尾随逗号 */
|
||||
const commaDangle = val => {
|
||||
if (val?.rules?.['@stylistic/comma-dangle']?.[0] === 'warn') {
|
||||
const rule = val?.rules?.['@stylistic/comma-dangle']?.[1];
|
||||
Object.keys(rule).forEach(key => {
|
||||
rule[key] = 'always-multiline';
|
||||
});
|
||||
val.rules['@stylistic/comma-dangle'][1] = rule;
|
||||
}
|
||||
|
||||
/** 三元表达式 */
|
||||
if (val?.rules?.['@stylistic/indent']) {
|
||||
val.rules['@stylistic/indent'][2] = {
|
||||
...val.rules?.['@stylistic/indent']?.[2],
|
||||
flatTernaryExpressions: true,
|
||||
offsetTernaryExpressions: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** 支持下划线 - 禁用 camelcase 规则 */
|
||||
if (val?.rules?.camelcase) {
|
||||
val.rules.camelcase = 'off';
|
||||
}
|
||||
|
||||
/** 未使用的变量强制报错 */
|
||||
if (val?.rules?.['@typescript-eslint/no-unused-vars']) {
|
||||
val.rules['@typescript-eslint/no-unused-vars'] = ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}];
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
/** 忽略的文件 */
|
||||
const ignores = [
|
||||
'node_modules',
|
||||
'**/dist/**',
|
||||
'launcher',
|
||||
];
|
||||
|
||||
const options = neostandard({
|
||||
ts: true,
|
||||
ignores,
|
||||
semi: true, // 强制使用分号
|
||||
}).map(commaDangle);
|
||||
|
||||
export default options;
|
||||
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.
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
chcp 65001
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
@@ -7,7 +7,7 @@ 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"
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
@@ -27,6 +27,6 @@ if not exist "%QQpath%" (
|
||||
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%" %*
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
pause
|
||||
pause
|
||||
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
chcp 65001
|
||||
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||
@@ -7,7 +7,7 @@ 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"
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -16,19 +16,18 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
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%" %*
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
|
||||
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
|
||||
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
||||
|
||||
pause
|
||||
pause
|
||||
@@ -1,11 +1,11 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
chcp 65001
|
||||
net session >nul 2>&1
|
||||
if %ERRORLEVEL% == 0 (
|
||||
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\" %*' -Verb runAs"
|
||||
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ 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"
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -25,17 +25,16 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
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%" %*
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
|
||||
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
|
||||
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
|
||||
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
||||
@@ -1,11 +1,11 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
chcp 65001
|
||||
net session >nul 2>&1
|
||||
if %ERRORLEVEL% == 0 (
|
||||
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\" %*' -Verb runAs"
|
||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %1' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ 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"
|
||||
set RetString=%%b
|
||||
goto :napcat_boot
|
||||
)
|
||||
|
||||
@@ -25,10 +25,10 @@ for %%a in ("%RetString%") do (
|
||||
set "pathWithoutUninstall=%%~dpa"
|
||||
)
|
||||
|
||||
set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid: %QQpath%
|
||||
pause
|
||||
exit /b
|
||||
)
|
||||
@@ -36,4 +36,4 @@ if not exist "%QQPath%" (
|
||||
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%" %*
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||
@@ -1,5 +1,5 @@
|
||||
const path = require('path');
|
||||
const CurrentPath = path.dirname(__filename);
|
||||
(async () => {
|
||||
await import('file://' + path.join(CurrentPath, './napcat/napcat.mjs'));
|
||||
})();
|
||||
await import("file://" + path.join(CurrentPath, './napcat/napcat.mjs'));
|
||||
})();
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"verHash": "2c9d3f6c",
|
||||
"version": "9.9.22-40990",
|
||||
"linuxVersion": "3.2.20-40990",
|
||||
"linuxVerHash": "ec800879",
|
||||
"version": "9.9.16-29456",
|
||||
"verHash": "dd395162",
|
||||
"linuxVersion": "3.2.13-29456",
|
||||
"linuxVerHash": "e379390a",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
"productName": "QQ",
|
||||
@@ -17,9 +18,9 @@
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "40990",
|
||||
"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本脚本启动即可
|
||||
BIN
logo.png
BIN
logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 335 KiB |
@@ -4,12 +4,16 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "0.0.1",
|
||||
"version": "4.1.0",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
"name": "NapNeko",
|
||||
"link": "https://github.com/NapNeko"
|
||||
"name": "MliKiowa",
|
||||
"link": "https://github.com/MliKiowa"
|
||||
},
|
||||
{
|
||||
"name": "Young",
|
||||
"link": "https://github.com/Wesley-Young"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
@@ -22,9 +22,3 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# NPM LOCK files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
dist.zip
|
||||
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": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"webui:lint": "eslint . --fix",
|
||||
"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');
|
||||
63
napcat.webui/src/pages/AboutUs.vue
Normal file
63
napcat.webui/src/pages/AboutUs.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<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: 1.0.0 </t-tag>
|
||||
<t-tag class="tag-item" theme="success"> NapCat: 4.?.? </t-tag>
|
||||
<t-tag class="tag-item" theme="success"> Tdesign: 1.10.3 </t-tag>
|
||||
</span>
|
||||
</t-list-item>
|
||||
</t-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</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>
|
||||
244
napcat.webui/src/pages/NetWork.vue
Normal file
244
napcat.webui/src/pages/NetWork.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<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, reactive, Reactive } 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;
|
||||
|
||||
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
|
||||
httpServers: httpServerDefaultConfigs,
|
||||
httpClients: httpClientDefaultConfigs,
|
||||
websocketServers: websocketServerDefaultConfigs,
|
||||
websocketClients: websocketClientDefaultConfigs,
|
||||
};
|
||||
|
||||
const componentMap: Record<
|
||||
ConfigKey,
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent
|
||||
> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
};
|
||||
|
||||
interface ClientPanel {
|
||||
name: string;
|
||||
key: ConfigKey;
|
||||
data: Ref<ConfigUnion>;
|
||||
}
|
||||
|
||||
type ComponentKey = keyof typeof componentMap;
|
||||
|
||||
// TODO: store these state in global store (aka pinia)
|
||||
const activeTab = ref<number>(0);
|
||||
const isDialogVisible = ref(false);
|
||||
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
|
||||
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
|
||||
|
||||
const getComponent = (type: ComponentKey) => {
|
||||
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 = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
|
||||
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
|
||||
};
|
||||
|
||||
const addConfigDataToPanel = (data: NetworkConfig) => {
|
||||
Object.entries(data).forEach(([key, configs]) => {
|
||||
if (key in defaultConfigs) {
|
||||
addToPanel(configs as ConfigUnion[], key as ConfigKey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parsePanelData = (): NetworkConfig => {
|
||||
return {
|
||||
websocketClients: clientPanelData
|
||||
.filter((panel) => panel.key === 'websocketClients')
|
||||
.map((panel) => panel.data as WebsocketClientConfig),
|
||||
websocketServers: clientPanelData
|
||||
.filter((panel) => panel.key === 'websocketServers')
|
||||
.map((panel) => panel.data as WebsocketServerConfig),
|
||||
httpClients: clientPanelData
|
||||
.filter((panel) => panel.key === 'httpClients')
|
||||
.map((panel) => panel.data as HttpClientConfig),
|
||||
httpServers: clientPanelData
|
||||
.filter((panel) => panel.key === 'httpServers')
|
||||
.map((panel) => panel.data as HttpServerConfig),
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// It's better to "saveConfig" instead of using deep watch
|
||||
const saveConfig = async () => {
|
||||
const config = parsePanelData();
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
userConfig.network = config;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
MessagePlugin.success('配置保存成功');
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showAddTabDialog = () => {
|
||||
newTab.value = { name: '', type: 'httpServers' };
|
||||
isDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const addTab = async () => {
|
||||
const { name, type } = newTab.value;
|
||||
if (clientPanelData.some(panel => panel.name === name)) {
|
||||
MessagePlugin.error('选项卡名称已存在');
|
||||
return;
|
||||
}
|
||||
const defaultConfig = structuredClone(defaultConfigs[type]);
|
||||
defaultConfig.name = name;
|
||||
clientPanelData.push({ name, data: defaultConfig, key: type });
|
||||
isDialogVisible.value = false;
|
||||
await nextTick();
|
||||
activeTab.value = clientPanelData.length - 1;
|
||||
MessagePlugin.success('选项卡添加成功');
|
||||
};
|
||||
|
||||
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
|
||||
clientPanelData.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>
|
||||
65
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
65
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<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>
|
||||
71
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
71
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<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>
|
||||
68
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
68
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<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>
|
||||
74
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
74
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<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>
|
||||
267
napcat.webui/src/pages/new/NetWork.old.vue
Normal file
267
napcat.webui/src/pages/new/NetWork.old.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<t-space class="full-space">
|
||||
<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>
|
||||
<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, reactive, Reactive } 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';
|
||||
|
||||
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
|
||||
|
||||
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
|
||||
|
||||
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
|
||||
httpServers: httpServerDefaultConfigs,
|
||||
httpClients: httpClientDefaultConfigs,
|
||||
websocketServers: websocketServerDefaultConfigs,
|
||||
websocketClients: websocketClientDefaultConfigs,
|
||||
};
|
||||
|
||||
const componentMap: Record<
|
||||
ConfigKey,
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent
|
||||
> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
};
|
||||
|
||||
interface ClientPanel {
|
||||
name: string;
|
||||
key: ConfigKey;
|
||||
data: Ref<ConfigUnion>;
|
||||
}
|
||||
|
||||
type ComponentKey = keyof typeof componentMap;
|
||||
|
||||
// TODO: store these state in global store (aka pinia)
|
||||
const activeTab = ref<number>(0);
|
||||
const isDialogVisible = ref(false);
|
||||
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
|
||||
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
|
||||
|
||||
const getComponent = (type: ComponentKey) => {
|
||||
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 = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
|
||||
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
|
||||
};
|
||||
|
||||
const addConfigDataToPanel = (data: NetworkConfig) => {
|
||||
Object.entries(data).forEach(([key, configs]) => {
|
||||
if (key in defaultConfigs) {
|
||||
addToPanel(configs as ConfigUnion[], key as ConfigKey);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const parsePanelData = (): NetworkConfig => {
|
||||
return {
|
||||
websocketClients: clientPanelData
|
||||
.filter((panel) => panel.key === 'websocketClients')
|
||||
.map((panel) => panel.data as WebsocketClientConfig),
|
||||
websocketServers: clientPanelData
|
||||
.filter((panel) => panel.key === 'websocketServers')
|
||||
.map((panel) => panel.data as WebsocketServerConfig),
|
||||
httpClients: clientPanelData
|
||||
.filter((panel) => panel.key === 'httpClients')
|
||||
.map((panel) => panel.data as HttpClientConfig),
|
||||
httpServers: clientPanelData
|
||||
.filter((panel) => panel.key === 'httpServers')
|
||||
.map((panel) => panel.data as HttpServerConfig),
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// It's better to "saveConfig" instead of using deep watch
|
||||
const saveConfig = async () => {
|
||||
const config = parsePanelData();
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
userConfig.network = config;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
MessagePlugin.success('配置保存成功');
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showAddTabDialog = () => {
|
||||
newTab.value = { name: '', type: 'httpServers' };
|
||||
isDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const addTab = async () => {
|
||||
const { name, type } = newTab.value;
|
||||
if (clientPanelData.some(panel => panel.name === name)) {
|
||||
MessagePlugin.error('选项卡名称已存在');
|
||||
return;
|
||||
}
|
||||
const defaultConfig = structuredClone(defaultConfigs[type]);
|
||||
defaultConfig.name = name;
|
||||
clientPanelData.push({ name, data: defaultConfig, key: type });
|
||||
isDialogVisible.value = false;
|
||||
await nextTick();
|
||||
activeTab.value = clientPanelData.length - 1;
|
||||
MessagePlugin.success('选项卡添加成功');
|
||||
};
|
||||
|
||||
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
|
||||
clientPanelData.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>
|
||||
<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>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
74
package.json
74
package.json
@@ -2,35 +2,59 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"version": "4.1.0",
|
||||
"scripts": {
|
||||
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
|
||||
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
|
||||
"build:shell:config": "pnpm --filter napcat-shell run build && pnpm --filter napcat-develop run copy-env",
|
||||
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
|
||||
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
|
||||
"build:plugin-builtin": "pnpm --filter napcat-plugin-builtin run build || exit 1",
|
||||
"build:openapi": "pnpm --filter napcat-schema run build:openapi || exit 1",
|
||||
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
|
||||
"typecheck": "pnpm -r --if-present run typecheck",
|
||||
"test": "pnpm --filter napcat-test run test",
|
||||
"test:ui": "pnpm --filter napcat-test run test:ui",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
"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": {
|
||||
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||
"@vitest/ui": "^4.0.9",
|
||||
"eslint": "^9.39.1",
|
||||
"neostandard": "^0.12.2",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-cp": "^6.0.3",
|
||||
"vitest": "^4.0.9"
|
||||
"@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",
|
||||
"ws": "^8.18.3"
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"piscina": "^4.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { InstanceContext, NapCatCore } from 'napcat-core';
|
||||
import { NapCatPathWrapper } from 'napcat-common/src/path';
|
||||
import { NapCatOneBot11Adapter } from 'napcat-onebot';
|
||||
import { NapCatProtocolAdapter } from 'napcat-protocol';
|
||||
|
||||
// 协议适配器类型
|
||||
export type ProtocolAdapterType = 'onebot11' | 'napcat-protocol';
|
||||
|
||||
// 协议适配器接口
|
||||
export interface IProtocolAdapter {
|
||||
readonly name: string;
|
||||
readonly enabled: boolean;
|
||||
init (): Promise<void>;
|
||||
close (): Promise<void>;
|
||||
}
|
||||
|
||||
// 协议适配器包装器
|
||||
class OneBotAdapterWrapper implements IProtocolAdapter {
|
||||
readonly name = 'onebot11';
|
||||
private adapter: NapCatOneBot11Adapter;
|
||||
|
||||
constructor (adapter: NapCatOneBot11Adapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
get enabled (): boolean {
|
||||
return true; // OneBot11 默认启用
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
await this.adapter.InitOneBot();
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.adapter.networkManager.closeAllAdapters();
|
||||
}
|
||||
|
||||
getAdapter (): NapCatOneBot11Adapter {
|
||||
return this.adapter;
|
||||
}
|
||||
}
|
||||
|
||||
// NapCat Protocol 适配器包装器
|
||||
class NapCatProtocolAdapterWrapper implements IProtocolAdapter {
|
||||
readonly name = 'napcat-protocol';
|
||||
private adapter: NapCatProtocolAdapter;
|
||||
|
||||
constructor (adapter: NapCatProtocolAdapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
get enabled (): boolean {
|
||||
return this.adapter.isEnabled();
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
await this.adapter.initProtocol();
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
await this.adapter.close();
|
||||
}
|
||||
|
||||
getAdapter (): NapCatProtocolAdapter {
|
||||
return this.adapter;
|
||||
}
|
||||
}
|
||||
|
||||
// 协议适配器管理器
|
||||
export class NapCatAdapterManager {
|
||||
private core: NapCatCore;
|
||||
private context: InstanceContext;
|
||||
private pathWrapper: NapCatPathWrapper;
|
||||
|
||||
// 协议适配器实例
|
||||
private onebotAdapter: OneBotAdapterWrapper | null = null;
|
||||
private napcatProtocolAdapter: NapCatProtocolAdapterWrapper | null = null;
|
||||
|
||||
// 所有已注册的适配器
|
||||
private adapters: Map<string, IProtocolAdapter> = new Map();
|
||||
|
||||
constructor (core: NapCatCore, context: InstanceContext, pathWrapper: NapCatPathWrapper) {
|
||||
this.core = core;
|
||||
this.context = context;
|
||||
this.pathWrapper = pathWrapper;
|
||||
}
|
||||
|
||||
// 初始化所有协议适配器
|
||||
async initAdapters (): Promise<void> {
|
||||
this.context.logger.log('[AdapterManager] 开始初始化协议适配器...');
|
||||
|
||||
// 初始化 OneBot11 适配器 (默认启用)
|
||||
try {
|
||||
const onebot = new NapCatOneBot11Adapter(this.core, this.context, this.pathWrapper);
|
||||
this.onebotAdapter = new OneBotAdapterWrapper(onebot);
|
||||
this.adapters.set('onebot11', this.onebotAdapter);
|
||||
await this.onebotAdapter.init();
|
||||
this.context.logger.log('[AdapterManager] OneBot11 适配器初始化完成');
|
||||
} catch (e) {
|
||||
this.context.logger.logError('[AdapterManager] OneBot11 适配器初始化失败:', e);
|
||||
}
|
||||
|
||||
// 初始化 NapCat Protocol 适配器 (默认关闭,需要配置启用)
|
||||
try {
|
||||
const napcatProtocol = new NapCatProtocolAdapter(this.core, this.context, this.pathWrapper);
|
||||
this.napcatProtocolAdapter = new NapCatProtocolAdapterWrapper(napcatProtocol);
|
||||
this.adapters.set('napcat-protocol', this.napcatProtocolAdapter);
|
||||
|
||||
if (this.napcatProtocolAdapter.enabled) {
|
||||
await this.napcatProtocolAdapter.init();
|
||||
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器初始化完成');
|
||||
} else {
|
||||
this.context.logger.log('[AdapterManager] NapCat Protocol 适配器未启用,跳过初始化');
|
||||
}
|
||||
} catch (e) {
|
||||
this.context.logger.logError('[AdapterManager] NapCat Protocol 适配器初始化失败:', e);
|
||||
}
|
||||
|
||||
this.context.logger.log(`[AdapterManager] 协议适配器初始化完成,已加载 ${this.adapters.size} 个适配器`);
|
||||
}
|
||||
|
||||
// 获取 OneBot11 适配器
|
||||
getOneBotAdapter (): NapCatOneBot11Adapter | null {
|
||||
return this.onebotAdapter?.getAdapter() ?? null;
|
||||
}
|
||||
|
||||
// 获取 NapCat Protocol 适配器
|
||||
getNapCatProtocolAdapter (): NapCatProtocolAdapter | null {
|
||||
return this.napcatProtocolAdapter?.getAdapter() ?? null;
|
||||
}
|
||||
|
||||
// 获取指定适配器
|
||||
getAdapter (name: ProtocolAdapterType): IProtocolAdapter | undefined {
|
||||
return this.adapters.get(name);
|
||||
}
|
||||
|
||||
// 获取所有已启用的适配器
|
||||
getEnabledAdapters (): IProtocolAdapter[] {
|
||||
return Array.from(this.adapters.values()).filter(adapter => adapter.enabled);
|
||||
}
|
||||
|
||||
// 获取所有适配器
|
||||
getAllAdapters (): IProtocolAdapter[] {
|
||||
return Array.from(this.adapters.values());
|
||||
}
|
||||
|
||||
// 关闭所有适配器
|
||||
async closeAllAdapters (): Promise<void> {
|
||||
this.context.logger.log('[AdapterManager] 开始关闭所有协议适配器...');
|
||||
|
||||
for (const [name, adapter] of this.adapters) {
|
||||
try {
|
||||
await adapter.close();
|
||||
this.context.logger.log(`[AdapterManager] ${name} 适配器已关闭`);
|
||||
} catch (e) {
|
||||
this.context.logger.logError(`[AdapterManager] 关闭 ${name} 适配器失败:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
this.adapters.clear();
|
||||
this.context.logger.log('[AdapterManager] 所有协议适配器已关闭');
|
||||
}
|
||||
|
||||
// 重新加载指定适配器
|
||||
async reloadAdapter (name: ProtocolAdapterType): Promise<void> {
|
||||
const adapter = this.adapters.get(name);
|
||||
if (adapter) {
|
||||
await adapter.close();
|
||||
await adapter.init();
|
||||
this.context.logger.log(`[AdapterManager] ${name} 适配器已重新加载`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { NapCatOneBot11Adapter } from 'napcat-onebot';
|
||||
export { NapCatProtocolAdapter } from 'napcat-protocol';
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "napcat-adapter",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-common": "workspace:*",
|
||||
"napcat-onebot": "workspace:*",
|
||||
"napcat-protocol": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": [
|
||||
"*.ts",
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "napcat-common",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./src/*": {
|
||||
"import": "./src/*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.18.0",
|
||||
"file-type": "^21.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void | Promise<void>;
|
||||
|
||||
export class CancelableTask<T> {
|
||||
private promise: Promise<T>;
|
||||
private cancelCallback: (() => void) | null = null;
|
||||
private isCanceled = false;
|
||||
private cancelListeners: Array<() => void> = [];
|
||||
|
||||
constructor (executor: TaskExecutor<T>) {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
const onCancel = (callback: () => void) => {
|
||||
this.cancelCallback = callback;
|
||||
};
|
||||
|
||||
const execute = async () => {
|
||||
try {
|
||||
await executor(
|
||||
(value) => {
|
||||
if (!this.isCanceled) {
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!this.isCanceled) {
|
||||
reject(reason);
|
||||
}
|
||||
},
|
||||
onCancel
|
||||
);
|
||||
} catch (error) {
|
||||
if (!this.isCanceled) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
execute();
|
||||
});
|
||||
}
|
||||
|
||||
public cancel () {
|
||||
if (this.cancelCallback) {
|
||||
this.cancelCallback();
|
||||
}
|
||||
this.isCanceled = true;
|
||||
this.cancelListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
public isTaskCanceled (): boolean {
|
||||
return this.isCanceled;
|
||||
}
|
||||
|
||||
public onCancel (listener: () => void) {
|
||||
this.cancelListeners.push(listener);
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onrejected);
|
||||
}
|
||||
|
||||
public finally (onfinally?: (() => void) | undefined | null): Promise<T> {
|
||||
return this.promise.finally(onfinally);
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator] () {
|
||||
return {
|
||||
next: () => this.promise.then(value => ({ value, done: true })),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
import fs from 'fs';
|
||||
// generate Claude 3.7 Sonet Thinking
|
||||
|
||||
interface FileRecord {
|
||||
filePath: string;
|
||||
addedTime: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
interface CleanupTask {
|
||||
fileRecord: FileRecord;
|
||||
timer: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
class CleanupQueue {
|
||||
private tasks: Map<string, CleanupTask> = new Map();
|
||||
private readonly MAX_RETRIES = 3;
|
||||
private isProcessing: boolean = false;
|
||||
private pendingOperations: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* 执行队列中的待处理操作,确保异步安全
|
||||
*/
|
||||
private executeNextOperation (): void {
|
||||
if (this.pendingOperations.length === 0) {
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
const operation = this.pendingOperations.shift();
|
||||
operation?.();
|
||||
|
||||
// 使用 setImmediate 允许事件循环继续,防止阻塞
|
||||
setImmediate(() => this.executeNextOperation());
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全执行操作,防止竞态条件
|
||||
* @param operation 要执行的操作
|
||||
*/
|
||||
private safeExecute (operation: () => void): void {
|
||||
this.pendingOperations.push(operation);
|
||||
if (!this.isProcessing) {
|
||||
this.executeNextOperation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param filePath 文件路径
|
||||
* @returns 文件是否存在
|
||||
*/
|
||||
private fileExists (filePath: string): boolean {
|
||||
try {
|
||||
return fs.existsSync(filePath);
|
||||
} catch (_error) {
|
||||
// console.log(`检查文件存在出错: ${filePath}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文件到清理队列
|
||||
* @param filePath 文件路径
|
||||
* @param cleanupDelay 清理延迟时间(毫秒)
|
||||
*/
|
||||
addFile (filePath: string, cleanupDelay: number): void {
|
||||
this.safeExecute(() => {
|
||||
// 如果文件已在队列中,取消原来的计时器
|
||||
if (this.tasks.has(filePath)) {
|
||||
this.cancelCleanup(filePath);
|
||||
}
|
||||
|
||||
// 创建新的文件记录
|
||||
const fileRecord: FileRecord = {
|
||||
filePath,
|
||||
addedTime: Date.now(),
|
||||
retries: 0,
|
||||
};
|
||||
|
||||
// 设置计时器
|
||||
const timer = setTimeout(() => {
|
||||
this.cleanupFile(fileRecord, cleanupDelay);
|
||||
}, cleanupDelay);
|
||||
|
||||
// 添加到任务队列
|
||||
this.tasks.set(filePath, { fileRecord, timer });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加文件到清理队列
|
||||
* @param filePaths 文件路径数组
|
||||
* @param cleanupDelay 清理延迟时间(毫秒)
|
||||
*/
|
||||
addFiles (filePaths: string[], cleanupDelay: number): void {
|
||||
this.safeExecute(() => {
|
||||
for (const filePath of filePaths) {
|
||||
// 内部直接处理,不通过 safeExecute 以保证批量操作的原子性
|
||||
if (this.tasks.has(filePath)) {
|
||||
// 取消已有的计时器,但不使用 cancelCleanup 方法以避免重复的安全检查
|
||||
const existingTask = this.tasks.get(filePath);
|
||||
if (existingTask) {
|
||||
clearTimeout(existingTask.timer);
|
||||
}
|
||||
}
|
||||
|
||||
const fileRecord: FileRecord = {
|
||||
filePath,
|
||||
addedTime: Date.now(),
|
||||
retries: 0,
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.cleanupFile(fileRecord, cleanupDelay);
|
||||
}, cleanupDelay);
|
||||
|
||||
this.tasks.set(filePath, { fileRecord, timer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理文件
|
||||
* @param record 文件记录
|
||||
* @param delay 延迟时间,用于重试
|
||||
*/
|
||||
private cleanupFile (record: FileRecord, delay: number): void {
|
||||
this.safeExecute(() => {
|
||||
// 首先检查文件是否存在,不存在则视为清理成功
|
||||
if (!this.fileExists(record.filePath)) {
|
||||
// console.log(`文件已不存在,跳过清理: ${record.filePath}`);
|
||||
this.tasks.delete(record.filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试删除文件
|
||||
fs.unlinkSync(record.filePath);
|
||||
// 删除成功,从队列中移除任务
|
||||
this.tasks.delete(record.filePath);
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
|
||||
// 明确处理文件不存在的情况
|
||||
if (err.code === 'ENOENT') {
|
||||
// console.log(`文件在删除时不存在,视为清理成功: ${record.filePath}`);
|
||||
this.tasks.delete(record.filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 文件没有访问权限等情况
|
||||
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
||||
// console.error(`没有权限删除文件: ${record.filePath}`, err);
|
||||
}
|
||||
|
||||
// 其他删除失败情况,考虑重试
|
||||
if (record.retries < this.MAX_RETRIES - 1) {
|
||||
// 还有重试机会,增加重试次数
|
||||
record.retries++;
|
||||
// console.log(`清理文件失败,将重试(${record.retries}/${this.MAX_RETRIES}): ${record.filePath}`);
|
||||
|
||||
// 设置相同的延迟时间再次尝试
|
||||
const timer = setTimeout(() => {
|
||||
this.cleanupFile(record, delay);
|
||||
}, delay);
|
||||
|
||||
// 更新任务
|
||||
this.tasks.set(record.filePath, { fileRecord: record, timer });
|
||||
} else {
|
||||
// 已达到最大重试次数,从队列中移除任务
|
||||
this.tasks.delete(record.filePath);
|
||||
// console.error(`清理文件失败,已达最大重试次数(${this.MAX_RETRIES}): ${record.filePath}`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消文件的清理任务
|
||||
* @param filePath 文件路径
|
||||
* @returns 是否成功取消
|
||||
*/
|
||||
cancelCleanup (filePath: string): boolean {
|
||||
let cancelled = false;
|
||||
this.safeExecute(() => {
|
||||
const task = this.tasks.get(filePath);
|
||||
if (task) {
|
||||
clearTimeout(task.timer);
|
||||
this.tasks.delete(filePath);
|
||||
cancelled = true;
|
||||
}
|
||||
});
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取队列中的文件数量
|
||||
* @returns 文件数量
|
||||
*/
|
||||
getQueueSize (): number {
|
||||
return this.tasks.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有待清理的文件
|
||||
* @returns 文件路径数组
|
||||
*/
|
||||
getPendingFiles (): string[] {
|
||||
return Array.from(this.tasks.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有清理任务
|
||||
*/
|
||||
clearAll (): void {
|
||||
this.safeExecute(() => {
|
||||
// 取消所有定时器
|
||||
for (const task of this.tasks.values()) {
|
||||
clearTimeout(task.timer);
|
||||
}
|
||||
this.tasks.clear();
|
||||
// console.log('已清空所有清理任务');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const cleanTaskQueue = new CleanupQueue();
|
||||
9
packages/napcat-common/src/env.d.ts
vendored
9
packages/napcat-common/src/env.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NAPCAT_VERSION: string;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,42 +0,0 @@
|
||||
type Handler<T> = () => T | Promise<T>;
|
||||
type Checker<T> = (result: T) => T | Promise<T>;
|
||||
|
||||
export class Fallback<T> {
|
||||
private handlers: Handler<T>[] = [];
|
||||
private checker: Checker<T>;
|
||||
|
||||
constructor (checker?: Checker<T>) {
|
||||
this.checker = checker || (async (result: T) => result);
|
||||
}
|
||||
|
||||
add (handler: Handler<T>): this {
|
||||
this.handlers.push(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 执行处理程序链
|
||||
async run (): Promise<T> {
|
||||
const errors: Error[] = [];
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
const result = await handler();
|
||||
const data = await this.checker(result);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
throw new AggregateError(errors, 'All handlers failed');
|
||||
}
|
||||
}
|
||||
export class FallbackUtil {
|
||||
static boolchecker<T>(value: T, condition: boolean): T {
|
||||
if (condition) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error('Condition is false, throwing error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Peer } from './types';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
class TimeBasedCache<K, V> {
|
||||
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
|
||||
private keyList = new Set<K>();
|
||||
private operationCount = 0;
|
||||
|
||||
constructor (private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
|
||||
|
||||
public put (key: K, value: V): void {
|
||||
const timestamp = Date.now();
|
||||
const cacheEntry = { value, timestamp, frequency: 1 };
|
||||
this.cache.set(key, cacheEntry);
|
||||
this.keyList.add(key);
|
||||
this.operationCount++;
|
||||
if (this.keyList.size > this.maxCapacity) this.evict();
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
}
|
||||
|
||||
public get (key: K): V | undefined {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && Date.now() - entry.timestamp < this.ttl) {
|
||||
entry.timestamp = Date.now();
|
||||
entry.frequency++;
|
||||
this.operationCount++;
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
return entry.value;
|
||||
} else {
|
||||
this.deleteKey(key);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private cleanup (count: number): void {
|
||||
const currentTime = Date.now();
|
||||
let cleaned = 0;
|
||||
for (const key of this.keyList) {
|
||||
if (cleaned >= count) break;
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && currentTime - entry.timestamp >= this.ttl) {
|
||||
this.deleteKey(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
this.operationCount = 0; // 重置操作计数器
|
||||
}
|
||||
|
||||
private deleteKey (key: K): void {
|
||||
this.cache.delete(key);
|
||||
this.keyList.delete(key);
|
||||
}
|
||||
|
||||
private evict (): void {
|
||||
while (this.keyList.size > this.maxCapacity) {
|
||||
let oldestKey: K | undefined;
|
||||
let minFrequency = Infinity;
|
||||
for (const key of this.keyList) {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.frequency < minFrequency) {
|
||||
minFrequency = entry.frequency;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
if (oldestKey !== undefined) this.deleteKey(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FileUUIDData {
|
||||
peer: Peer;
|
||||
modelId?: string;
|
||||
fileId?: string;
|
||||
msgId?: string;
|
||||
elementId?: string;
|
||||
fileUUID?: string;
|
||||
}
|
||||
|
||||
class FileUUIDManager {
|
||||
private cache: TimeBasedCache<string, FileUUIDData>;
|
||||
|
||||
constructor (ttl: number) {
|
||||
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
|
||||
}
|
||||
|
||||
public encode (data: FileUUIDData, endString: string = '', customUUID?: string): string {
|
||||
const uuid = customUUID || randomUUID().replace(/-/g, '') + endString;
|
||||
this.cache.put(uuid, data);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public decode (uuid: string): FileUUIDData | undefined {
|
||||
return this.cache.get(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUIDWrap {
|
||||
private manager: FileUUIDManager;
|
||||
|
||||
constructor (ttl: number = 86400000) {
|
||||
this.manager = new FileUUIDManager(ttl);
|
||||
}
|
||||
|
||||
public encodeModelId (peer: Peer, modelId: string, fileId: string, fileUUID: string = '', endString: string = '', customUUID?: string): string {
|
||||
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
|
||||
}
|
||||
|
||||
public decodeModelId (uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
|
||||
public encode (peer: Peer, msgId: string, elementId: string, fileUUID: string = '', customUUID?: string): string {
|
||||
return this.manager.encode({ peer, msgId, elementId, fileUUID }, '', customUUID);
|
||||
}
|
||||
|
||||
public decode (uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();
|
||||
@@ -1,440 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import { stat } from 'fs/promises';
|
||||
import crypto, { randomUUID } from 'crypto';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import tls from 'node:tls';
|
||||
import { solveProblem } from '@/napcat-common/src/helper';
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
url: string;
|
||||
headers?: Record<string, string> | string;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
type Uri2LocalRes = {
|
||||
success: boolean,
|
||||
errMsg: string,
|
||||
fileName: string,
|
||||
path: string;
|
||||
};
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export function checkFileExist (path: string, timeout: number = 3000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
function check () {
|
||||
if (fs.existsSync(path)) {
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error(`文件不存在: ${path}`));
|
||||
} else {
|
||||
setTimeout(check, 100);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export async function checkFileExistV2 (path: string, timeout: number = 3000): Promise<void> {
|
||||
// 使用 Promise.race 来同时进行文件状态检查和超时计时
|
||||
// Promise.race 会返回第一个解决(resolve)或拒绝(reject)的 Promise
|
||||
await Promise.race([
|
||||
checkFile(path),
|
||||
timeoutPromise(timeout, `文件不存在: ${path}`),
|
||||
]);
|
||||
}
|
||||
|
||||
// 转换超时时间至 Promise
|
||||
function timeoutPromise (timeout: number, errorMsg: string): Promise<void> {
|
||||
return new Promise((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(errorMsg));
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
// 异步检查文件是否存在
|
||||
async function checkFile (path: string): Promise<void> {
|
||||
try {
|
||||
await stat(path);
|
||||
} catch (error: unknown) {
|
||||
if ((error as Error & { code: string; }).code === 'ENOENT') {
|
||||
// 如果文件不存在,则抛出一个错误
|
||||
throw new Error(`文件不存在: ${path}`);
|
||||
} else {
|
||||
// 对于 stat 调用的其他错误,重新抛出
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
||||
}
|
||||
|
||||
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) => {
|
||||
// 当读取到数据时,更新哈希对象的状态
|
||||
hash.update(data);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
// 文件读取完成,计算哈希
|
||||
const md5 = hash.digest('hex');
|
||||
resolve(md5);
|
||||
});
|
||||
|
||||
stream.on('error', (err: Error) => {
|
||||
// 处理可能的读取错误
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||
let url: string;
|
||||
let proxy: string | undefined;
|
||||
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;
|
||||
proxy = options.proxy;
|
||||
if (options.headers) {
|
||||
if (typeof options.headers === 'string') {
|
||||
headers = JSON.parse(options.headers);
|
||||
} else {
|
||||
headers = options.headers;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (useReferer && !headers['Referer']) {
|
||||
headers['Referer'] = url;
|
||||
}
|
||||
|
||||
// 如果配置了代理,使用代理下载
|
||||
if (proxy) {
|
||||
try {
|
||||
const response = await httpDownloadWithProxy(url, headers, proxy);
|
||||
return new Response(response, { status: 200, statusText: 'OK' });
|
||||
} catch (proxyError) {
|
||||
// 如果代理失败,记录错误并尝试直接下载
|
||||
console.error('代理下载失败,尝试直接下载:', proxyError);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
||||
if (err.cause) {
|
||||
throw err.cause;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return fetchRes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 HTTP/HTTPS 代理下载文件
|
||||
*/
|
||||
function httpDownloadWithProxy (url: string, headers: Record<string, string>, proxy: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const targetUrl = new URL(url);
|
||||
const proxyUrl = new URL(proxy);
|
||||
|
||||
const isTargetHttps = targetUrl.protocol === 'https:';
|
||||
const proxyPort = parseInt(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80);
|
||||
|
||||
// 代理认证头
|
||||
const proxyAuthHeader = proxyUrl.username && proxyUrl.password
|
||||
? { 'Proxy-Authorization': 'Basic ' + Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString('base64') }
|
||||
: {};
|
||||
|
||||
if (isTargetHttps) {
|
||||
// HTTPS 目标:需要通过 CONNECT 建立隧道
|
||||
const connectReq = http.request({
|
||||
host: proxyUrl.hostname,
|
||||
port: proxyPort,
|
||||
method: 'CONNECT',
|
||||
path: `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||
headers: {
|
||||
'Host': `${targetUrl.hostname}:${targetUrl.port || 443}`,
|
||||
...proxyAuthHeader,
|
||||
},
|
||||
});
|
||||
|
||||
connectReq.on('connect', (res, socket) => {
|
||||
if (res.statusCode !== 200) {
|
||||
socket.destroy();
|
||||
reject(new Error(`代理 CONNECT 失败: ${res.statusCode} ${res.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 在隧道上建立 TLS 连接
|
||||
const tlsSocket = tls.connect({
|
||||
socket: socket,
|
||||
servername: targetUrl.hostname,
|
||||
rejectUnauthorized: true,
|
||||
}, () => {
|
||||
// TLS 握手成功,发送 HTTP 请求
|
||||
const requestPath = targetUrl.pathname + targetUrl.search;
|
||||
const requestHeaders = {
|
||||
...headers,
|
||||
'Host': targetUrl.hostname,
|
||||
'Connection': 'close',
|
||||
};
|
||||
|
||||
const headerLines = Object.entries(requestHeaders)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\r\n');
|
||||
|
||||
const httpRequest = `GET ${requestPath} HTTP/1.1\r\n${headerLines}\r\n\r\n`;
|
||||
tlsSocket.write(httpRequest);
|
||||
});
|
||||
|
||||
// 解析 HTTP 响应
|
||||
let responseData = Buffer.alloc(0);
|
||||
let headersParsed = false;
|
||||
let statusCode = 0;
|
||||
let isChunked = false;
|
||||
let bodyData = Buffer.alloc(0);
|
||||
let redirectLocation: string | null = null;
|
||||
|
||||
tlsSocket.on('data', (chunk: Buffer) => {
|
||||
responseData = Buffer.concat([responseData, chunk]);
|
||||
|
||||
if (!headersParsed) {
|
||||
const headerEndIndex = responseData.indexOf('\r\n\r\n');
|
||||
if (headerEndIndex !== -1) {
|
||||
headersParsed = true;
|
||||
const headerStr = responseData.subarray(0, headerEndIndex).toString();
|
||||
const headerLines = headerStr.split('\r\n');
|
||||
|
||||
// 解析状态码
|
||||
const statusLine = headerLines[0];
|
||||
const statusMatch = statusLine?.match(/HTTP\/\d\.\d\s+(\d+)/);
|
||||
statusCode = statusMatch ? parseInt(statusMatch[1]!) : 0;
|
||||
|
||||
// 解析响应头
|
||||
for (const line of headerLines.slice(1)) {
|
||||
const [key, ...valueParts] = line.split(':');
|
||||
const value = valueParts.join(':').trim();
|
||||
if (key?.toLowerCase() === 'transfer-encoding' && value.toLowerCase() === 'chunked') {
|
||||
isChunked = true;
|
||||
} else if (key?.toLowerCase() === 'location') {
|
||||
redirectLocation = value;
|
||||
}
|
||||
}
|
||||
|
||||
bodyData = responseData.subarray(headerEndIndex + 4);
|
||||
}
|
||||
} else {
|
||||
bodyData = Buffer.concat([bodyData, chunk]);
|
||||
}
|
||||
});
|
||||
|
||||
tlsSocket.on('end', () => {
|
||||
// 处理重定向
|
||||
if (statusCode >= 300 && statusCode < 400 && redirectLocation) {
|
||||
const redirectUrl = redirectLocation.startsWith('http')
|
||||
? redirectLocation
|
||||
: `${targetUrl.protocol}//${targetUrl.host}${redirectLocation}`;
|
||||
httpDownloadWithProxy(redirectUrl, headers, proxy).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode !== 200) {
|
||||
reject(new Error(`下载失败: ${statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 chunked 编码
|
||||
if (isChunked) {
|
||||
resolve(parseChunkedBody(bodyData));
|
||||
} else {
|
||||
resolve(bodyData);
|
||||
}
|
||||
});
|
||||
|
||||
tlsSocket.on('error', (err) => {
|
||||
reject(new Error(`TLS 连接错误: ${err.message}`));
|
||||
});
|
||||
});
|
||||
|
||||
connectReq.on('error', (err) => {
|
||||
reject(new Error(`代理连接错误: ${err.message}`));
|
||||
});
|
||||
|
||||
connectReq.end();
|
||||
} else {
|
||||
// HTTP 目标:直接通过代理请求
|
||||
const req = http.request({
|
||||
host: proxyUrl.hostname,
|
||||
port: proxyPort,
|
||||
method: 'GET',
|
||||
path: url, // 完整 URL
|
||||
headers: {
|
||||
...headers,
|
||||
'Host': targetUrl.hostname,
|
||||
...proxyAuthHeader,
|
||||
},
|
||||
}, (response) => {
|
||||
handleResponse(response, resolve, reject, url, headers, proxy);
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
reject(new Error(`代理请求错误: ${err.message}`));
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 chunked 编码的响应体
|
||||
*/
|
||||
function parseChunkedBody (data: Buffer): Buffer {
|
||||
const chunks: Buffer[] = [];
|
||||
let offset = 0;
|
||||
|
||||
while (offset < data.length) {
|
||||
// 查找 chunk 大小行的结束
|
||||
const lineEnd = data.indexOf('\r\n', offset);
|
||||
if (lineEnd === -1) break;
|
||||
|
||||
const sizeStr = data.subarray(offset, lineEnd).toString().split(';')[0]; // 忽略 chunk 扩展
|
||||
const chunkSize = parseInt(sizeStr!, 16);
|
||||
|
||||
if (chunkSize === 0) break; // 最后一个 chunk
|
||||
|
||||
const chunkStart = lineEnd + 2;
|
||||
const chunkEnd = chunkStart + chunkSize;
|
||||
|
||||
if (chunkEnd > data.length) break;
|
||||
|
||||
chunks.push(data.subarray(chunkStart, chunkEnd));
|
||||
offset = chunkEnd + 2; // 跳过 chunk 数据后的 \r\n
|
||||
}
|
||||
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 响应
|
||||
*/
|
||||
function handleResponse (
|
||||
response: http.IncomingMessage,
|
||||
resolve: (value: Buffer) => void,
|
||||
reject: (reason: Error) => void,
|
||||
_url: string,
|
||||
headers: Record<string, string>,
|
||||
proxy: string
|
||||
): void {
|
||||
// 处理重定向
|
||||
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||
httpDownloadWithProxy(response.headers.location, headers, proxy).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`下载失败: ${response.statusCode} ${response.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
response.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
response.on('error', reject);
|
||||
}
|
||||
|
||||
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||
const useReferer = typeof options === 'string';
|
||||
let resp = await tryDownload(options);
|
||||
if (resp.status === 403 && useReferer) {
|
||||
resp = await tryDownload(options, true);
|
||||
}
|
||||
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
|
||||
const blob = await resp.blob();
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
|
||||
export 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(path.normalize(uri))) {
|
||||
return { Uri: path.normalize(uri), Type: FileUriType.Local };
|
||||
}
|
||||
return undefined;
|
||||
}, Uri);
|
||||
if (LocalFileRet) return LocalFileRet;
|
||||
const OtherFileRet = await solveProblem((uri: string) => {
|
||||
// 再判断是否是Http
|
||||
if (uri.startsWith('http:') || uri.startsWith('https:')) {
|
||||
return { Uri: uri, Type: FileUriType.Remote };
|
||||
}
|
||||
// 再判断是否是Base64
|
||||
if (uri.startsWith('base64:')) {
|
||||
return { Uri: uri, Type: FileUriType.Base64 };
|
||||
}
|
||||
// 默认file://
|
||||
if (uri.startsWith('file:')) {
|
||||
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
if (uri.startsWith('data:')) {
|
||||
const data = uri.split(',')[1];
|
||||
if (data) return { Uri: data, Type: FileUriType.Base64 };
|
||||
}
|
||||
return undefined;
|
||||
}, Uri);
|
||||
if (OtherFileRet) return OtherFileRet;
|
||||
|
||||
return { Uri, Type: FileUriType.Unknown };
|
||||
}
|
||||
|
||||
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>, proxy?: string): Promise<Uri2LocalRes> {
|
||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||
|
||||
const filePath = path.join(dir, filename);
|
||||
|
||||
switch (UriType) {
|
||||
case FileUriType.Local: {
|
||||
const fileExt = path.extname(HandledUri);
|
||||
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
|
||||
const tempFilePath = path.join(dir, filename + fileExt);
|
||||
fs.copyFileSync(HandledUri, tempFilePath);
|
||||
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
|
||||
}
|
||||
|
||||
case FileUriType.Remote: {
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
||||
case FileUriType.Base64: {
|
||||
const base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||
const base64Buffer = Buffer.from(base64, 'base64');
|
||||
fs.writeFileSync(filePath, base64Buffer);
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
export interface ResourceConfig<T extends any[], R> {
|
||||
/** 资源获取函数 */
|
||||
resourceFn: (...args: T) => Promise<R>;
|
||||
/** 失败后禁用时间(毫秒),默认 30 秒 */
|
||||
disableTime?: number;
|
||||
/** 最大重试次数,默认 3 次 */
|
||||
maxRetries?: number;
|
||||
/** 主动测试间隔(毫秒),默认 60 秒 */
|
||||
healthCheckInterval?: number;
|
||||
/** 最大健康检查失败次数,超过后永久禁用,默认 5 次 */
|
||||
maxHealthCheckFailures?: number;
|
||||
/** 健康检查函数,如果提供则优先使用此函数进行健康检查 */
|
||||
healthCheckFn?: (...args: T) => Promise<boolean>;
|
||||
/** 测试参数(用于健康检查) */
|
||||
testArgs?: T;
|
||||
}
|
||||
|
||||
interface ResourceTypeState {
|
||||
/** 资源配置 */
|
||||
config: {
|
||||
resourceFn: (...args: any[]) => Promise<any>;
|
||||
healthCheckFn?: (...args: any[]) => Promise<boolean>;
|
||||
disableTime: number;
|
||||
maxRetries: number;
|
||||
healthCheckInterval: number;
|
||||
maxHealthCheckFailures: number;
|
||||
testArgs?: any[];
|
||||
};
|
||||
/** 是否启用 */
|
||||
isEnabled: boolean;
|
||||
/** 禁用截止时间 */
|
||||
disableUntil: number;
|
||||
/** 当前重试次数 */
|
||||
currentRetries: number;
|
||||
/** 健康检查失败次数 */
|
||||
healthCheckFailureCount: number;
|
||||
/** 是否永久禁用 */
|
||||
isPermanentlyDisabled: boolean;
|
||||
/** 上次健康检查时间 */
|
||||
lastHealthCheckTime: number;
|
||||
/** 成功次数统计 */
|
||||
successCount: number;
|
||||
/** 失败次数统计 */
|
||||
failureCount: number;
|
||||
}
|
||||
|
||||
export class ResourceManager {
|
||||
private resourceTypes = new Map<string, ResourceTypeState>();
|
||||
private destroyed = false;
|
||||
|
||||
/**
|
||||
* 调用资源(自动注册或复用已有配置)
|
||||
*/
|
||||
async callResource<T extends any[], R>(
|
||||
type: string,
|
||||
config: ResourceConfig<T, R>,
|
||||
...args: T
|
||||
): Promise<R> {
|
||||
if (this.destroyed) {
|
||||
throw new Error('ResourceManager has been destroyed');
|
||||
}
|
||||
|
||||
// 获取或创建资源类型状态
|
||||
let state = this.resourceTypes.get(type);
|
||||
|
||||
if (!state) {
|
||||
// 首次注册
|
||||
state = {
|
||||
config: {
|
||||
resourceFn: config.resourceFn as (...args: any[]) => Promise<any>,
|
||||
healthCheckFn: config.healthCheckFn as ((...args: any[]) => Promise<boolean>) | undefined,
|
||||
disableTime: config.disableTime ?? 30000,
|
||||
maxRetries: config.maxRetries ?? 3,
|
||||
healthCheckInterval: config.healthCheckInterval ?? 60000,
|
||||
maxHealthCheckFailures: config.maxHealthCheckFailures ?? 20,
|
||||
testArgs: config.testArgs as any[] | undefined,
|
||||
},
|
||||
isEnabled: true,
|
||||
disableUntil: 0,
|
||||
currentRetries: 0,
|
||||
healthCheckFailureCount: 0,
|
||||
isPermanentlyDisabled: false,
|
||||
lastHealthCheckTime: 0,
|
||||
successCount: 0,
|
||||
failureCount: 0,
|
||||
};
|
||||
this.resourceTypes.set(type, state);
|
||||
}
|
||||
|
||||
// 在调用前检查是否需要进行健康检查
|
||||
await this.checkAndPerformHealthCheck(state);
|
||||
|
||||
// 检查资源状态
|
||||
if (state.isPermanentlyDisabled) {
|
||||
throw new Error(`Resource type '${type}' is permanently disabled (success: ${state.successCount}, failure: ${state.failureCount})`);
|
||||
}
|
||||
|
||||
if (!this.isResourceAvailable(type)) {
|
||||
const disableUntilDate = new Date(state.disableUntil).toISOString();
|
||||
throw new Error(`Resource type '${type}' is currently disabled until ${disableUntilDate} (success: ${state.successCount}, failure: ${state.failureCount})`);
|
||||
}
|
||||
|
||||
// 调用资源
|
||||
try {
|
||||
const result = await config.resourceFn(...args);
|
||||
this.onResourceSuccess(state);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.onResourceFailure(state);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源类型是否可用
|
||||
*/
|
||||
isResourceAvailable (type: string): boolean {
|
||||
const state = this.resourceTypes.get(type);
|
||||
if (!state) {
|
||||
return true; // 未注册的资源类型视为可用
|
||||
}
|
||||
|
||||
if (state.isPermanentlyDisabled || !state.isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() >= state.disableUntil;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源类型统计信息
|
||||
*/
|
||||
getResourceStats (type: string): { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean } | null {
|
||||
const state = this.resourceTypes.get(type);
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
successCount: state.successCount,
|
||||
failureCount: state.failureCount,
|
||||
isEnabled: state.isEnabled,
|
||||
isPermanentlyDisabled: state.isPermanentlyDisabled,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有资源类型统计
|
||||
*/
|
||||
getAllResourceStats (): Map<string, { successCount: number; failureCount: number; isEnabled: boolean; isPermanentlyDisabled: boolean }> {
|
||||
const stats = new Map();
|
||||
for (const [type, state] of this.resourceTypes) {
|
||||
stats.set(type, {
|
||||
successCount: state.successCount,
|
||||
failureCount: state.failureCount,
|
||||
isEnabled: state.isEnabled,
|
||||
isPermanentlyDisabled: state.isPermanentlyDisabled,
|
||||
});
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销资源类型
|
||||
*/
|
||||
unregister (type: string): boolean {
|
||||
return this.resourceTypes.delete(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁管理器
|
||||
*/
|
||||
destroy (): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resourceTypes.clear();
|
||||
this.destroyed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并执行健康检查(如果需要)
|
||||
*/
|
||||
private async checkAndPerformHealthCheck (state: ResourceTypeState): Promise<void> {
|
||||
// 如果资源可用或已永久禁用,无需健康检查
|
||||
if (state.isEnabled && Date.now() >= state.disableUntil) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.isPermanentlyDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 检查是否还在禁用期内
|
||||
if (now < state.disableUntil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否需要进行健康检查(根据间隔时间)
|
||||
if (now - state.lastHealthCheckTime < state.config.healthCheckInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行健康检查
|
||||
await this.performHealthCheck(state);
|
||||
}
|
||||
|
||||
private async performHealthCheck (state: ResourceTypeState): Promise<void> {
|
||||
state.lastHealthCheckTime = Date.now();
|
||||
|
||||
try {
|
||||
let healthCheckResult: boolean;
|
||||
|
||||
if (state.config.healthCheckFn) {
|
||||
const testArgs = state.config.testArgs || [];
|
||||
healthCheckResult = await state.config.healthCheckFn(...testArgs);
|
||||
} else {
|
||||
const testArgs = state.config.testArgs || [];
|
||||
await state.config.resourceFn(...testArgs);
|
||||
healthCheckResult = true;
|
||||
}
|
||||
|
||||
if (healthCheckResult) {
|
||||
// 健康检查成功,重新启用
|
||||
state.isEnabled = true;
|
||||
state.disableUntil = 0;
|
||||
state.currentRetries = 0;
|
||||
state.healthCheckFailureCount = 0;
|
||||
} else {
|
||||
throw new Error('Health check function returned false');
|
||||
}
|
||||
} catch {
|
||||
// 健康检查失败,增加失败计数
|
||||
state.healthCheckFailureCount++;
|
||||
|
||||
// 检查是否达到最大健康检查失败次数
|
||||
if (state.healthCheckFailureCount >= state.config.maxHealthCheckFailures) {
|
||||
// 永久禁用资源
|
||||
state.isPermanentlyDisabled = true;
|
||||
state.disableUntil = 0;
|
||||
} else {
|
||||
// 继续禁用一段时间
|
||||
state.disableUntil = Date.now() + state.config.disableTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onResourceSuccess (state: ResourceTypeState): void {
|
||||
state.currentRetries = 0;
|
||||
state.disableUntil = 0;
|
||||
state.healthCheckFailureCount = 0;
|
||||
state.successCount++;
|
||||
}
|
||||
|
||||
private onResourceFailure (state: ResourceTypeState): void {
|
||||
state.currentRetries++;
|
||||
state.failureCount++;
|
||||
|
||||
// 如果重试次数达到上限,禁用资源
|
||||
if (state.currentRetries >= state.config.maxRetries) {
|
||||
state.disableUntil = Date.now() + state.config.disableTime;
|
||||
state.currentRetries = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局实例
|
||||
export const resourceManager = new ResourceManager();
|
||||
|
||||
// 便捷函数
|
||||
export async function registerResource<T extends any[], R> (
|
||||
type: string,
|
||||
config: ResourceConfig<T, R>,
|
||||
...args: T
|
||||
): Promise<R> {
|
||||
return resourceManager.callResource(type, config, ...args);
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { QQVersionConfigType, QQLevel } from './types';
|
||||
import { compareSemVer } from './version';
|
||||
import { getAllGitHubTags as getAllTagsFromMirror } from './mirror';
|
||||
|
||||
// 导出 compareSemVer 供其他模块使用
|
||||
export { compareSemVer } from './version';
|
||||
|
||||
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 {
|
||||
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 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>((_resolve, 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 {
|
||||
if (process.env['NAPCAT_QQ_PACKAGE_INFO_PATH']) {
|
||||
return process.env['NAPCAT_QQ_PACKAGE_INFO_PATH'];
|
||||
}
|
||||
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 {
|
||||
if (process.env['NAPCAT_QQ_VERSION_CONFIG_PATH']) {
|
||||
return process.env['NAPCAT_QQ_VERSION_CONFIG_PATH'];
|
||||
}
|
||||
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 { penguinNum, crownNum, sunNum, moonNum, starNum } = level;
|
||||
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 parseAppidFromMajorV2 (nodeMajor: string): string | undefined {
|
||||
const marker = Buffer.from('QQAppId/', 'utf-8');
|
||||
const filePath = path.resolve(nodeMajor);
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
|
||||
let searchPosition = 0;
|
||||
while (true) {
|
||||
const index = fileContent.indexOf(marker, searchPosition);
|
||||
if (index === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const start = index + marker.length;
|
||||
const end = fileContent.indexOf(0x00, start);
|
||||
if (end === -1) {
|
||||
break;
|
||||
}
|
||||
const content = fileContent.subarray(start, end);
|
||||
const str = content.toString('utf-8');
|
||||
if (/^\d+$/.test(str)) {
|
||||
return str;
|
||||
}
|
||||
|
||||
searchPosition = end + 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
searchPosition = end + 1;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ============== GitHub Tags 获取 ==============
|
||||
// 使用 mirror 模块统一管理镜像
|
||||
|
||||
export async function getAllTags (mirror?: string): Promise<{ tags: string[], mirror: string; }> {
|
||||
return getAllTagsFromMirror('NapNeko', 'NapCatQQ', mirror);
|
||||
}
|
||||
|
||||
|
||||
export async function getLatestTag (mirror?: string): Promise<string> {
|
||||
const { tags } = await getAllTags(mirror);
|
||||
|
||||
// 使用 SemVer 规范排序
|
||||
tags.sort((a, b) => compareSemVer(a, b));
|
||||
|
||||
const latest = tags.at(-1);
|
||||
if (!latest) {
|
||||
throw new Error('No tags found');
|
||||
}
|
||||
// 去掉开头的 v
|
||||
return latest.replace(/^v/, '');
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
export interface ILogWrapper {
|
||||
fileLogEnabled: boolean;
|
||||
consoleLogEnabled: boolean;
|
||||
cleanOldLogs (logDir: string): void;
|
||||
setFileAndConsoleLogLevel (fileLogLevel: LogLevel, consoleLogLevel: LogLevel): void;
|
||||
setLogSelfInfo (selfInfo: { nick: string; uid: string; }): void;
|
||||
setFileLogEnabled (isEnabled: boolean): void;
|
||||
setConsoleLogEnabled (isEnabled: boolean): void;
|
||||
formatMsg (msg: any[]): string;
|
||||
_log (level: LogLevel, ...args: any[]): void;
|
||||
log (...args: any[]): void;
|
||||
logDebug (...args: any[]): void;
|
||||
logError (...args: any[]): void;
|
||||
logWarn (...args: any[]): void;
|
||||
logFatal (...args: any[]): void;
|
||||
logMessage (msg: unknown, selfInfo: unknown): void;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import crypto from 'crypto';
|
||||
import { Peer } from './types';
|
||||
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];
|
||||
if (key !== undefined) {
|
||||
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 = 5000) {
|
||||
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();
|
||||
if (hash[0]) {
|
||||
// 设置第一个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 ?? '0'),
|
||||
peerUid: peerUid ?? '',
|
||||
guildId: '',
|
||||
};
|
||||
return { MsgId: msgId ?? '0', 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);
|
||||
}
|
||||
|
||||
isShortId (message_id: string): boolean {
|
||||
const num = Number(message_id);
|
||||
// 判断是否是整数并且在 INT32 的范围内
|
||||
return Number.isInteger(num) && num >= -2147483648 && num <= 2147483647;
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user