Compare commits
1140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7216755430 | ||
|
|
0c91f9c66b | ||
|
|
e8855a59b0 | ||
|
|
5de2664af4 | ||
|
|
5284e0ac5a | ||
|
|
67d6cd3f2e | ||
|
|
0ba5862753 | ||
|
|
d4478275ee | ||
|
|
163bb88751 | ||
|
|
ec6762d916 | ||
|
|
ed1872a349 | ||
|
|
a7fd70ac3a | ||
|
|
7e38f1d227 | ||
|
|
0ca68010a5 | ||
|
|
822f683a14 | ||
|
|
f4d3d33954 | ||
|
|
d1abf788a5 | ||
|
|
9ba6b2ed40 | ||
|
|
3a880e389b | ||
|
|
1c7ac42a46 | ||
|
|
3e8b575015 | ||
|
|
7c22170e1e | ||
|
|
f143da6ba8 | ||
|
|
d0d3934869 | ||
|
|
808165b008 | ||
|
|
d23785f34d | ||
|
|
31daf41135 | ||
|
|
a2450b72be | ||
|
|
fbccf8be24 | ||
|
|
37ae17b53f | ||
|
|
35566970fd | ||
|
|
e70cd1eff7 | ||
|
|
fbd3241845 | ||
|
|
cf69ccdbc9 | ||
|
|
f3de4d48d3 | ||
|
|
17d5110069 | ||
|
|
c5de5e00fc | ||
|
|
ea7cd7f7e1 | ||
|
|
cc23599776 | ||
|
|
c6ec2126e0 | ||
|
|
f1756c4d1c | ||
|
|
4940d72867 | ||
|
|
91e0839ed5 | ||
|
|
334c4233e6 | ||
|
|
71bb4f68f3 | ||
|
|
47983e2915 | ||
|
|
ae42eed6e2 | ||
|
|
cb061890d3 | ||
|
|
31feec26b5 | ||
|
|
e93cd3529f | ||
|
|
1ad700b935 | ||
|
|
68c8b984ad | ||
|
|
8eb1aa2fb4 | ||
|
|
2d3f4e696b | ||
|
|
b241881c74 | ||
|
|
aecf33f4dc | ||
|
|
dd4374389b | ||
|
|
100efb03ab | ||
|
|
ce9482f19d | ||
|
|
4e37b002f9 | ||
|
|
7e7262415b | ||
|
|
3365211507 | ||
|
|
05b38825c0 | ||
|
|
95f4a4d37e | ||
|
|
cd495fc7a0 | ||
|
|
656279d74b | ||
|
|
377c780d1a | ||
|
|
aefa8985b1 | ||
|
|
b034940dfd | ||
|
|
cb8e10cc7e | ||
|
|
afed164ba1 | ||
|
|
a34a86288b | ||
|
|
50bcd71144 | ||
|
|
fa3a229827 | ||
|
|
e56b912bbd | ||
|
|
da0dd01460 | ||
|
|
578dda2f17 | ||
|
|
649165bf00 | ||
|
|
c4f7107038 | ||
|
|
7f81bf45ee | ||
|
|
7e6035d98b | ||
|
|
2405cb03d8 | ||
|
|
32d3ff6998 | ||
|
|
84f0e0f9a0 | ||
|
|
8697061a90 | ||
|
|
872a3e0100 | ||
|
|
4fcbdc4d89 | ||
|
|
176af14915 | ||
|
|
81cf1fd98e | ||
|
|
5189099146 | ||
|
|
7fc17d45ba | ||
|
|
c54f74609e | ||
|
|
a2d7ac4878 | ||
|
|
fd0afa3b25 | ||
|
|
7685cc3dfc | ||
|
|
f9c0b9d106 | ||
|
|
d31f0a45b4 | ||
|
|
7c701781a1 | ||
|
|
3c612e03ff | ||
|
|
f27db01145 | ||
|
|
ae97cfba03 | ||
|
|
162ddc1bf5 | ||
|
|
afb6ef421a | ||
|
|
173a165c4b | ||
|
|
d525f9b03d | ||
|
|
f2ba789cc0 | ||
|
|
2cdc9bdc09 | ||
|
|
c123b34d5f | ||
|
|
d25b43ebf2 | ||
|
|
8fe4a9e6ac | ||
|
|
09da80aad5 | ||
|
|
3d3f718fd5 | ||
|
|
6068abdec0 | ||
|
|
3957d7af5a | ||
|
|
a2837974fe | ||
|
|
6f8edfe570 | ||
|
|
0b655db4dd | ||
|
|
d800466a30 | ||
|
|
fa80441e36 | ||
|
|
1990761ad6 | ||
|
|
ef63812391 | ||
|
|
0f033b0ac8 | ||
|
|
9fdef3cde9 | ||
|
|
20e8643193 | ||
|
|
8645ed4d9d | ||
|
|
c0b9817ff5 | ||
|
|
b147e57c1c | ||
|
|
ad4a108781 | ||
|
|
df824d77ae | ||
|
|
19888d52dc | ||
|
|
4dc8b3ed3b | ||
|
|
8df54d5cd3 | ||
|
|
aa982b3071 | ||
|
|
8e71dec63a | ||
|
|
31bb1e5dee | ||
|
|
75e1e8dd79 | ||
|
|
d32ccc6eb5 | ||
|
|
7b3e94d568 | ||
|
|
5cfe479044 | ||
|
|
f04ffa5dc6 | ||
|
|
a2a73ce2dd | ||
|
|
66d02eeb6a | ||
|
|
b99c0ca437 | ||
|
|
019b90984d | ||
|
|
5043a49779 | ||
|
|
36aa08a8f5 | ||
|
|
8bc8df32f9 | ||
|
|
bc183ae002 | ||
|
|
b85f9197e3 | ||
|
|
c8fd66fa9b | ||
|
|
6e9f448a0c | ||
|
|
142016778f | ||
|
|
159fb8cd3a | ||
|
|
01c911e178 | ||
|
|
8b3ea8dcef | ||
|
|
fe8b270ab3 | ||
|
|
f02ae5894f | ||
|
|
a6a0b408af | ||
|
|
e9856ac80f | ||
|
|
f553f9dc8d | ||
|
|
5608638e9a | ||
|
|
a53c20767a | ||
|
|
a92bef5b33 | ||
|
|
a9a3b6ec6e | ||
|
|
20d41fff9e | ||
|
|
0b4d7e1346 | ||
|
|
46b9049a24 | ||
|
|
521f4dc365 | ||
|
|
04b507d749 | ||
|
|
5638127813 | ||
|
|
30a7797ba9 | ||
|
|
d09a82b1b8 | ||
|
|
85b5c881ba | ||
|
|
eebce222cf | ||
|
|
ec5ca5d89a | ||
|
|
31a7767ae4 | ||
|
|
fec024334a | ||
|
|
2ad2af4d7c | ||
|
|
2a160d296f | ||
|
|
e43f229e04 | ||
|
|
9158ebc136 | ||
|
|
d758fe3a2b | ||
|
|
4abd0668a3 | ||
|
|
0181700c3b | ||
|
|
6083e9cfcc | ||
|
|
5fec190c70 | ||
|
|
e1743ae5e4 | ||
|
|
ded921c55e | ||
|
|
57e717e898 | ||
|
|
55f21c6caa | ||
|
|
4360775eff | ||
|
|
e2486606f9 | ||
|
|
f8eb368cdb | ||
|
|
9ce51fb082 | ||
|
|
89e50be1e9 | ||
|
|
1259dcea5b | ||
|
|
b4900066b3 | ||
|
|
28acd94cbd | ||
|
|
7aedacb27f | ||
|
|
c9b45ec1a2 | ||
|
|
54cacc30e4 | ||
|
|
9b26fc99d3 | ||
|
|
204846b404 | ||
|
|
3d654791b9 | ||
|
|
63f42f1592 | ||
|
|
0ab0b939da | ||
|
|
522a123f9a | ||
|
|
ec9f8d6e12 | ||
|
|
3c750c75a9 | ||
|
|
5b2b1f499b | ||
|
|
531ffcd55d | ||
|
|
068e4d8bb5 | ||
|
|
5dc33e78ad | ||
|
|
d76a2170a0 | ||
|
|
ec2af3120c | ||
|
|
8de49a3109 | ||
|
|
dbb5a0022e | ||
|
|
cb8c8d6b57 | ||
|
|
93c140ed4e | ||
|
|
457b072f0e | ||
|
|
1869493473 | ||
|
|
f33c66ce15 | ||
|
|
e8d6f86458 | ||
|
|
a000ffdf0d | ||
|
|
202338a160 | ||
|
|
e3eb129a52 | ||
|
|
7654e9f2bb | ||
|
|
a60c03f42f | ||
|
|
60aae228a1 | ||
|
|
b1417f9b56 | ||
|
|
eeeaddbb60 | ||
|
|
b1109022bb | ||
|
|
1807789511 | ||
|
|
49a5b631c2 | ||
|
|
e7aaec81e2 | ||
|
|
05f8e8f3c3 | ||
|
|
e760876470 | ||
|
|
e0ec4d4ebb | ||
|
|
79fa0ade0d | ||
|
|
652b5d6118 | ||
|
|
f4dedf4803 | ||
|
|
06f6a542f5 | ||
|
|
d5b8f886d6 | ||
|
|
97b6dccc30 | ||
|
|
a755487e22 | ||
|
|
5407392f08 | ||
|
|
3359c5ded9 | ||
|
|
caaf8be3b2 | ||
|
|
28ce5d3cb4 | ||
|
|
3dd56c711e | ||
|
|
511fb82ce0 | ||
|
|
1e5524a009 | ||
|
|
d5e6afc7b9 | ||
|
|
91e633b0fb | ||
|
|
397b9880b9 | ||
|
|
54eb26ba67 | ||
|
|
355f7fb4a0 | ||
|
|
325c455e38 | ||
|
|
a7a9792efe | ||
|
|
4bab93e545 | ||
|
|
3c60997b1c | ||
|
|
bf684d9166 | ||
|
|
5c4ee30f37 | ||
|
|
346374b442 | ||
|
|
cc8a387bde | ||
|
|
962685ade6 | ||
|
|
36fdaac406 | ||
|
|
db16db911b | ||
|
|
bb84dfcc27 | ||
|
|
3e71e541e6 | ||
|
|
bcc856d583 | ||
|
|
1eaf480a7d | ||
|
|
86123af7fc | ||
|
|
c3cd2aaf89 | ||
|
|
599afdc7ba | ||
|
|
ffe54af8d9 | ||
|
|
a7b30ef844 | ||
|
|
50b8cb14dc | ||
|
|
0b18f868bc | ||
|
|
c9acb9ae28 | ||
|
|
2e1506a05d | ||
|
|
b35283f970 | ||
|
|
54ac072bfb | ||
|
|
58cefb9cdc | ||
|
|
be4344634d | ||
|
|
2da5d242f7 | ||
|
|
fbd00b2576 | ||
|
|
72548c9575 | ||
|
|
003f3e946d | ||
|
|
dc51d01351 | ||
|
|
c5db525f4a | ||
|
|
c1377e6de7 | ||
|
|
d2c4f425c7 | ||
|
|
803b1a6c77 | ||
|
|
9a35ee9cd1 | ||
|
|
458d22223c | ||
|
|
4ed61136b2 | ||
|
|
1445a29e15 | ||
|
|
55ef040852 | ||
|
|
0c88319248 | ||
|
|
6778bd69de | ||
|
|
f9c9b3a852 | ||
|
|
9ce9e46c57 | ||
|
|
656bde25c8 | ||
|
|
791a360f28 | ||
|
|
376245b749 | ||
|
|
d3e3527c2b | ||
|
|
5b78dfbd5a | ||
|
|
1e461aae3c | ||
|
|
42abc2b9cb | ||
|
|
4f8c320658 | ||
|
|
cbacc89907 | ||
|
|
8e6da5e2d0 | ||
|
|
02980c4d1a | ||
|
|
0129188739 | ||
|
|
98ef642cd1 | ||
|
|
32e886e53b | ||
|
|
315d847f06 | ||
|
|
381d320967 | ||
|
|
2f5b62decb | ||
|
|
2afdb2a0da | ||
|
|
5bfbf92c21 | ||
|
|
a775a0dde9 | ||
|
|
d7f00c0594 | ||
|
|
77c8f874b6 | ||
|
|
fb0a20919b | ||
|
|
0300ba4648 | ||
|
|
d472eee777 | ||
|
|
41bd06e50a | ||
|
|
97334dfbf5 | ||
|
|
e3d8c8e940 | ||
|
|
f2c62db76e | ||
|
|
b1b051c4ce | ||
|
|
a754b2ecc7 | ||
|
|
e0eb625b75 | ||
|
|
937be7678e | ||
|
|
9b88946209 | ||
|
|
74de3d9100 | ||
|
|
42d50014a1 | ||
|
|
a36ae315b0 | ||
|
|
2161ec5fa7 | ||
|
|
32bba007cd | ||
|
|
84d3dc9f40 | ||
|
|
890d032794 | ||
|
|
66f30e1ebf | ||
|
|
ada614d007 | ||
|
|
ea3ab7f13f | ||
|
|
a5e4c24de3 | ||
|
|
bcc7d25b64 | ||
|
|
aae676fdc7 | ||
|
|
0e9aa43476 | ||
|
|
b2ff556aa6 | ||
|
|
69c5b78678 | ||
|
|
8be7f74e9f | ||
|
|
a05150ebe1 | ||
|
|
5e6b607ded | ||
|
|
df2dabfe76 | ||
|
|
5e032fcc6a | ||
|
|
44200a2208 | ||
|
|
e39bb05f01 | ||
|
|
677731dd70 | ||
|
|
fa8e6f2c59 | ||
|
|
509b23ff04 | ||
|
|
cf1765f5a4 | ||
|
|
c541c7e257 | ||
|
|
298b8b71c8 | ||
|
|
5c120a8231 | ||
|
|
88ee8f89fe | ||
|
|
12b8130372 | ||
|
|
58332dad24 | ||
|
|
e97f3e1283 | ||
|
|
e406dca7ae | ||
|
|
e4c1807f76 | ||
|
|
f4412bb086 | ||
|
|
27af8e52ac | ||
|
|
4c9a220300 | ||
|
|
1fe822cd20 | ||
|
|
0ab8d025bf | ||
|
|
a0f3d66607 | ||
|
|
06e7c3363a | ||
|
|
4d200de6b7 | ||
|
|
6200097f7c | ||
|
|
c7af0384fb | ||
|
|
dc87615bd6 | ||
|
|
ff2cfcee97 | ||
|
|
f3c07ed8fc | ||
|
|
7ab44dcb34 | ||
|
|
aa6699d06e | ||
|
|
3cb51a17a6 | ||
|
|
994e8ced3e | ||
|
|
75d26465f1 | ||
|
|
f5052935bd | ||
|
|
84b89de2a6 | ||
|
|
c4f9c4f630 | ||
|
|
c213cd6c3a | ||
|
|
9d22d6e3a0 | ||
|
|
a38419e3cb | ||
|
|
a64779684e | ||
|
|
ecd7012eee | ||
|
|
74a1011fcc | ||
|
|
d4b0a4acca | ||
|
|
ac6e593315 | ||
|
|
b1e77b1658 | ||
|
|
722c3554e9 | ||
|
|
1d08966571 | ||
|
|
fb50ae7544 | ||
|
|
ea695fc9e9 | ||
|
|
5c6d1e6a14 | ||
|
|
b030c40853 | ||
|
|
d6782c35e2 | ||
|
|
120e6db119 | ||
|
|
fa10f8ce19 | ||
|
|
31494b4687 | ||
|
|
857ed0f343 | ||
|
|
8133ff08a7 | ||
|
|
2d315c4d8e | ||
|
|
505f7b6ac9 | ||
|
|
2735eb14bd | ||
|
|
7afbc95eda | ||
|
|
91bb83d8c1 | ||
|
|
55550790e4 | ||
|
|
40221926a9 | ||
|
|
d069374a95 | ||
|
|
1183fe2057 | ||
|
|
f4605d4f74 | ||
|
|
3ce3fb685b | ||
|
|
07c2f7371f | ||
|
|
114aae98a9 | ||
|
|
0bd6548f45 | ||
|
|
06c5b7807b | ||
|
|
e8ef08cae2 | ||
|
|
0d251a9343 | ||
|
|
69a19b0e32 | ||
|
|
1b0b5f3494 | ||
|
|
5abdc8c538 | ||
|
|
9f0ba6d385 | ||
|
|
7863a0f45f | ||
|
|
ef4c2a935c | ||
|
|
40d2e948e4 | ||
|
|
3de4e905d3 | ||
|
|
655f4e199c | ||
|
|
13c3d3a2fb | ||
|
|
db2dca45f6 | ||
|
|
7330a05c78 | ||
|
|
a39c932868 | ||
|
|
a2c24c9197 | ||
|
|
5c3efc681f | ||
|
|
e70d2bd708 | ||
|
|
cf75a961fb | ||
|
|
159f317071 | ||
|
|
713eef592a | ||
|
|
cf03ad8fd9 | ||
|
|
0c0b27901a | ||
|
|
137fe3c8f2 | ||
|
|
d96174076a | ||
|
|
6d5662d96e | ||
|
|
57abd47d99 | ||
|
|
5092b3d791 | ||
|
|
649409d1be | ||
|
|
8f549d896a | ||
|
|
a1359ddbb5 | ||
|
|
304a0dda3e | ||
|
|
fff9c4a4d8 | ||
|
|
2c76102fc4 | ||
|
|
f576cd9417 | ||
|
|
9cfd224b74 | ||
|
|
c12f8de8b4 | ||
|
|
ed9a7c52e2 | ||
|
|
38fcaaa28b | ||
|
|
5317a1c1a9 | ||
|
|
4bc5933ea2 | ||
|
|
6a6bd33fe5 | ||
|
|
8256942a3d | ||
|
|
697632eee8 | ||
|
|
6bbf5b254d | ||
|
|
5831898c4a | ||
|
|
2cc413bec1 | ||
|
|
0af36e89d9 | ||
|
|
b2c0f5d2e5 | ||
|
|
80b74c7da9 | ||
|
|
f14f13b158 | ||
|
|
9dda00b6fa | ||
|
|
a29debb738 | ||
|
|
b990fc43df | ||
|
|
915e9552ee | ||
|
|
c522e0a386 | ||
|
|
c9cc08a9ba | ||
|
|
66e1b1662f | ||
|
|
9372e83bd8 | ||
|
|
b38a240dbb | ||
|
|
76b9506395 | ||
|
|
f1cf636aa2 | ||
|
|
312dcd0e13 | ||
|
|
42c2419613 | ||
|
|
8f7f748e82 | ||
|
|
7ad3bad1be | ||
|
|
5cd682e69f | ||
|
|
5d57780e84 | ||
|
|
f399955204 | ||
|
|
770652fe6b | ||
|
|
9ed5fa8c67 | ||
|
|
5a4ad29727 | ||
|
|
1eda3f2e33 | ||
|
|
95cb95ef96 | ||
|
|
4e7c96634c | ||
|
|
58587b8aea | ||
|
|
3fbf6239db | ||
|
|
faec53d497 | ||
|
|
482dcc534e | ||
|
|
854f61dda6 | ||
|
|
fca38713a1 | ||
|
|
5dd3bade53 | ||
|
|
665360f48d | ||
|
|
65719cb56a | ||
|
|
bdb76d4639 | ||
|
|
15634412ef | ||
|
|
bbcf9649fa | ||
|
|
e845d7314e | ||
|
|
6927b1c94f | ||
|
|
a09c6acd0d | ||
|
|
0963650ccb | ||
|
|
380688b353 | ||
|
|
ad5466bff8 | ||
|
|
a83652bf3f | ||
|
|
c632de314d | ||
|
|
259c9610d5 | ||
|
|
e9936c5524 | ||
|
|
3f60440e72 | ||
|
|
71a15f92fb | ||
|
|
32bc0dd820 | ||
|
|
20d1ac9d01 | ||
|
|
18baf89e0e | ||
|
|
3a1d1f2e59 | ||
|
|
e9a048721d | ||
|
|
68f0c7ff1a | ||
|
|
2875fe94ea | ||
|
|
1870427c0f | ||
|
|
636568fd30 | ||
|
|
bbc2391bf8 | ||
|
|
401684542a | ||
|
|
870edb2513 | ||
|
|
7ad09169ea | ||
|
|
c1a0f8915b | ||
|
|
dcdab8e5a1 | ||
|
|
eb3278fdab | ||
|
|
34db3af48d | ||
|
|
198da960dd | ||
|
|
cb83918fb3 | ||
|
|
f59a48540b | ||
|
|
ccf9c1a5fb | ||
|
|
ba6a85142a | ||
|
|
440baccd2a | ||
|
|
690c073328 | ||
|
|
3f0730ed4f | ||
|
|
01d5663bc8 | ||
|
|
49806cd00e | ||
|
|
935b0848e5 | ||
|
|
5ca20a89a2 | ||
|
|
e89a2266ec | ||
|
|
6607533311 | ||
|
|
4057054220 | ||
|
|
055e43845e | ||
|
|
d67270f2f8 | ||
|
|
d061b6c190 | ||
|
|
945f87d77f | ||
|
|
6c9be52d39 | ||
|
|
98e347f010 | ||
|
|
607e367bb1 | ||
|
|
7a25dc1ef1 | ||
|
|
e22ec4be09 | ||
|
|
51a06622f9 | ||
|
|
22faf5b831 | ||
|
|
e781c662b2 | ||
|
|
5744698d24 | ||
|
|
2c2ab3cd48 | ||
|
|
cfae4f5acd | ||
|
|
de541e3249 | ||
|
|
f5187c5c01 | ||
|
|
9936279443 | ||
|
|
2818773fd4 | ||
|
|
b9293cbcd0 | ||
|
|
5b9e44ddfc | ||
|
|
1791accab7 | ||
|
|
08081360f3 | ||
|
|
e933a95e97 | ||
|
|
4ef457fe6f | ||
|
|
bd9cae8921 | ||
|
|
303a74f8fd | ||
|
|
0b7f126ce1 | ||
|
|
308b5c027f | ||
|
|
ed3abc4b43 | ||
|
|
87ecb3b380 | ||
|
|
7e31763a25 | ||
|
|
c9df57d16a | ||
|
|
3d0f8ee657 | ||
|
|
6421bb4f5c | ||
|
|
3919743885 | ||
|
|
a5a57b9e20 | ||
|
|
e31d2810ad | ||
|
|
140e62fdcd | ||
|
|
014b4deb87 | ||
|
|
956b6cd172 | ||
|
|
bbaca3f044 | ||
|
|
bb8a44b918 | ||
|
|
b5574d5999 | ||
|
|
06dde072da | ||
|
|
8e92a81bb9 | ||
|
|
2c7345ae88 | ||
|
|
33d4696155 | ||
|
|
7d2dcc10e5 | ||
|
|
e82687454c | ||
|
|
84382caebc | ||
|
|
662530e507 | ||
|
|
edf81d0a2e | ||
|
|
7cbae86941 | ||
|
|
8ff7420a5e | ||
|
|
7ae59b1419 | ||
|
|
41036f8ee8 | ||
|
|
380777ca04 | ||
|
|
c658cd1096 | ||
|
|
c7b9946d2f | ||
|
|
0caca473d6 | ||
|
|
3e5d35957d | ||
|
|
6b8b14aba2 | ||
|
|
5db7a90a24 | ||
|
|
88b86611a3 | ||
|
|
886fe2052e | ||
|
|
e4dd194d4a | ||
|
|
a47af60f58 | ||
|
|
35f24eb806 | ||
|
|
36e3119d34 | ||
|
|
8ff3ad824e | ||
|
|
556000c002 | ||
|
|
fda050d3fe | ||
|
|
b1047309c9 | ||
|
|
d766c4945e | ||
|
|
43c98c45b9 | ||
|
|
f7556b5af3 | ||
|
|
cd781c4cf6 | ||
|
|
cd8698b157 | ||
|
|
d921dcddf1 | ||
|
|
9f318ddaef | ||
|
|
5c35ea11c3 | ||
|
|
3b16effff0 | ||
|
|
d3a27ad701 | ||
|
|
2a4589e268 | ||
|
|
80a34c82b9 | ||
|
|
fca7a65ee0 | ||
|
|
30a75bc581 | ||
|
|
7b365367f7 | ||
|
|
3ed5f543e2 | ||
|
|
ceea50b116 | ||
|
|
a5455e27d1 | ||
|
|
6f83d01321 | ||
|
|
c453b82e9f | ||
|
|
b7da316447 | ||
|
|
fb20b2e16c | ||
|
|
9df7c341a9 | ||
|
|
7c113d6e04 | ||
|
|
a6f22167ff | ||
|
|
d49e69735a | ||
|
|
eca73eae18 | ||
|
|
d3a34dfdf9 | ||
|
|
623188d884 | ||
|
|
f093f52792 | ||
|
|
d53607a118 | ||
|
|
5f637e064a | ||
|
|
e4b21e94f5 | ||
|
|
fc37288827 | ||
|
|
dad7245a3a | ||
|
|
4190831081 | ||
|
|
c509a01d7d | ||
|
|
6d259593fd | ||
|
|
bd3e06520f | ||
|
|
41dccd98a9 | ||
|
|
54e6d5c3f2 | ||
|
|
3f6249f39c | ||
|
|
a888714629 | ||
|
|
17ef3231df | ||
|
|
cc30b51d58 | ||
|
|
9d40eacc15 | ||
|
|
a0415c5f4e | ||
|
|
941978b578 | ||
|
|
06538b9122 | ||
|
|
dd895d7c17 | ||
|
|
9257a6cfde | ||
|
|
f8260067ab | ||
|
|
40b06daf1e | ||
|
|
e30915a06b | ||
|
|
2ab3898d28 | ||
|
|
6e38e748b8 | ||
|
|
7ecdd63bef | ||
|
|
8056962203 | ||
|
|
7a42f8c26f | ||
|
|
6c510e42e8 | ||
|
|
45d6ebf084 | ||
|
|
2147c4ffee | ||
|
|
d4ab191f34 | ||
|
|
a5e53a713b | ||
|
|
e3f965a9d6 | ||
|
|
dd00d4c8a5 | ||
|
|
4d292a75fa | ||
|
|
50b6733f57 | ||
|
|
19479b4b3c | ||
|
|
540f58d8ec | ||
|
|
749f1dfcf9 | ||
|
|
176691bb96 | ||
|
|
b9ec8ac9b1 | ||
|
|
28d973b9cb | ||
|
|
a6be54937c | ||
|
|
0664b9af84 | ||
|
|
407d8d1fd2 | ||
|
|
108897f6ad | ||
|
|
3d2decb0ec | ||
|
|
386b884f1b | ||
|
|
ace4da2297 | ||
|
|
a8fb48fb50 | ||
|
|
61f065c0c6 | ||
|
|
d6cf6d120a | ||
|
|
c20c19d8e0 | ||
|
|
bd8bbf76ab | ||
|
|
faccff1834 | ||
|
|
99d3c5a117 | ||
|
|
31eb09edef | ||
|
|
4180c2d754 | ||
|
|
68f5deedff | ||
|
|
9f72196414 | ||
|
|
b32efa9131 | ||
|
|
3cf502fea3 | ||
|
|
863a953ae1 | ||
|
|
a44104d8f7 | ||
|
|
f602bbb0cf | ||
|
|
2807ff5927 | ||
|
|
4fb8e6a4da | ||
|
|
7ec61f089d | ||
|
|
f7a500a8cf | ||
|
|
2b319bd694 | ||
|
|
24cf7c01f8 | ||
|
|
aa50e73909 | ||
|
|
d9b33b5439 | ||
|
|
da58c6bec0 | ||
|
|
c4cbac4331 | ||
|
|
ac26a99143 | ||
|
|
640252d391 | ||
|
|
258a1dda5e | ||
|
|
53a7ce2e46 | ||
|
|
291e2fd8fd | ||
|
|
f180c7698f | ||
|
|
183d6f3011 | ||
|
|
ba71d7ad03 | ||
|
|
26cfaac3bd | ||
|
|
556c8b24c0 | ||
|
|
31f0f527b7 | ||
|
|
b2e0cab702 | ||
|
|
3e3609e0f2 | ||
|
|
83e73d9842 | ||
|
|
673a175ddf | ||
|
|
12eacd3530 | ||
|
|
030f0551fd | ||
|
|
47f5947410 | ||
|
|
aaefa2e83c | ||
|
|
f8e92f7c8d | ||
|
|
b6430e6eb6 | ||
|
|
e60605c7bb | ||
|
|
58d2bd3c81 | ||
|
|
6534d05b76 | ||
|
|
2d7de174c5 | ||
|
|
79aa1dc67f | ||
|
|
7792ad9ea0 | ||
|
|
be6671923b | ||
|
|
0fa1b3f044 | ||
|
|
4ab751696b | ||
|
|
dce4eedf7d | ||
|
|
129b67b751 | ||
|
|
9ab776d53a | ||
|
|
2759a34d96 | ||
|
|
2f9f42750e | ||
|
|
30abd1f904 | ||
|
|
008075466e | ||
|
|
5b4035c320 | ||
|
|
e3feb6a73c | ||
|
|
40fe73317d | ||
|
|
073745030c | ||
|
|
c523437506 | ||
|
|
9eef570d37 | ||
|
|
be37b8cbbd | ||
|
|
c635496677 | ||
|
|
8753ecfd92 | ||
|
|
5eda1f2870 | ||
|
|
d5a60074f7 | ||
|
|
91df57d932 | ||
|
|
e27d4c4302 | ||
|
|
55847f6e10 | ||
|
|
b39d8bae27 | ||
|
|
b0cf23f775 | ||
|
|
c641246056 | ||
|
|
1e5bc9bbea | ||
|
|
99b504b5f6 | ||
|
|
1146454fec | ||
|
|
805e014a75 | ||
|
|
d3acd1efc1 | ||
|
|
9fcd218a5a | ||
|
|
d6a0830cfe | ||
|
|
40a63b9c66 | ||
|
|
eeb19a04cc | ||
|
|
91e457eb03 | ||
|
|
78d1919d7f | ||
|
|
8393acf173 | ||
|
|
bca152a047 | ||
|
|
6a15908a93 | ||
|
|
c626bbab74 | ||
|
|
c5c7dcc6f2 | ||
|
|
03dafe727e | ||
|
|
744921c45e | ||
|
|
abc4a4dcba | ||
|
|
7e0da2f929 | ||
|
|
a3b70d0f1f | ||
|
|
d291724f06 | ||
|
|
122a9ca2cc | ||
|
|
48aaddd32b | ||
|
|
47401af856 | ||
|
|
709adfd812 | ||
|
|
038d0c5412 | ||
|
|
6bb4362ed4 | ||
|
|
e617f9452d | ||
|
|
6d8bb49a37 | ||
|
|
4f6073ee86 | ||
|
|
2e7176304b | ||
|
|
e36cf11004 | ||
|
|
0e49e17f68 | ||
|
|
524de45f6b | ||
|
|
85741a4b60 | ||
|
|
f9ccb8c978 | ||
|
|
ea3d069e49 | ||
|
|
3e6024f183 | ||
|
|
337871693a | ||
|
|
2d921c4577 | ||
|
|
9accff7323 | ||
|
|
88b1ee8c31 | ||
|
|
3ac618bb4e | ||
|
|
0051df3741 | ||
|
|
7eb4e010b0 | ||
|
|
33cc23ada3 | ||
|
|
e5aee372e3 | ||
|
|
6b6ce4a761 | ||
|
|
8c4ea7f8f2 | ||
|
|
c8b268b806 | ||
|
|
faf390bb18 | ||
|
|
cf5e0e0f14 | ||
|
|
7b79f9cc17 | ||
|
|
708d599966 | ||
|
|
1ecd5b78e6 | ||
|
|
fca2e3c51a | ||
|
|
95ea761b2d | ||
|
|
6b3bfa1ee9 | ||
|
|
df3e302a9d | ||
|
|
c88a68c9a8 | ||
|
|
92d01b9cdd | ||
|
|
fe04fa5986 | ||
|
|
c382f541b4 | ||
|
|
f420527207 | ||
|
|
e0c83ebf79 | ||
|
|
c7fb18fc08 | ||
|
|
2db8ab937d | ||
|
|
819f5dd8e5 | ||
|
|
d4a8ed735e | ||
|
|
f07e3bb4d5 | ||
|
|
fa5ef0c221 | ||
|
|
da7499ec0b | ||
|
|
d2f4327e44 | ||
|
|
2eba640180 | ||
|
|
29ae55f340 | ||
|
|
3d2bca3f9f | ||
|
|
7fd8c0c822 | ||
|
|
a9e9c81505 | ||
|
|
e8cc68bdea | ||
|
|
9e51a661a4 | ||
|
|
a167aaf55f | ||
|
|
a54ecbcaa0 | ||
|
|
788462cdfa | ||
|
|
45c5965b99 | ||
|
|
ce7614de46 | ||
|
|
9f78e1ce1e | ||
|
|
2c7b0625e8 | ||
|
|
c3a5da9be1 | ||
|
|
ca796e1920 | ||
|
|
7ce04cf781 | ||
|
|
024a3eb760 | ||
|
|
1702f429b4 | ||
|
|
96d79cf495 | ||
|
|
a6a11a7026 | ||
|
|
970a49e2a5 | ||
|
|
2e013ed4f5 | ||
|
|
f8c396b1fe | ||
|
|
b54870cb60 | ||
|
|
84318acb18 | ||
|
|
a11a042b93 | ||
|
|
8a8aa8f62c | ||
|
|
93f78f4db5 | ||
|
|
404bfdd5e6 | ||
|
|
e4577dc2f1 | ||
|
|
5c932e5a27 | ||
|
|
4bd63c6267 | ||
|
|
aabe24f903 | ||
|
|
69cebd7fbc | ||
|
|
8da371176a | ||
|
|
dd08adf1d1 | ||
|
|
2f67bef139 | ||
|
|
8968c51cdc | ||
|
|
f2fdcc9289 | ||
|
|
aa3a575cbe | ||
|
|
11816d038d | ||
|
|
6a990edb38 | ||
|
|
fa12865924 | ||
|
|
ecdd717742 | ||
|
|
6851334af9 | ||
|
|
9051b29565 | ||
|
|
95c7d3dfbd | ||
|
|
bc1148c00a | ||
|
|
d4556d9299 | ||
|
|
5d389a2359 | ||
|
|
305116874b | ||
|
|
b08a29897f | ||
|
|
b59c1d9122 | ||
|
|
adb9cea701 | ||
|
|
5e148d2e82 | ||
|
|
a0d780558e | ||
|
|
ad56065a4e | ||
|
|
f5dee80b6e | ||
|
|
9cc75881b8 | ||
|
|
593fb13b61 | ||
|
|
fca90592d6 | ||
|
|
d6848e2855 | ||
|
|
7539a4129f | ||
|
|
5402574266 | ||
|
|
853175aa1a | ||
|
|
feb84809ec | ||
|
|
a812c568e4 | ||
|
|
11db25e355 | ||
|
|
ecd2fba629 | ||
|
|
a6763cf5a1 | ||
|
|
c9e91a9b94 | ||
|
|
43fb62c5bd | ||
|
|
cb8727d487 | ||
|
|
a94e03e2fd | ||
|
|
425c3c6432 | ||
|
|
89b9610016 | ||
|
|
62fe88f868 | ||
|
|
11a7f5fade | ||
|
|
fbde997f7c | ||
|
|
26734a35ef | ||
|
|
715c4ac534 | ||
|
|
bd4b0885a1 | ||
|
|
e3c7af3d91 | ||
|
|
a7ee21bfd8 | ||
|
|
d0f51d92ac | ||
|
|
e6dc148ea2 | ||
|
|
514ab6637f | ||
|
|
377794abe8 | ||
|
|
0f3251f35b | ||
|
|
8002dc5bc5 | ||
|
|
c75a13dcf4 | ||
|
|
91d153bb9d | ||
|
|
b32f9fa397 | ||
|
|
80593730ae | ||
|
|
090d54a78d | ||
|
|
b7d1fb181c | ||
|
|
6e56693ca7 | ||
|
|
7403db9b20 | ||
|
|
9d167cd883 | ||
|
|
197eec40ad | ||
|
|
07819a6618 | ||
|
|
b72156866d | ||
|
|
59a7d12a8c | ||
|
|
179351b23a | ||
|
|
790809e8e5 | ||
|
|
1414a8a8c9 | ||
|
|
9ab41734a5 | ||
|
|
03cace2ea1 | ||
|
|
c7371ab869 | ||
|
|
b32d4b618c | ||
|
|
3a27f37686 | ||
|
|
fe2d21979d | ||
|
|
48b1f3d4f0 | ||
|
|
93ed589ac7 | ||
|
|
96de9e2c16 | ||
|
|
b25f9d3bec | ||
|
|
15854c605b | ||
|
|
ac193cc94a | ||
|
|
d626f872e6 | ||
|
|
3eb66fa34a | ||
|
|
0fdd0175b7 | ||
|
|
dec9b477e0 | ||
|
|
a0a4b0dd1d | ||
|
|
8dc6da56a7 | ||
|
|
b4e07aacfe | ||
|
|
19b47f0f42 | ||
|
|
f9ef3d63c7 | ||
|
|
2b574d33b5 | ||
|
|
6039e9bb46 | ||
|
|
adfd4b043f | ||
|
|
719189be55 | ||
|
|
ef9907f4b6 | ||
|
|
16b7447df1 | ||
|
|
4157746478 | ||
|
|
5120786708 | ||
|
|
0176fa75ef | ||
|
|
e6968f2d80 | ||
|
|
c0dd8a53e8 | ||
|
|
3cb3135235 | ||
|
|
28182cac64 | ||
|
|
73b80d2482 | ||
|
|
f22eb22409 | ||
|
|
4a95b17a47 | ||
|
|
f4a71159fd | ||
|
|
c0431e3dc2 | ||
|
|
7f87cee282 | ||
|
|
c24c704439 | ||
|
|
232e5d55b8 | ||
|
|
da24ae7e1c | ||
|
|
8fc13f8a8f | ||
|
|
7e1fe31085 | ||
|
|
c3cba8ba4e | ||
|
|
ba619986c9 | ||
|
|
dcef3f3c3b | ||
|
|
823faa2790 | ||
|
|
ef4248d2a3 | ||
|
|
3917cb0dc9 | ||
|
|
520cec0eaa | ||
|
|
e7655e0ff6 | ||
|
|
350ced55c0 | ||
|
|
2ca6d0a00e | ||
|
|
844abad0d0 | ||
|
|
d278e9d8bc | ||
|
|
6e261f30c2 | ||
|
|
84f0e43369 | ||
|
|
941b30847b | ||
|
|
3223a06983 | ||
|
|
1b874a0264 | ||
|
|
26525a0ff9 | ||
|
|
49234ea5c7 | ||
|
|
c474158a09 | ||
|
|
813a541e7f | ||
|
|
efd489bfd4 | ||
|
|
9c88fcb610 | ||
|
|
c1cac8de19 | ||
|
|
b03fa54e63 | ||
|
|
9785731f25 | ||
|
|
566afde18b | ||
|
|
bae8dabc5c | ||
|
|
0a9dfea20a | ||
|
|
50498aa52d | ||
|
|
cc99fa8346 | ||
|
|
500c10ea7a | ||
|
|
71a62caf8f | ||
|
|
fe9c565ad4 | ||
|
|
f5f915dc91 | ||
|
|
eba17fd9b4 | ||
|
|
86f6caa714 | ||
|
|
8ec5a4d071 | ||
|
|
eae49667ef | ||
|
|
9a87b5ec1a | ||
|
|
ee1291e42c | ||
|
|
15aa1fd0b8 | ||
|
|
e069e0e8aa | ||
|
|
57e72c197f | ||
|
|
1d0d25eea2 | ||
|
|
1a6194b38c | ||
|
|
22057083ce | ||
|
|
6b041becb0 | ||
|
|
96d4a91ee9 | ||
|
|
3069900202 | ||
|
|
c46fb0f48a | ||
|
|
07cd8f883e | ||
|
|
cfdb9d64ad | ||
|
|
b73e3aa3b7 | ||
|
|
cd315b0e71 | ||
|
|
4d4d79e66f | ||
|
|
395ce97a78 | ||
|
|
e44e8423d0 | ||
|
|
fa13a56697 | ||
|
|
6383164aec | ||
|
|
d9adfad1c0 | ||
|
|
901828f5a6 | ||
|
|
2a4b0cbc09 | ||
|
|
c5434efd56 | ||
|
|
b73f283095 | ||
|
|
24ef54f01c | ||
|
|
bff3b85337 | ||
|
|
811d9a7237 | ||
|
|
a764cb8dc2 | ||
|
|
9204b9b286 | ||
|
|
da94faa9bb | ||
|
|
4b53e9a895 | ||
|
|
f5db96187b | ||
|
|
857b191b03 | ||
|
|
09014d1ab5 | ||
|
|
7557b71869 | ||
|
|
9c4751794f | ||
|
|
d07187bd5d | ||
|
|
2c6a6ba440 | ||
|
|
4592bf7817 | ||
|
|
afd6d450a0 | ||
|
|
b134849dcf | ||
|
|
e7d0f6d6da | ||
|
|
16a29b0127 | ||
|
|
1f5596ef16 | ||
|
|
bef05432d0 | ||
|
|
4c5a26698e | ||
|
|
67533d7743 | ||
|
|
0cc86c6348 | ||
|
|
607dd68620 | ||
|
|
7c8cbc0799 | ||
|
|
ec0c2e8c33 | ||
|
|
7f3dbe0552 | ||
|
|
0e9044e0c8 | ||
|
|
3171640193 | ||
|
|
a56cee3485 | ||
|
|
c8ee371982 | ||
|
|
5778daeb60 | ||
|
|
f51f3b9861 | ||
|
|
44dd1a0b02 | ||
|
|
61a00ffcbf | ||
|
|
4b0a0f0a32 | ||
|
|
a3088fb8bc | ||
|
|
88fd1f9eb1 | ||
|
|
15156bac1e | ||
|
|
a898d2e7be | ||
|
|
95b003802c | ||
|
|
95c9eae4ed | ||
|
|
e3814403e4 | ||
|
|
3d16d52dd8 | ||
|
|
1ae47fffb4 | ||
|
|
4e7096b9e2 | ||
|
|
8cc9b7f6a7 | ||
|
|
fb45c1020e | ||
|
|
e9db4ae8f4 | ||
|
|
d14a1dd948 | ||
|
|
1c0b434f47 | ||
|
|
573451bade |
@@ -12,10 +12,10 @@ insert_final_newline = true
|
|||||||
# Set default charset
|
# Set default charset
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
||||||
# 2 space indentation
|
# 4 space indentation
|
||||||
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
[*.{cjs,mjs,js,jsx,ts,tsx,css,scss,sass,html,json}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
[*.bat]
|
[*.bat]
|
||||||
charset = latin1
|
charset = latin1
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_BUILD_TYPE = Production
|
|
||||||
VITE_BUILD_PLATFORM = Framework
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_BUILD_TYPE = Production
|
|
||||||
VITE_BUILD_PLATFORM = Shell
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_BUILD_TYPE = Production
|
|
||||||
VITE_BUILD_PLATFORM = Universal
|
|
||||||
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug 反馈
|
name: Bug 反馈
|
||||||
description: 报告可能的 NapCat 异常行为
|
description: 报告可能的 NapCat 异常行为
|
||||||
title: '[BUG] '
|
title: "[BUG] "
|
||||||
labels: bug
|
labels: bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -10,6 +10,10 @@ body:
|
|||||||
在提交新的 Bug 反馈前,请确保您:
|
在提交新的 Bug 反馈前,请确保您:
|
||||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||||
* 不与现有的某一 issue 重复
|
* 不与现有的某一 issue 重复
|
||||||
|
* **不接受因发送不当内容而导致的问题报告**
|
||||||
|
- 包括但不限于:多媒体发送失败、转发消息失败、消息被拦截等因 18+ 内容、违规内容或触发风控的问题
|
||||||
|
- 提交 issue 前,请确认您发送的多媒体内容、链接、文本等均为正常合规内容,不会触发平台风控机制
|
||||||
|
- 因违规内容导致的问题,一律不予受理
|
||||||
- type: input
|
- type: input
|
||||||
id: system-version
|
id: system-version
|
||||||
attributes:
|
attributes:
|
||||||
@@ -30,7 +34,7 @@ body:
|
|||||||
id: napcat-version
|
id: napcat-version
|
||||||
attributes:
|
attributes:
|
||||||
label: NapCat 版本
|
label: NapCat 版本
|
||||||
description: 可在 LiteLoaderQQNT 的设置页或是 QQNT 的设置页侧栏中找到
|
description: 可在 WebUI 的「系统信息」页中找到
|
||||||
placeholder: 1.0.0
|
placeholder: 1.0.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
60
.github/ISSUE_TEMPLATE/feat_request.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
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: 你还想补充什么?
|
||||||
43
.github/prompt/default.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# {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
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 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
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* 构建状态评论模板
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 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();
|
||||||
83
.github/workflows/auto-release.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
name: Auto Release Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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}\"}}"
|
||||||
77
.github/workflows/build.yml
vendored
@@ -1,47 +1,96 @@
|
|||||||
name: "Build Action"
|
name: Build NapCat Artifacts
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build-LiteLoader:
|
Build-Framework:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Main Repository
|
- name: Clone Main Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||||
- name: Use Node.js 20.X
|
- name: Use Node.js 20.X
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- name: Build NapCat.Framework
|
- name: Generate Version
|
||||||
run: |
|
run: |
|
||||||
npm i && cd napcat.webui && npm i && cd .. || exit 1
|
# 获取最近的 release tag (格式: vX.X.X)
|
||||||
npm run build:framework && npm run depend || exit 1
|
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||||
rm package-lock.json
|
# 去掉 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
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: NapCat.Framework
|
name: NapCat.Framework
|
||||||
path: dist
|
path: framework-dist
|
||||||
Build-Shell:
|
Build-Shell:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Main Repository
|
- name: Clone Main Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 需要完整历史来获取 tags
|
||||||
- name: Use Node.js 20.X
|
- name: Use Node.js 20.X
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- name: Build NapCat.Shell
|
- name: Generate Version
|
||||||
run: |
|
run: |
|
||||||
npm i && cd napcat.webui && npm i && cd .. || exit 1
|
# 获取最近的 release tag (格式: vX.X.X)
|
||||||
npm run build:shell && npm run depend || exit 1
|
LATEST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "v0.0.0")
|
||||||
rm package-lock.json
|
# 去掉 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
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: NapCat.Shell
|
name: NapCat.Shell
|
||||||
path: dist
|
path: shell-dist
|
||||||
|
|||||||
303
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 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
|
||||||
540
.github/workflows/release.yml
vendored
@@ -1,152 +1,444 @@
|
|||||||
name: "Build Release"
|
name: Release NapCat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- 'v*'
|
||||||
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
|
|
||||||
|
env:
|
||||||
|
OPENROUTER_API_URL: https://91vip.futureppo.top/v1/chat/completions
|
||||||
|
OPENROUTER_MODEL: "copilot/gemini-3-flash-preview"
|
||||||
|
RELEASE_NAME: "NapCat"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
# 验证版本号格式
|
||||||
|
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'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Main Repository
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Use Node.js 20.X
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
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:
|
Build-Shell:
|
||||||
|
needs: validate-version
|
||||||
|
if: needs.validate-version.outputs.valid == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [check-version]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Main Repository
|
- name: Clone Main Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
- name: Use Node.js 20.X
|
||||||
repository: 'NapNeko/NapCatQQ'
|
uses: actions/setup-node@v4
|
||||||
submodules: true
|
with:
|
||||||
ref: main
|
node-version: 20.x
|
||||||
token: ${{ secrets.NAPCAT_BUILD }}
|
- 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: Use Node.js 20.X
|
- name: Download Artifacts
|
||||||
uses: actions/setup-node@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
path: ./artifacts
|
||||||
|
|
||||||
- name: Build NuCat Shell
|
|
||||||
run: |
|
|
||||||
npm i
|
|
||||||
cd napcat.webui
|
|
||||||
npm i
|
|
||||||
cd ..
|
|
||||||
npm run build:shell
|
|
||||||
cd dist
|
|
||||||
npm i --omit=dev
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Setup tools
|
||||||
uses: actions/upload-artifact@v4
|
run: |
|
||||||
with:
|
sudo apt update
|
||||||
name: NapCat.Shell
|
sudo apt install -y aria2 unzip zip p7zip-full curl jq
|
||||||
path: dist
|
|
||||||
|
- name: Download QQ x64, Node.js and Assemble NapCat.Shell.Windows.Node.zip
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
cd "$TMPDIR"
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 1) 下载 QQ x64
|
||||||
|
# -----------------------------
|
||||||
|
# JS_URL="https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
|
||||||
|
# JS_URL="https://slave.docadan488.workers.dev/proxy?url=https://cdn-go.cn/qq-web/im.qq.com_new/latest/rainbow/windowsConfig.js"
|
||||||
|
# NT_URL=$(curl -fsSL "$JS_URL" | grep -oP '"ntDownloadX64Url"\s*:\s*"\K[^"]+')
|
||||||
|
NT_URL="https://dldir1v6.qq.com/qqfile/qq/QQNT/eb263b35/QQ9.9.23.42086_x64.exe"
|
||||||
|
QQ_ZIP="$(basename "$NT_URL")"
|
||||||
|
aria2c -x16 -s16 -k1M -o "$QQ_ZIP" "$NT_URL"
|
||||||
|
|
||||||
|
QQ_EXTRACT="$TMPDIR/qq_extracted"
|
||||||
|
mkdir -p "$QQ_EXTRACT"
|
||||||
|
7z x -y -o"$QQ_EXTRACT" "$QQ_ZIP" >/dev/null
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 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")
|
||||||
|
for name in "${QQ_TARGETS[@]}"; do
|
||||||
|
find "$QQ_EXTRACT" -iname "$name" -exec cp -a {} "$OUT_DIR" \; || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 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
|
||||||
|
# -----------------------------
|
||||||
|
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
|
||||||
|
|
||||||
release-napcat:
|
release-napcat:
|
||||||
needs: [Build-LiteLoader,Build-Shell]
|
needs: [Build-Framework, Build-Shell, Download-QNX64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Clone Main Repository
|
- name: Download Artifacts
|
||||||
uses: actions/checkout@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
repository: 'NapNeko/NapCatQQ'
|
path: ./artifacts
|
||||||
submodules: true
|
|
||||||
ref: main
|
|
||||||
token: ${{ secrets.NAPCAT_BUILD }}
|
|
||||||
|
|
||||||
- name: Download All Artifact
|
- name: Download NapCat.Shell.Windows.OneKey.zip
|
||||||
uses: actions/download-artifact@v4
|
run: |
|
||||||
|
curl -L -o NapCat.Shell.Windows.OneKey.zip https://github.com/NapNeko/NapCatResource/raw/main/NapCat.Shell.Windows.OneKey.zip
|
||||||
- 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
|
- name: Zip Artifacts
|
||||||
unzip -q ./external/LiteLoaderWrapper.zip -d ./NapCat.Framework.Windows.Once
|
run: |
|
||||||
cd ./NapCat.Framework.Windows.Once
|
cd artifacts
|
||||||
ls
|
[ -d NapCat.Framework ] && (cd NapCat.Framework && zip -qr ../../NapCat.Framework.zip .)
|
||||||
mkdir -p ./LL/plugins/NapCatQQ
|
[ -d NapCat.Shell ] && (cd NapCat.Shell && zip -qr ../../NapCat.Shell.zip .)
|
||||||
unzip -q ../NapCat.Framework.zip -d ./LL/plugins/NapCatQQ
|
[ -d NapCat.Shell.Windows.Node ] && (cd NapCat.Shell.Windows.Node && zip -qr ../../NapCat.Shell.Windows.Node.zip .)
|
||||||
zip -q -r NapCat.Framework.Windows.Once.zip *
|
cd ..
|
||||||
cd ..
|
|
||||||
mv ./NapCat.Framework.Windows.Once/NapCat.Framework.Windows.Once.zip ./
|
- name: Generate release note via OpenRouter
|
||||||
- name: Extract version from tag
|
env:
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
|
OPENROUTER_API_URL: ${{ env.OPENROUTER_API_URL }}
|
||||||
- name: Clone Changes Log
|
OPENROUTER_MODEL: ${{ env.OPENROUTER_MODEL }}
|
||||||
run: curl -o CHANGELOG.md https://fastly.jsdelivr.net/gh/NapNeko/NapCatQQ@main/docs/changelogs/CHANGELOG.v${{ env.VERSION }}.md
|
GITHUB_OWNER: "NapNeko" # 替换成你的 repo owner
|
||||||
|
GITHUB_REPO: "NapCatQQ" # 替换成你的 repo 名
|
||||||
- name: Create Release Draft and Upload Artifacts
|
run: |
|
||||||
uses: softprops/action-gh-release@v1
|
set -euo pipefail
|
||||||
with:
|
|
||||||
name: NapCat V${{ env.VERSION }}
|
# 当前 tag
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
CURRENT_TAG="${GITHUB_REF#refs/tags/}"
|
||||||
body_path: CHANGELOG.md
|
echo "Current tag: $CURRENT_TAG"
|
||||||
files: |
|
|
||||||
NapCat.Framework.zip
|
# 从 GitHub API 获取 tag 列表
|
||||||
NapCat.Shell.zip
|
TAGS_JSON=$(curl -s "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/tags?per_page=100")
|
||||||
NapCat.Framework.Windows.Once.zip
|
TAGS=( $(echo "$TAGS_JSON" | jq -r '.[].name' | sort -V) )
|
||||||
draft: true
|
|
||||||
|
# 找到上一个 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:1500}')
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -1,14 +1,12 @@
|
|||||||
# Develop
|
# Develop
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
out/
|
out/
|
||||||
dist/
|
dist/
|
||||||
/src/core.lib/common/
|
/src/core.lib/common/
|
||||||
/localdebug/
|
devconfig/*
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea/*
|
.idea/*
|
||||||
|
|
||||||
@@ -16,3 +14,6 @@ dist/
|
|||||||
*.db
|
*.db
|
||||||
checkVersion.sh
|
checkVersion.sh
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
tests/run/
|
||||||
|
guild1.db-wal
|
||||||
|
guild1.db-shm
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"tabWidth": 4,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"bracketSpacing": true,
|
|
||||||
"arrowParens": "always",
|
|
||||||
"printWidth": 120,
|
|
||||||
"endOfLine": "auto"
|
|
||||||
}
|
|
||||||
12
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "调试程序",
|
||||||
|
"command": "pnpm run dev:shell",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
37
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 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.
|
||||||
98
README.md
@@ -1,67 +1,89 @@
|
|||||||
|
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|
# NapCat
|
||||||
|
|
||||||
|
_Modern protocol-side framework implemented based on NTQQ._
|
||||||
|
|
||||||
|
> 云起兮风生,心向远方兮路未曾至.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
## 欢迎回家
|
|
||||||
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
|
||||||
|
|
||||||
## 特性介绍
|
## New Feature
|
||||||
- [x] **安装简单**:就算是笨蛋也能使用
|
|
||||||
- [x] **性能友好**:就算是低内存也能使用
|
|
||||||
- [x] **接口丰富**:就算是没有也能使用
|
|
||||||
- [x] **稳定好用**:就算是被捉也能使用
|
|
||||||
|
|
||||||
## 使用框架
|
在 v4.8.115+ 版本开始
|
||||||
|
|
||||||
|
1. NapCatQQ 支持 [Stream Api](https://napneko.github.io/develop/file)
|
||||||
|
2. NapCatQQ 推荐 message_id/user_id/group_id 均使用字符串类型
|
||||||
|
|
||||||
|
- [1] 解决 Docker/跨设备/大文件 的多媒体上下传问题
|
||||||
|
- [2] 采用字符串可以解决扩展到int64的问题,同时也可以解决部分语言(如JavaScript)对大整数支持不佳的问题,增加极少成本。
|
||||||
|
|
||||||
|
## Welcome
|
||||||
|
|
||||||
|
- NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
|
||||||
|
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||||
|
|
||||||
|
## Feature
|
||||||
|
|
||||||
|
- **Easy to Use**
|
||||||
|
- 作为初学者能够轻松使用.
|
||||||
|
- **Quick and Efficient**
|
||||||
|
- 在低内存操作系统长时运行.
|
||||||
|
- **Rich API Interface**
|
||||||
|
- 完整实现了大部分标准接口.
|
||||||
|
- **Stable and Reliable**
|
||||||
|
- 持续稳定的开发与维护.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||||
|
|
||||||
**首次使用**请务必查看如下文档看使用教程
|
**首次使用**请务必查看如下文档看使用教程
|
||||||
|
|
||||||
### 文档地址
|
> 项目非盈利,涉及 对接问题/基础问题/下层框架问题 请自行搜索解决,本项目社区不提供此类解答。
|
||||||
|
|
||||||
[Cloudflare.Worker](https://doc.napneko.icu/)
|
## Link
|
||||||
|
|
||||||
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
| Docs | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
|
||||||
|
|:-:|:-:|:-:|:-:|
|
||||||
|
|
||||||
[Github.IO](https://napneko.github.io/)
|
| Docs | [](https://napneko.pages.dev/) | [](https://napcat.cyou/) | [](https://www.napcat.wiki) |
|
||||||
|
|:-:|:-:|:-:|:-:|
|
||||||
|
|
||||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
| QQ Group | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/8zJMLjqy2Y) | [](https://qm.qq.com/q/CMmPbGw0jA) | [](https://qm.qq.com/q/I6LU87a0Yq) |
|
||||||
|
|:-:|:-:|:-:|:-:|:-:|
|
||||||
|
|
||||||
[Server.Other](https://docs.napcat.cyou/)
|
| Telegram | [](https://t.me/napcatqq) |
|
||||||
|
|:-:|:-:|
|
||||||
|
|
||||||
[Qbot.News](https://neko.qbot.news)
|
| DeepWiki | [](https://deepwiki.com/NapNeko/NapCatQQ) |
|
||||||
|
|:-:|:-:|
|
||||||
|
|
||||||
## 回家旅途
|
> 请不要在其余社区提及本项目(包括其余协议端/相关应用端项目)引发争论,如有建议到达官方交流群讨论或PR。
|
||||||
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
|
|
||||||
|
|
||||||
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
|
## Thanks
|
||||||
|
|
||||||
[Telegram](https://t.me/MelodicMoonlight)
|
- [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||||
|
|
||||||
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
|
- [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
|
||||||
|
|
||||||
## 性能设计/协议标准
|
- [MaiBot](https://github.com/MaiM-with-u/MaiBot) 一只赛博群友 麦麦 Bot框架 在此推荐一下
|
||||||
NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
|
|
||||||
|
|
||||||
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段,消息Id无法持久,无法上报撤回消息原始内容。
|
- [qq-chat-exporter](https://github.com/shuakami/qq-chat-exporter/) 基于NapCat的消息导出工具 在此推荐一下
|
||||||
|
|
||||||
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
|
- 不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||||
|
|
||||||
## 感谢他们
|
|
||||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
|
||||||
|
|
||||||
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
|
|
||||||
|
|
||||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 特殊感谢
|
## License
|
||||||
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
|
|
||||||
|
|
||||||
## 开源附加
|
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
|
||||||
|
|
||||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
1. 第三方库代码或修改部分遵循其原始开源许可.
|
||||||
|
2. 本项目获取部分项目授权而不受部分约束
|
||||||
|
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
|
||||||
|
|
||||||
|
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
|
||||||
|
|||||||
11
SECURITY.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| > 4.0 | :white_check_mark: |
|
||||||
|
| < 4.0 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
you should open an issue
|
||||||
52
eslint.config.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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;
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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
BIN
external/logo.png
vendored
|
Before Width: | Height: | Size: 204 KiB |
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM ./launcher.bat 123456
|
|
||||||
REM ./launcher-win10.bat 123456
|
|
||||||
REM 带有REM的为注释 删掉你需要的系统的那行REM这三个单词 修改QQ本脚本启动即可
|
|
||||||
BIN
logo.png
|
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 250 KiB |
3
napcat.webui/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["Vue.volar"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# 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).
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>NapCat WebUI</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="./src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "napcat.webui",
|
|
||||||
"private": true,
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
|
||||||
"webui:dev": "vite --host",
|
|
||||||
"webui:build": "vite build",
|
|
||||||
"webui:preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
|
||||||
"event-source-polyfill": "^1.0.31",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"tdesign-icons-vue-next": "^0.3.3",
|
|
||||||
"tdesign-vue-next": "^1.10.3",
|
|
||||||
"vue": "^3.5.13",
|
|
||||||
"vue-router": "^4.4.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
|
||||||
"@eslint/js": "^9.14.0",
|
|
||||||
"@types/event-source-polyfill": "^1.0.5",
|
|
||||||
"@types/qrcode": "^1.5.5",
|
|
||||||
"@vitejs/plugin-legacy": "^5.4.3",
|
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-vue": "^9.31.0",
|
|
||||||
"globals": "^15.12.0",
|
|
||||||
"terser": "^5.36.0",
|
|
||||||
"typescript": "~5.6.2",
|
|
||||||
"vite": "^5.4.10",
|
|
||||||
"vue-tsc": "^2.1.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 201 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,112 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app" theme-mode="dark">
|
|
||||||
<router-view />
|
|
||||||
</div>
|
|
||||||
<div v-if="show">
|
|
||||||
<t-sticky-tool shape="round" placement="right-bottom" :offset="[-50, 10]" @click="changeTheme">
|
|
||||||
<t-sticky-item label="浅色" popup="切换浅色模式">
|
|
||||||
<template #icon><sunny-icon /></template>
|
|
||||||
</t-sticky-item>
|
|
||||||
<t-sticky-item label="深色" popup="切换深色模式">
|
|
||||||
<template #icon><mode-dark-icon /></template>
|
|
||||||
</t-sticky-item>
|
|
||||||
<t-sticky-item label="自动" popup="跟随系统">
|
|
||||||
<template #icon><control-platform-icon /></template>
|
|
||||||
</t-sticky-item>
|
|
||||||
</t-sticky-tool>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
|
|
||||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
|
||||||
interface Item {
|
|
||||||
label: string;
|
|
||||||
popup: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Context {
|
|
||||||
item: Item;
|
|
||||||
}
|
|
||||||
enum ThemeMode {
|
|
||||||
Dark = 'dark',
|
|
||||||
Light = 'light',
|
|
||||||
Auto = 'auto',
|
|
||||||
}
|
|
||||||
const themeLabelMap: Record<string, ThemeMode> = {
|
|
||||||
"浅色": ThemeMode.Light,
|
|
||||||
"深色": ThemeMode.Dark,
|
|
||||||
"自动": ThemeMode.Auto,
|
|
||||||
};
|
|
||||||
const show = ref<boolean>(true);
|
|
||||||
const createSetThemeAttributeFunction = () => {
|
|
||||||
let mediaQueryForAutoTheme: MediaQueryList | null = null;
|
|
||||||
return (mode: ThemeMode | null) => {
|
|
||||||
const element = document.documentElement;
|
|
||||||
if (mode === ThemeMode.Dark) {
|
|
||||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
|
||||||
} else if (mode === ThemeMode.Light) {
|
|
||||||
element.removeAttribute('theme-mode');
|
|
||||||
} else if (mode === ThemeMode.Auto) {
|
|
||||||
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
|
||||||
if (e.matches) {
|
|
||||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
|
||||||
} else {
|
|
||||||
element.removeAttribute('theme-mode');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
|
|
||||||
const event = new Event('change');
|
|
||||||
Object.defineProperty(event, 'matches', {
|
|
||||||
value: mediaQueryForAutoTheme.matches,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
mediaQueryForAutoTheme.dispatchEvent(event);
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (mediaQueryForAutoTheme) {
|
|
||||||
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const setThemeAttribute = createSetThemeAttributeFunction();
|
|
||||||
|
|
||||||
const getStoredTheme = (): ThemeMode | null => {
|
|
||||||
return localStorage.getItem('theme') as ThemeMode | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTheme = () => {
|
|
||||||
const storedTheme = getStoredTheme();
|
|
||||||
if (storedTheme === null) {
|
|
||||||
setThemeAttribute(ThemeMode.Auto);
|
|
||||||
} else {
|
|
||||||
setThemeAttribute(storedTheme);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTheme = (context: Context) => {
|
|
||||||
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
|
|
||||||
console.log(themeLabel);
|
|
||||||
setThemeAttribute(themeLabel);
|
|
||||||
localStorage.setItem('theme', themeLabel);
|
|
||||||
};
|
|
||||||
const haddingFbars = () => {
|
|
||||||
show.value = !smallScreen.matches;
|
|
||||||
if (smallScreen.matches) {
|
|
||||||
localStorage.setItem('theme', 'auto');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
initTheme();
|
|
||||||
haddingFbars();
|
|
||||||
window.addEventListener('resize', haddingFbars);
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', haddingFbars);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style></style>
|
|
||||||
|
Before Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 201 KiB |
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,66 +0,0 @@
|
|||||||
export class githubApiManager {
|
|
||||||
public async GetBaseData(): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (ConfigResponse.status == 200) {
|
|
||||||
return await ConfigResponse.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting github data :', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
public async GetReleasesData(): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/releases', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (ConfigResponse.status == 200) {
|
|
||||||
return await ConfigResponse.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting releases data:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
public async GetPullsData(): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/pulls', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (ConfigResponse.status == 200) {
|
|
||||||
return await ConfigResponse.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting Pulls data:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
public async GetContributors(): Promise<Response | null> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/contributors', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (ConfigResponse.status == 200) {
|
|
||||||
return await ConfigResponse.json();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting Pulls data:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
||||||
type LogListItem = string;
|
|
||||||
type LogListData = LogListItem[];
|
|
||||||
let eventSourcePoly: EventSourcePolyfill | null = null;
|
|
||||||
export class LogManager {
|
|
||||||
private readonly 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;
|
|
||||||
}
|
|
||||||
public async GetLogList(): Promise<LogListData> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLogList`, {
|
|
||||||
method: 'GET',
|
|
||||||
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 LogListData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting LogList:', error);
|
|
||||||
}
|
|
||||||
return [] as LogListData;
|
|
||||||
}
|
|
||||||
public async GetLog(FileName: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLog?id=${FileName}`, {
|
|
||||||
method: 'GET',
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting LogData:', error);
|
|
||||||
}
|
|
||||||
return 'null';
|
|
||||||
}
|
|
||||||
public async getRealTimeLogs(): Promise<EventSourcePolyfill | null> {
|
|
||||||
this.creatEventSource();
|
|
||||||
return eventSourcePoly;
|
|
||||||
}
|
|
||||||
private creatEventSource() {
|
|
||||||
try {
|
|
||||||
eventSourcePoly = new EventSourcePolyfill(`${this.apiPrefix}/Log/GetLogRealTime`, {
|
|
||||||
heartbeatTimeout: 3 * 60 * 1000,
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
Accept: 'text/event-stream',
|
|
||||||
},
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建SSE连接出错:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
|
||||||
import { ResponseCode } from '../../../src/webui/src/const/status';
|
|
||||||
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 == ResponseCode.Success) {
|
|
||||||
return ConfigResponseJson.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting OB11 config:', error);
|
|
||||||
}
|
|
||||||
return {} as OneBotConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ config: JSON.stringify(config) }),
|
|
||||||
});
|
|
||||||
if (ConfigResponse.status == 200) {
|
|
||||||
const ConfigResponseJson = await ConfigResponse.json();
|
|
||||||
if (ConfigResponseJson.code == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting OB11 config:', error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkQQLoginStatus(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (QQLoginResponse.status == 200) {
|
|
||||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
|
||||||
if (QQLoginResponseJson.code == 0) {
|
|
||||||
return QQLoginResponseJson.data.isLogin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking QQ login status:', error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
|
|
||||||
try {
|
|
||||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (QQLoginResponse.status == 200) {
|
|
||||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
|
||||||
if (QQLoginResponseJson.code == 0) {
|
|
||||||
return QQLoginResponseJson.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking QQ login status:', error);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkWebUiLogined(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (LoginResponse.status == 200) {
|
|
||||||
const LoginResponseJson = await LoginResponse.json();
|
|
||||||
if (LoginResponseJson.code == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking web UI login status:', error);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loginWithToken(token: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ token: token }),
|
|
||||||
});
|
|
||||||
const loginResponseJson = await loginResponse.json();
|
|
||||||
const retCode = loginResponseJson.code;
|
|
||||||
if (retCode === 0) {
|
|
||||||
this.retCredential = loginResponseJson.data.Credential;
|
|
||||||
return this.retCredential;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error logging in with token:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getQQLoginQrcode(): Promise<string> {
|
|
||||||
try {
|
|
||||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (QQLoginResponse.status == 200) {
|
|
||||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
|
||||||
if (QQLoginResponseJson.code == 0) {
|
|
||||||
return QQLoginResponseJson.data.qrcode || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting QQ login QR code:', error);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getQQQuickLoginList(): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (QQLoginResponse.status == 200) {
|
|
||||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
|
||||||
if (QQLoginResponseJson.code == 0) {
|
|
||||||
return QQLoginResponseJson.data || [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting QQ quick login list:', error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
|
|
||||||
try {
|
|
||||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.retCredential,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ uin: uin }),
|
|
||||||
});
|
|
||||||
if (QQLoginResponse.status == 200) {
|
|
||||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
|
||||||
if (QQLoginResponseJson.code == 0) {
|
|
||||||
return { result: true, errMsg: '' };
|
|
||||||
} else {
|
|
||||||
return { result: false, errMsg: QQLoginResponseJson.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting quick login:', error);
|
|
||||||
}
|
|
||||||
return { result: false, errMsg: '接口异常' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-layout class="dashboard-container">
|
|
||||||
<div v-if="!mediaQuery.matches">
|
|
||||||
<SidebarMenu
|
|
||||||
:menu-items="menuItems"
|
|
||||||
class="sidebar-menu"
|
|
||||||
:menu-width="sidebarWidth"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<t-layout>
|
|
||||||
<router-view />
|
|
||||||
</t-layout>
|
|
||||||
<div v-if="mediaQuery.matches" class="bottom-menu">
|
|
||||||
<BottomMenu :menu-items="menuItems" />
|
|
||||||
</div>
|
|
||||||
</t-layout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import SidebarMenu from './webui/Nav.vue';
|
|
||||||
import BottomMenu from './webui/NavBottom.vue';
|
|
||||||
import emitter from '@/ts/event-bus';
|
|
||||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
|
||||||
const sidebarWidth = ['232px', '64px'];
|
|
||||||
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' },
|
|
||||||
]);
|
|
||||||
emitter.on('sendMenu', (event) => {
|
|
||||||
const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
|
|
||||||
emitter.emit('sendWidth', menuWidth);
|
|
||||||
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (mediaQuery.matches){
|
|
||||||
localStorage.setItem('menuWidth', '0');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dashboard-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.bottom-menu {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.content {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.t-head-menu__inner .t-menu:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
.t-head-menu__inner{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.t-head-menu .t-menu{
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
.t-menu__content{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-card class="layout" :bordered="false">
|
|
||||||
<div class="login-container">
|
|
||||||
<h2 class="sotheby-font">QQ Login</h2>
|
|
||||||
<div class="login-methods">
|
|
||||||
<t-tooltip content="快速登录">
|
|
||||||
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
|
|
||||||
@click="loginMethod = 'quick'">Quick Login</t-button>
|
|
||||||
</t-tooltip>
|
|
||||||
<t-tooltip content="二维码登录">
|
|
||||||
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
|
|
||||||
@click="loginMethod = 'qrcode'">QR Code</t-button>
|
|
||||||
</t-tooltip>
|
|
||||||
</div>
|
|
||||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
|
||||||
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
|
|
||||||
@change="selectAccount">
|
|
||||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
|
||||||
</t-select>
|
|
||||||
</div>
|
|
||||||
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
|
||||||
<canvas ref="qrcodeCanvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
|
||||||
</t-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
|
||||||
import * as QRCode from 'qrcode';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const loginMethod = ref<'quick' | 'qrcode'>('quick');
|
|
||||||
const quickLoginList = ref<string[]>([]);
|
|
||||||
const selectedAccount = ref<string>('');
|
|
||||||
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
|
||||||
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
|
||||||
let heartBeatTimer: number | null = null;
|
|
||||||
let qrcodeUrl: string = '';
|
|
||||||
|
|
||||||
const selectAccount = async (accountName: string): Promise<void> => {
|
|
||||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
|
||||||
if (result) {
|
|
||||||
if (heartBeatTimer) {
|
|
||||||
clearInterval(heartBeatTimer);
|
|
||||||
}
|
|
||||||
await MessagePlugin.success('登录成功即将跳转');
|
|
||||||
await router.push({ path: '/dashboard/basic-info' });
|
|
||||||
} else {
|
|
||||||
await MessagePlugin.error('登录失败,' + errMsg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
|
|
||||||
if (!canvas) {
|
|
||||||
console.error('Canvas element not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Error generating QR Code:', error);
|
|
||||||
} else {
|
|
||||||
console.log('QR Code generated!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const HeartBeat = async (): Promise<void> => {
|
|
||||||
const isLogined = await qqLoginManager.checkQQLoginStatusWithQrcode();
|
|
||||||
if (isLogined?.isLogin) {
|
|
||||||
if (heartBeatTimer) {
|
|
||||||
clearInterval(heartBeatTimer);
|
|
||||||
}
|
|
||||||
await MessagePlugin.success('登录成功即将跳转');
|
|
||||||
await router.push({ path: '/dashboard/basic-info' });
|
|
||||||
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
|
|
||||||
qrcodeUrl = isLogined.qrcodeurl;
|
|
||||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const InitPages = async (): Promise<void> => {
|
|
||||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
|
||||||
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
|
|
||||||
await nextTick();
|
|
||||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
InitPages().then().catch((err) => {
|
|
||||||
console.error('InitPages Error:', err);
|
|
||||||
});
|
|
||||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (heartBeatTimer) {
|
|
||||||
clearInterval(heartBeatTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(loginMethod, async (newMethod) => {
|
|
||||||
if (newMethod === 'qrcode') {
|
|
||||||
await nextTick();
|
|
||||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.layout {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 400px;
|
|
||||||
min-width: 300px;
|
|
||||||
position: relative;
|
|
||||||
margin: 50px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.login-container {
|
|
||||||
width: 90%;
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-methods {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-method {
|
|
||||||
padding: 10px 15px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-method.active {
|
|
||||||
background-color: #e6f0ff;
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-form,
|
|
||||||
.qrcode {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qrcode {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sotheby-font {
|
|
||||||
font-family: Sotheby, Helvetica, monospace;
|
|
||||||
font-size: 3.125rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #888;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-card class="layout" :bordered="false">
|
|
||||||
<div class="login-container">
|
|
||||||
<h2 class="sotheby-font">WebUi Login</h2>
|
|
||||||
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
|
||||||
<t-form-item name="password">
|
|
||||||
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
|
||||||
<template #prefix-icon>
|
|
||||||
<lock-on-icon />
|
|
||||||
</template>
|
|
||||||
</t-input>
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item>
|
|
||||||
<t-button theme="primary" type="submit" block>登录</t-button>
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
|
||||||
</t-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import '../css/style.css';
|
|
||||||
import '../css/font.css';
|
|
||||||
import { reactive, onMounted } from 'vue';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { LockOnIcon } from 'tdesign-icons-vue-next';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
interface FormData {
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData: FormData = reactive({
|
|
||||||
token: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLoginSuccess = async (credential: string) => {
|
|
||||||
localStorage.setItem('auth', credential);
|
|
||||||
await checkLoginStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLoginFailure = (message: string) => {
|
|
||||||
MessagePlugin.error(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkLoginStatus = async () => {
|
|
||||||
const storedCredential = localStorage.getItem('auth');
|
|
||||||
if (!storedCredential) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loginManager = new QQLoginManager(storedCredential);
|
|
||||||
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
|
|
||||||
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
|
|
||||||
if (!isWenUiLoggedIn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
|
|
||||||
if (isQQLoggedIn) {
|
|
||||||
await router.push({ path: '/dashboard/basic-info' });
|
|
||||||
} else {
|
|
||||||
await router.push({ path: '/qqlogin' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loginWithToken = async (token: string) => {
|
|
||||||
const loginManager = new QQLoginManager('');
|
|
||||||
const credential = await loginManager.loginWithToken(token);
|
|
||||||
if (credential) {
|
|
||||||
await handleLoginSuccess(credential);
|
|
||||||
} else {
|
|
||||||
handleLoginFailure('登录失败,请检查Token');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const token = url.searchParams.get('token');
|
|
||||||
if (token) {
|
|
||||||
loginWithToken(token);
|
|
||||||
}
|
|
||||||
checkLoginStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
|
||||||
if (validateResult) {
|
|
||||||
await loginWithToken(formData.token);
|
|
||||||
} else {
|
|
||||||
handleLoginFailure('请填写Token');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.layout {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
.login-container {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 400px;
|
|
||||||
min-width: 300px;
|
|
||||||
position: relative;
|
|
||||||
margin: 50px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.login-container {
|
|
||||||
width: 90%;
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tdesign-demo-block-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tdesign-demo-block-column-large {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tdesign-demo-block-row {
|
|
||||||
display: flex;
|
|
||||||
column-gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sotheby-font {
|
|
||||||
font-family: Sotheby, Helvetica, monospace;
|
|
||||||
font-size: 3.125rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #888;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
|
|
||||||
<template #logo>
|
|
||||||
<div class="logo">
|
|
||||||
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
|
||||||
<div class="logo-textBox">
|
|
||||||
<div class="logo-text">{{ collapsed ? '' : 'NapCat' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
|
||||||
<t-tooltip :disabled="!collapsed" :content="item.label" placement="right">
|
|
||||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon :name="item.icon" />
|
|
||||||
</template>
|
|
||||||
{{ item.label }}
|
|
||||||
</t-menu-item>
|
|
||||||
</t-tooltip>
|
|
||||||
</router-link>
|
|
||||||
<template #operations>
|
|
||||||
<t-button
|
|
||||||
:disabled="disBtn"
|
|
||||||
class="t-demo-collapse-btn"
|
|
||||||
variant="text"
|
|
||||||
shape="square"
|
|
||||||
@click="changeCollapsed"
|
|
||||||
>
|
|
||||||
<template #icon><t-icon :name="iconName" /></template>
|
|
||||||
</t-button>
|
|
||||||
</template>
|
|
||||||
</t-menu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, watch } from 'vue';
|
|
||||||
import emitter from '@/ts/event-bus';
|
|
||||||
|
|
||||||
type MenuItem = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
route: string;
|
|
||||||
icon?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
defineProps<{
|
|
||||||
menuItems: MenuItem[];
|
|
||||||
menuWidth: string | number | Array<string | number>;
|
|
||||||
}>();
|
|
||||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
|
||||||
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
|
||||||
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
|
||||||
const disBtn = ref<boolean>(false);
|
|
||||||
|
|
||||||
const changeCollapsed = (): void => {
|
|
||||||
collapsed.value = !collapsed.value;
|
|
||||||
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
|
||||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
|
||||||
};
|
|
||||||
watch(collapsed, (newValue, oldValue) => {
|
|
||||||
emitter.emit('sendMenu', collapsed.value);
|
|
||||||
});
|
|
||||||
onMounted(() => {
|
|
||||||
emitter.emit('sendMenu', collapsed.value);
|
|
||||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
|
||||||
disBtn.value = e.matches;
|
|
||||||
if (e.matches) {
|
|
||||||
collapsed.value = e.matches;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mediaQuery.addEventListener('change', handleMediaChange);
|
|
||||||
const event = new Event('change');
|
|
||||||
Object.defineProperty(event, 'matches', {
|
|
||||||
value: mediaQuery.matches,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
mediaQuery.dispatchEvent(event);
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.sidebar-menu {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 200px;
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.sidebar-menu {
|
|
||||||
width: 100px; /* 移动端侧边栏宽度 */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.logo-img {
|
|
||||||
object-fit: contain;
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.logo-textBox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.logo-text {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 22px;
|
|
||||||
font-family: Sotheby, Helvetica, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<template>
|
|
||||||
<t-head-menu theme="light" class="bottom-menu">
|
|
||||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
|
||||||
<t-tooltip :content="item.label" placement="top">
|
|
||||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon :name="item.icon" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- {{item.label}}-->
|
|
||||||
</t-menu-item>
|
|
||||||
</t-tooltip>
|
|
||||||
</router-link>
|
|
||||||
</t-head-menu>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
type MenuItem = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
route: string;
|
|
||||||
icon?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
menuItems: MenuItem[];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.bottom-menu {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
border-top: 0.8px solid #ddd;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Sotheby';
|
|
||||||
src: url('../assets/Sotheby.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: 'ProtoNerdFontItalic';
|
|
||||||
src: url('../assets/0xProtoNerdFont-Italic.ttf') format('truetype');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import { createApp } from 'vue';
|
|
||||||
import App from './App.vue';
|
|
||||||
import {
|
|
||||||
Button as TButton,
|
|
||||||
Input as TInput,
|
|
||||||
Form as TForm,
|
|
||||||
FormItem as TFormItem,
|
|
||||||
Select as TSelect,
|
|
||||||
Option as TOption,
|
|
||||||
Menu as TMenu,
|
|
||||||
MenuItem as TMenuItem,
|
|
||||||
Icon as TIcon,
|
|
||||||
Submenu as TSubmenu,
|
|
||||||
Col as TCol,
|
|
||||||
Row as TRow,
|
|
||||||
Card as TCard,
|
|
||||||
Divider as TDivider,
|
|
||||||
Link as TLink,
|
|
||||||
List as TList,
|
|
||||||
Alert as TAlert,
|
|
||||||
Tag as TTag,
|
|
||||||
Descriptions as TDescriptionsProps,
|
|
||||||
DescriptionsItem as TDescriptionsItem,
|
|
||||||
Collapse as TCollapse,
|
|
||||||
CollapsePanel as TCollapsePanel,
|
|
||||||
ListItem as TListItem,
|
|
||||||
Tabs as TTabs,
|
|
||||||
TabPanel as TTabPanel,
|
|
||||||
Space as TSpace,
|
|
||||||
Checkbox as TCheckbox,
|
|
||||||
Popup as TPopup,
|
|
||||||
Dialog as TDialog,
|
|
||||||
Switch as TSwitch,
|
|
||||||
Tooltip as Tooltip,
|
|
||||||
StickyTool as TStickyTool,
|
|
||||||
StickyItem as TStickyItem,
|
|
||||||
Layout as TLayout,
|
|
||||||
Content as TContent,
|
|
||||||
Footer as TFooter,
|
|
||||||
Aside as TAside,
|
|
||||||
Popconfirm as Tpopconfirm,
|
|
||||||
Empty as TEmpty,
|
|
||||||
Dropdown as TDropdown,
|
|
||||||
Typography as TTypographyText,
|
|
||||||
TreeSelect as TTreeSelect,
|
|
||||||
Loading as TLoading,
|
|
||||||
HeadMenu as THeadMenu
|
|
||||||
} from 'tdesign-vue-next';
|
|
||||||
import router from './router';
|
|
||||||
import 'tdesign-vue-next/es/style/index.css';
|
|
||||||
const app = createApp(App);
|
|
||||||
app.use(router);
|
|
||||||
app.use(TButton);
|
|
||||||
app.use(TInput);
|
|
||||||
app.use(TForm);
|
|
||||||
app.use(TFormItem);
|
|
||||||
app.use(TSelect);
|
|
||||||
app.use(TOption);
|
|
||||||
app.use(TMenu);
|
|
||||||
app.use(TMenuItem);
|
|
||||||
app.use(TIcon);
|
|
||||||
app.use(TSubmenu);
|
|
||||||
app.use(TCol);
|
|
||||||
app.use(TRow);
|
|
||||||
app.use(TCard);
|
|
||||||
app.use(TDivider);
|
|
||||||
app.use(TLink);
|
|
||||||
app.use(TList);
|
|
||||||
app.use(TAlert);
|
|
||||||
app.use(TTag);
|
|
||||||
app.use(TDescriptionsProps);
|
|
||||||
app.use(TDescriptionsItem);
|
|
||||||
app.use(TCollapse);
|
|
||||||
app.use(TCollapsePanel);
|
|
||||||
app.use(TListItem);
|
|
||||||
app.use(TTabs);
|
|
||||||
app.use(TTabPanel);
|
|
||||||
app.use(TSpace);
|
|
||||||
app.use(TCheckbox);
|
|
||||||
app.use(TPopup);
|
|
||||||
app.use(TDialog);
|
|
||||||
app.use(TSwitch);
|
|
||||||
app.use(Tooltip);
|
|
||||||
app.use(TStickyTool);
|
|
||||||
app.use(TStickyItem);
|
|
||||||
app.use(TLayout);
|
|
||||||
app.use(TContent);
|
|
||||||
app.use(TFooter);
|
|
||||||
app.use(TAside);
|
|
||||||
app.use(Tpopconfirm);
|
|
||||||
app.use(TEmpty);
|
|
||||||
app.use(TDropdown);
|
|
||||||
app.use(TTypographyText);
|
|
||||||
app.use(TTreeSelect);
|
|
||||||
app.use(TLoading);
|
|
||||||
app.use(THeadMenu);
|
|
||||||
app.mount('#app');
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about-us">
|
|
||||||
<div>
|
|
||||||
<t-divider content="面板关于信息" align="left">
|
|
||||||
<template #content>
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center">
|
|
||||||
<info-circle-icon></info-circle-icon>
|
|
||||||
<div style="margin-left: 5px">面板关于信息</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-divider>
|
|
||||||
<t-alert theme="success" class="header" message="NapCat.WebUi is running" />
|
|
||||||
<t-list>
|
|
||||||
<t-list-item>
|
|
||||||
<div class="label-box">
|
|
||||||
<star-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Star:</span>
|
|
||||||
</div>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/stargazers">{{
|
|
||||||
githubBastData?.stargazers_count
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<tips-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">issues:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
|
|
||||||
githubBastData?.open_issues_count
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<git-pull-request-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Pull Requests:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/pulls">{{githubPullData?.length
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item >
|
|
||||||
<bookmark-add-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Releases:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/releases">{{
|
|
||||||
githubReleasesData&&githubReleasesData[0]?timeDifference(githubReleasesData[0].published_at) + '前更新':''
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<usergroup-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Contributors:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/graphs/contributors">{{githubContributorsData?.length}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<browse-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Watchers:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/watchers">{{
|
|
||||||
githubBastData?.watchers
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<fork-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Fork:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/fork">{{
|
|
||||||
githubBastData?.forks_count
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<statue-of-jesus-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">License:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ#License-1-ov-file">{{
|
|
||||||
githubBastData?.license.key
|
|
||||||
}}</t-link>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
<t-list-item>
|
|
||||||
<component-layout-filled-icon class="item-icon" size="large" />
|
|
||||||
<span class="item-label">Version:</span>
|
|
||||||
<span class="item-content">
|
|
||||||
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
|
|
||||||
<t-tag class="tag-item nc-color">
|
|
||||||
NapCat:
|
|
||||||
{{ napCatVersion }}
|
|
||||||
</t-tag>
|
|
||||||
<t-tag v-if="githubReleasesData&&githubReleasesData[0] ?.tag_name" class="tag-item nc-color">
|
|
||||||
New NapCat:
|
|
||||||
{{ githubReleasesData[0].tag_name }}
|
|
||||||
</t-tag>
|
|
||||||
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
|
|
||||||
</span>
|
|
||||||
</t-list-item>
|
|
||||||
</t-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import pkg from '../../package.json';
|
|
||||||
import { napCatVersion } from '../../../src/common/version';
|
|
||||||
import {
|
|
||||||
InfoCircleIcon,
|
|
||||||
TipsFilledIcon,
|
|
||||||
StarFilledIcon,
|
|
||||||
GitPullRequestFilledIcon,
|
|
||||||
ForkFilledIcon,
|
|
||||||
StatueOfJesusFilledIcon,
|
|
||||||
BookmarkAddFilledIcon,
|
|
||||||
UsergroupFilledIcon,
|
|
||||||
BrowseFilledIcon,
|
|
||||||
ComponentLayoutFilledIcon,
|
|
||||||
} from 'tdesign-icons-vue-next';
|
|
||||||
import { githubApiManager } from '@/backend/githubApi';
|
|
||||||
import { onMounted, ref } from 'vue';
|
|
||||||
const githubApi = new githubApiManager();
|
|
||||||
const githubBastData = ref<any>(null);
|
|
||||||
const githubReleasesData = ref<any>(null);
|
|
||||||
const githubContributorsData = ref<any>(null);
|
|
||||||
const githubPullData = ref<any>(null);
|
|
||||||
const getBaseData = async () => {
|
|
||||||
githubBastData.value = await githubApi.GetBaseData();
|
|
||||||
githubReleasesData.value = await githubApi.GetReleasesData();
|
|
||||||
githubContributorsData.value = await githubApi.GetContributors();
|
|
||||||
githubPullData.value = await githubApi.GetPullsData();
|
|
||||||
};
|
|
||||||
const timeDifference = (timestamp: string): string => {
|
|
||||||
const givenTime = new Date(timestamp);
|
|
||||||
const currentTime = new Date();
|
|
||||||
const diffInMilliseconds = currentTime.getTime() - givenTime.getTime();
|
|
||||||
|
|
||||||
const seconds = Math.floor(diffInMilliseconds / 1000);
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}小时`;
|
|
||||||
} else if (minutes > 0) {
|
|
||||||
return `${minutes}分钟`;
|
|
||||||
} else {
|
|
||||||
return `${seconds}秒`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
getBaseData();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.about-us {
|
|
||||||
padding: 20px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.label-box {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.item-icon {
|
|
||||||
padding: 5px;
|
|
||||||
color: #ffffff;
|
|
||||||
border-radius: 3px;
|
|
||||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
|
||||||
}
|
|
||||||
.item-label {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: auto;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.item-content {
|
|
||||||
flex: 2;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-item {
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.t-list-item {
|
|
||||||
padding: 5px var(--td-comp-paddingLR-l);
|
|
||||||
}
|
|
||||||
.item-label {
|
|
||||||
flex: 2;
|
|
||||||
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
.pgk-color {
|
|
||||||
color: white;
|
|
||||||
background-image: linear-gradient(-225deg, #9be15d 0%, #00e3ae 100%);
|
|
||||||
}
|
|
||||||
.nc-color {
|
|
||||||
color: white;
|
|
||||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
|
||||||
}
|
|
||||||
.td-color {
|
|
||||||
color: white;
|
|
||||||
background-image: linear-gradient(225deg, #0acffe 0%, #495aff 100%);
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background-image: linear-gradient(225deg, #dfffcd 0%, #90f9c4 48%, #39f3bb 100%) !important;
|
|
||||||
}
|
|
||||||
.link-text{
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(-225deg, #B6CEE8 0%, #F578DC 100%);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="basic-info">
|
|
||||||
<h1>面板基础信息</h1>
|
|
||||||
<p>这里显示面板的基础信息。</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="title">
|
|
||||||
<t-divider content="日志查看" align="left">
|
|
||||||
<template #content>
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center">
|
|
||||||
<system-log-icon></system-log-icon>
|
|
||||||
<div style="margin-left: 5px">日志查看</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-divider>
|
|
||||||
</div>
|
|
||||||
<div class="tab-box">
|
|
||||||
<t-tabs default-value="realtime" @change="selectType">
|
|
||||||
<t-tab-panel value="realtime" label="实时日志"></t-tab-panel>
|
|
||||||
<t-tab-panel value="history" label="历史日志"></t-tab-panel>
|
|
||||||
</t-tabs>
|
|
||||||
</div>
|
|
||||||
<div class="card-box">
|
|
||||||
<t-card class="card" :bordered="true">
|
|
||||||
<template #actions>
|
|
||||||
<t-row :align="'middle'" justify="center" :style="{ gap: smallScreen.matches ? '5px' : '24px' }">
|
|
||||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
|
||||||
<t-tooltip content="清理日志">
|
|
||||||
<t-button variant="text" shape="square" @click="clearLogs">
|
|
||||||
<clear-icon></clear-icon>
|
|
||||||
</t-button>
|
|
||||||
</t-tooltip>
|
|
||||||
</t-col>
|
|
||||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
|
||||||
<t-tooltip content="下载日志">
|
|
||||||
<t-button variant="text" shape="square" @click="downloadText">
|
|
||||||
<download-icon></download-icon>
|
|
||||||
</t-button>
|
|
||||||
</t-tooltip>
|
|
||||||
</t-col>
|
|
||||||
<t-col
|
|
||||||
v-if="LogDataType === 'history'"
|
|
||||||
flex="auto"
|
|
||||||
style="display: inline-flex; justify-content: center">
|
|
||||||
<t-tooltip content="历史日志">
|
|
||||||
<t-button variant="text" shape="square" @click="historyLog">
|
|
||||||
<history-icon></history-icon>
|
|
||||||
</t-button>
|
|
||||||
</t-tooltip>
|
|
||||||
</t-col>
|
|
||||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
|
||||||
<div class="tag-box">
|
|
||||||
<t-tag class="t-tag" :style="{ backgroundImage: typeKey[optValue.description] }">{{
|
|
||||||
optValue.content }}</t-tag>
|
|
||||||
</div>
|
|
||||||
<t-dropdown :options="options" :min-column-width="112" @click="openTypeList">
|
|
||||||
<t-button variant="text" shape="square">
|
|
||||||
<more-icon />
|
|
||||||
</t-button>
|
|
||||||
</t-dropdown>
|
|
||||||
</t-col>
|
|
||||||
</t-row>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="content" ref="contentBox">
|
|
||||||
<div v-for="item in LogDataType === 'realtime'
|
|
||||||
? realtimeLogHtmlList.get(optValue.description)
|
|
||||||
: historyLogHtmlList.get(optValue.description)">
|
|
||||||
<span>{{ item.time }}</span><span :id="item.type">{{ item.content }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-card>
|
|
||||||
</div>
|
|
||||||
<t-dialog v-model:visible="visibleBody" header="历史日志" :destroy-on-close="true" :show-in-attached-element="true"
|
|
||||||
:on-confirm="GetLogList" class=".t-dialog__ctx .t-dialog__position">
|
|
||||||
<t-select v-model="value" :options="logFileData" placeholder="请选择日志" :multiple="true"
|
|
||||||
style="text-align: left" />
|
|
||||||
</t-dialog>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { MoreIcon, ClearIcon, DownloadIcon, HistoryIcon, SystemLogIcon } from 'tdesign-icons-vue-next';
|
|
||||||
import { nextTick, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
|
||||||
import { LogManager } from '@/backend/log';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
|
||||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
|
||||||
const LogDataType = ref<string>('realtime');
|
|
||||||
const visibleBody = ref<boolean>(false);
|
|
||||||
const contentBox = ref<HTMLElement | null>(null);
|
|
||||||
let isMouseEntered = false;
|
|
||||||
const logManager = new LogManager(localStorage.getItem('auth') || '');
|
|
||||||
const eventSource = ref<EventSourcePolyfill | null>(null);
|
|
||||||
const intervalId = ref<number | null>(null);
|
|
||||||
const isPaused = ref(false);
|
|
||||||
interface OptionItem {
|
|
||||||
content: string;
|
|
||||||
value: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
const options = ref<OptionItem[]>([
|
|
||||||
{
|
|
||||||
content: '全部',
|
|
||||||
value: 1,
|
|
||||||
description: 'all',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '调试',
|
|
||||||
value: 2,
|
|
||||||
description: 'debug',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '提示',
|
|
||||||
value: 3,
|
|
||||||
description: 'info',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '警告',
|
|
||||||
value: 4,
|
|
||||||
description: 'warn',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '错误',
|
|
||||||
value: 5,
|
|
||||||
description: 'error',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '致命',
|
|
||||||
value: 5,
|
|
||||||
description: 'fatal',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const typeKey = ref<Record<string, string>>({
|
|
||||||
all: 'linear-gradient(60deg,#16a085 0%, #f4d03f 100%)',
|
|
||||||
debug: 'linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%)',
|
|
||||||
info: 'linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%)',
|
|
||||||
warn: 'linear-gradient(to right, #e14fad 0%, #f9d423 48%, #e37318 100%)',
|
|
||||||
error: 'linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%)',
|
|
||||||
fatal: 'linear-gradient(-225deg, #fd0700, #ec567f)',
|
|
||||||
});
|
|
||||||
interface logHtml {
|
|
||||||
type?: string;
|
|
||||||
content: string;
|
|
||||||
color?: string;
|
|
||||||
time?: string;
|
|
||||||
}
|
|
||||||
type LogHtmlMap = Map<string, logHtml[]>;
|
|
||||||
const realtimeLogHtmlList = ref<LogHtmlMap>(
|
|
||||||
new Map([
|
|
||||||
['all', []],
|
|
||||||
['debug', []],
|
|
||||||
['info', []],
|
|
||||||
['warn', []],
|
|
||||||
['error', []],
|
|
||||||
['fatal', []],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
const historyLogHtmlList = ref<LogHtmlMap>(
|
|
||||||
new Map([
|
|
||||||
['all', []],
|
|
||||||
['debug', []],
|
|
||||||
['info', []],
|
|
||||||
['warn', []],
|
|
||||||
['error', []],
|
|
||||||
['fatal', []],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
const logFileData = ref<{ label: string; value: string }[]>([]);
|
|
||||||
const value = ref([]);
|
|
||||||
const optValue = ref<OptionItem>({
|
|
||||||
content: '全部',
|
|
||||||
value: 1,
|
|
||||||
description: 'all',
|
|
||||||
});
|
|
||||||
const openTypeList = (data: OptionItem) => {
|
|
||||||
optValue.value = data;
|
|
||||||
};
|
|
||||||
const logType = ['debug', 'info', 'warn', 'error', 'fatal'];
|
|
||||||
//清理log
|
|
||||||
const clearLogs = () => {
|
|
||||||
if (LogDataType.value === 'realtime') {
|
|
||||||
clearAllLogs(realtimeLogHtmlList);
|
|
||||||
} else {
|
|
||||||
clearAllLogs(historyLogHtmlList);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const clearAllLogs = (logList: Ref<Map<string, Array<logHtml>>>) => {
|
|
||||||
if ((optValue.value && optValue.value.description === 'all') || !optValue.value) {
|
|
||||||
logList.value = new Map([
|
|
||||||
['all', []],
|
|
||||||
['debug', []],
|
|
||||||
['info', []],
|
|
||||||
['warn', []],
|
|
||||||
['error', []],
|
|
||||||
['fatal', []],
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
logList.value.set(optValue.value.description, []);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
//定时清理log
|
|
||||||
|
|
||||||
const TimerClear = () => {
|
|
||||||
clearAllLogs(realtimeLogHtmlList);
|
|
||||||
};
|
|
||||||
const startTimer = () => {
|
|
||||||
if (!isPaused.value) {
|
|
||||||
intervalId.value = window.setInterval(TimerClear, 0.5 * 60 * 1000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const pauseTimer = () => {
|
|
||||||
if (intervalId.value) {
|
|
||||||
window.clearInterval(intervalId.value);
|
|
||||||
isPaused.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const resumeTimer = () => {
|
|
||||||
if (isPaused.value) {
|
|
||||||
startTimer();
|
|
||||||
isPaused.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const stopTimer = () => {
|
|
||||||
if (intervalId.value) {
|
|
||||||
window.clearInterval(intervalId.value);
|
|
||||||
intervalId.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const extractContent = (text: string): string | null => {
|
|
||||||
const regex = /\[([^\]]+)]/;
|
|
||||||
const match = regex.exec(text);
|
|
||||||
if (match && match[1]) {
|
|
||||||
const extracted = match[1].toLowerCase();
|
|
||||||
if (logType.includes(extracted)) {
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
const loadData = (text: string, loadType: string) => {
|
|
||||||
const lines = text.split(/\r\n/);
|
|
||||||
lines.forEach((line) => {
|
|
||||||
if (loadType === 'realtime') {
|
|
||||||
let remoteJson = JSON.parse(line) as { message: string, level: string };
|
|
||||||
const type = remoteJson.level;
|
|
||||||
const actualType = type || 'other';
|
|
||||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
|
||||||
const data: logHtml = {
|
|
||||||
type: actualType,
|
|
||||||
content: remoteJson.message,
|
|
||||||
color: color,
|
|
||||||
time: '',
|
|
||||||
};
|
|
||||||
updateLogList(realtimeLogHtmlList, actualType, data);
|
|
||||||
} else if (loadType === 'history') {
|
|
||||||
const type = extractContent(line);
|
|
||||||
const actualType = type || 'other';
|
|
||||||
const timeRegex = /(\d{2}-\d{2} \d{2}:\d{2}:\d{2})|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
|
||||||
const match = timeRegex.exec(line);
|
|
||||||
let time = match ? match[0] : null;
|
|
||||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
|
||||||
const data: logHtml = {
|
|
||||||
type: actualType,
|
|
||||||
content: line.slice(match ? match[0].length : 0) || '',
|
|
||||||
color: color,
|
|
||||||
time: time ? time + ' ' : '',
|
|
||||||
};
|
|
||||||
updateLogList(historyLogHtmlList, actualType, data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateLogList = (logList: Ref<Map<string, Array<logHtml>>>, actualType: string, data: logHtml) => {
|
|
||||||
const allLogs = logList.value.get('all');
|
|
||||||
if (Array.isArray(allLogs)) {
|
|
||||||
allLogs.push(data);
|
|
||||||
}
|
|
||||||
if (actualType !== 'other') {
|
|
||||||
const typeLogs = logList.value.get(actualType);
|
|
||||||
if (Array.isArray(typeLogs)) {
|
|
||||||
typeLogs.push(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const selectType = (key: string) => {
|
|
||||||
LogDataType.value = key;
|
|
||||||
};
|
|
||||||
interface CustomURL extends URL {
|
|
||||||
recycleObjectURL: (url: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCompatibleWithCustomURL = (obj: any): obj is CustomURL => {
|
|
||||||
return typeof obj === 'object' && obj !== null && typeof (obj as any).recycleObjectURL === 'function';
|
|
||||||
};
|
|
||||||
|
|
||||||
const recycleURL = (url: string) => {
|
|
||||||
if (isCompatibleWithCustomURL(window.URL)) {
|
|
||||||
const customURL = window.URL as CustomURL;
|
|
||||||
customURL.recycleObjectURL(url);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const generateTXT = (textContent: string, fileName: string) => {
|
|
||||||
try {
|
|
||||||
const blob = new Blob([textContent], { type: 'text/plain' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = fileName;
|
|
||||||
a.click();
|
|
||||||
recycleURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('下载文本时出现错误:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const downloadText = () => {
|
|
||||||
if (LogDataType.value === 'realtime') {
|
|
||||||
const logs = realtimeLogHtmlList.value.get(optValue.value.description);
|
|
||||||
if (logs && logs.length > 0) {
|
|
||||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
|
||||||
generateTXT(result, '实时日志');
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('暂无可下载日志');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const logs = historyLogHtmlList.value.get(optValue.value.description);
|
|
||||||
if (logs && logs.length > 0) {
|
|
||||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
|
||||||
generateTXT(result, '历史日志');
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('暂无可下载日志');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const historyLog = async () => {
|
|
||||||
value.value = [];
|
|
||||||
visibleBody.value = true;
|
|
||||||
const res = await logManager.GetLogList();
|
|
||||||
clearAllLogs(historyLogHtmlList);
|
|
||||||
if (res.length > 0) {
|
|
||||||
logFileData.value = res.map((ele: string) => {
|
|
||||||
return { label: ele, value: ele };
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logFileData.value = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const GetLogList = async () => {
|
|
||||||
if (value.value.length > 0) {
|
|
||||||
for (const ele of value.value) {
|
|
||||||
try {
|
|
||||||
const data = await logManager.GetLog(ele);
|
|
||||||
if (data && data !== 'null') {
|
|
||||||
loadData(data, 'history');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`获取日志 ${ele} 时出现错误:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visibleBody.value = false;
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('请选择日志');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchRealTimeLogs = async () => {
|
|
||||||
eventSource.value = await logManager.getRealTimeLogs();
|
|
||||||
if (eventSource.value) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-expect-error
|
|
||||||
eventSource.value.onmessage = (event: MessageEvent) => {
|
|
||||||
console.log(event.data)
|
|
||||||
loadData(event.data, 'realtime');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const closeRealTimeLogs = async () => {
|
|
||||||
if (eventSource.value) {
|
|
||||||
eventSource.value.close();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (!isMouseEntered) {
|
|
||||||
nextTick(() => {
|
|
||||||
if (contentBox.value) {
|
|
||||||
contentBox.value.scrollTop = contentBox.value.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observeDOMChanges = () => {
|
|
||||||
if (contentBox.value) {
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
});
|
|
||||||
observer.observe(contentBox.value, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const showScrollbar = () => {
|
|
||||||
if (contentBox.value) {
|
|
||||||
contentBox.value.style.overflow = 'auto';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const hideScrollbar = () => {
|
|
||||||
if (contentBox.value) {
|
|
||||||
contentBox.value.style.overflow = 'hidden';
|
|
||||||
if (!isMouseEntered) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
realtimeLogHtmlList,
|
|
||||||
() => {
|
|
||||||
if (!isMouseEntered) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
historyLogHtmlList,
|
|
||||||
() => {
|
|
||||||
if (!isMouseEntered) {
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchRealTimeLogs();
|
|
||||||
startTimer();
|
|
||||||
contentBox.value = document.querySelector('.content');
|
|
||||||
if (contentBox.value) {
|
|
||||||
contentBox.value.style.overflow = 'hidden';
|
|
||||||
contentBox.value.addEventListener('mouseenter', () => {
|
|
||||||
isMouseEntered = true;
|
|
||||||
showScrollbar();
|
|
||||||
pauseTimer();
|
|
||||||
});
|
|
||||||
contentBox.value.addEventListener('mouseleave', () => {
|
|
||||||
isMouseEntered = false;
|
|
||||||
hideScrollbar();
|
|
||||||
resumeTimer();
|
|
||||||
setTimeout(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
observeDOMChanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
closeRealTimeLogs();
|
|
||||||
stopTimer();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.title {
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-box {
|
|
||||||
margin: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-box {
|
|
||||||
margin: 10px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
height: 56vh;
|
|
||||||
background-image: url('@/assets/logo.png');
|
|
||||||
border: 1px solid #ddd6d6 !important;
|
|
||||||
padding: 5px 10px;
|
|
||||||
text-align: left;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin-top: -10px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content span {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInOnce {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOutOnce {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content div {
|
|
||||||
animation: fadeInOnce 0.5s forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #888888;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-box {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.t-tag {
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
#debug {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#info {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#warn {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(225deg, #e14fad 0%, #f9d423 48%, #e37318 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#error {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#fatal {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(to right, #fd0700, #ec567f);
|
|
||||||
}
|
|
||||||
|
|
||||||
#other {
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 786px) {
|
|
||||||
.content {
|
|
||||||
height: 50vh;
|
|
||||||
font-family: ProtoNerdFontItalic, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 14.3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.card {
|
|
||||||
padding: 5px 10px 20px 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 786px) {
|
|
||||||
.card {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,654 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="headerBox" class="title">
|
|
||||||
<t-divider content="网络配置" align="left">
|
|
||||||
<template #content>
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center">
|
|
||||||
<wifi1-icon />
|
|
||||||
<div style="margin-left: 5px">网络配置</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-divider>
|
|
||||||
<t-divider align="right">
|
|
||||||
<t-button @click="addConfig()">
|
|
||||||
<template #icon><add-icon /></template>
|
|
||||||
添加配置</t-button
|
|
||||||
>
|
|
||||||
</t-divider>
|
|
||||||
</div>
|
|
||||||
<div v-if="loadPage" ref="setting" class="setting">
|
|
||||||
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
|
|
||||||
<t-tab-panel value="all" label="全部"></t-tab-panel>
|
|
||||||
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
|
|
||||||
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
|
|
||||||
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
|
|
||||||
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
|
||||||
</t-tabs>
|
|
||||||
</div>
|
|
||||||
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
|
|
||||||
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative" ></div>
|
|
||||||
</t-loading>
|
|
||||||
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
|
|
||||||
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
|
|
||||||
<div v-for="(item, index) in cardConfig" :key="index">
|
|
||||||
<t-card
|
|
||||||
:title="item.name"
|
|
||||||
:description="item.type"
|
|
||||||
:style="{ width: cardWidth + 'px' }"
|
|
||||||
:header-bordered="true"
|
|
||||||
class="setting-card"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<t-space>
|
|
||||||
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
|
|
||||||
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
|
|
||||||
<delete-icon size="20px"></delete-icon>
|
|
||||||
</t-popconfirm>
|
|
||||||
</t-space>
|
|
||||||
</template>
|
|
||||||
<div class="setting-content">
|
|
||||||
<t-card
|
|
||||||
class="card-address"
|
|
||||||
:style="{
|
|
||||||
borderLeft:
|
|
||||||
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="local-box" v-if="item.host && item.port">
|
|
||||||
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
|
||||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
|
||||||
<copy-icon
|
|
||||||
class="copy-icon"
|
|
||||||
size="20px"
|
|
||||||
@click="copyText(item.host + ':' + item.port)"
|
|
||||||
></copy-icon>
|
|
||||||
</div>
|
|
||||||
<div class="local-box" v-if="item.url">
|
|
||||||
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
|
|
||||||
<strong class="local">{{ item.url }}</strong>
|
|
||||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
|
||||||
</div>
|
|
||||||
</t-card>
|
|
||||||
<t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
|
|
||||||
<t-collapse-panel header="基础信息">
|
|
||||||
<t-descriptions
|
|
||||||
size="small"
|
|
||||||
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
|
||||||
class="setting-base-info"
|
|
||||||
>
|
|
||||||
<t-descriptions-item v-if="item.token" label="连接密钥">
|
|
||||||
<div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
|
|
||||||
<span>{{ showToken ? item.token : '******' }}</span>
|
|
||||||
<browse-icon
|
|
||||||
class="browse-icon"
|
|
||||||
v-if="showToken"
|
|
||||||
size="18px"
|
|
||||||
@click="showToken = false"
|
|
||||||
></browse-icon>
|
|
||||||
<browse-off-icon
|
|
||||||
class="browse-icon"
|
|
||||||
v-else
|
|
||||||
size="18px"
|
|
||||||
@click="showToken = true"
|
|
||||||
></browse-off-icon>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<t-popup :showArrow="true" trigger="click">
|
|
||||||
<t-tag theme="primary">点击查看</t-tag>
|
|
||||||
<template #content>
|
|
||||||
<div @click="copyText(item.token)">{{ item.token }}</div>
|
|
||||||
</template>
|
|
||||||
</t-popup>
|
|
||||||
</div>
|
|
||||||
</t-descriptions-item>
|
|
||||||
<t-descriptions-item label="消息格式">{{
|
|
||||||
item.messagePostFormat
|
|
||||||
}}</t-descriptions-item>
|
|
||||||
</t-descriptions>
|
|
||||||
</t-collapse-panel>
|
|
||||||
<t-collapse-panel header="状态信息">
|
|
||||||
<t-descriptions
|
|
||||||
size="small"
|
|
||||||
:layout="infoOneCol ? 'vertical' : 'horizontal'"
|
|
||||||
class="setting-base-info"
|
|
||||||
>
|
|
||||||
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
|
||||||
<t-tag
|
|
||||||
:class="item.debug ? 'tag-item-on' : 'tag-item-off'"
|
|
||||||
@click="toggleProperty(item, 'debug')"
|
|
||||||
>
|
|
||||||
{{ item.debug ? '开启' : '关闭' }}</t-tag
|
|
||||||
>
|
|
||||||
</t-descriptions-item>
|
|
||||||
<t-descriptions-item
|
|
||||||
v-if="item.hasOwnProperty('enableWebsocket')"
|
|
||||||
label="Websocket 功能"
|
|
||||||
>
|
|
||||||
<t-tag
|
|
||||||
:class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
|
|
||||||
@click="toggleProperty(item, 'enableWebsocket')"
|
|
||||||
>
|
|
||||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag
|
|
||||||
>
|
|
||||||
</t-descriptions-item>
|
|
||||||
<t-descriptions-item
|
|
||||||
v-if="item.hasOwnProperty('enableCors')"
|
|
||||||
label="跨域放行"
|
|
||||||
>
|
|
||||||
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
|
|
||||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag
|
|
||||||
>
|
|
||||||
</t-descriptions-item>
|
|
||||||
<t-descriptions-item
|
|
||||||
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
|
||||||
label="上报自身消息"
|
|
||||||
>
|
|
||||||
<t-tag
|
|
||||||
:class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
|
|
||||||
@click="toggleProperty(item, 'reportSelfMessage')"
|
|
||||||
>
|
|
||||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag
|
|
||||||
>
|
|
||||||
</t-descriptions-item>
|
|
||||||
<t-descriptions-item
|
|
||||||
v-if="item.hasOwnProperty('enableForcePushEvent')"
|
|
||||||
label="强制推送事件"
|
|
||||||
>
|
|
||||||
<t-tag
|
|
||||||
class="tag-item"
|
|
||||||
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
|
|
||||||
@click="toggleProperty(item, 'enableForcePushEvent')"
|
|
||||||
>
|
|
||||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag
|
|
||||||
>
|
|
||||||
</t-descriptions-item>
|
|
||||||
</t-descriptions>
|
|
||||||
</t-collapse-panel>
|
|
||||||
</t-collapse>
|
|
||||||
</div>
|
|
||||||
</t-card>
|
|
||||||
</div>
|
|
||||||
<div style="height: 20vh"></div>
|
|
||||||
</div>
|
|
||||||
<t-card v-else>
|
|
||||||
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
|
||||||
</t-card>
|
|
||||||
</div>
|
|
||||||
<t-dialog
|
|
||||||
v-model:visible="visibleBody"
|
|
||||||
:header="dialogTitle"
|
|
||||||
:destroy-on-close="true"
|
|
||||||
:show-in-attached-element="true"
|
|
||||||
:on-confirm="saveConfig"
|
|
||||||
class=".t-dialog__ctx .t-dialog__position"
|
|
||||||
>
|
|
||||||
<div slot="body" class="dialog-body">
|
|
||||||
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
|
|
||||||
<t-form-item
|
|
||||||
style="text-align: left"
|
|
||||||
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
|
|
||||||
label="名称"
|
|
||||||
name="name"
|
|
||||||
>
|
|
||||||
<t-input v-model="newTab.name" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item
|
|
||||||
style="text-align: left"
|
|
||||||
:rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
|
||||||
label="类型"
|
|
||||||
name="type"
|
|
||||||
>
|
|
||||||
<t-select v-model="newTab.type" @change="onloadDefault">
|
|
||||||
<t-option value="httpServers">HTTP 服务器</t-option>
|
|
||||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
|
||||||
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
|
||||||
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
|
||||||
</t-select>
|
|
||||||
</t-form-item>
|
|
||||||
<div>
|
|
||||||
<component
|
|
||||||
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
|
||||||
:config="newTab.data"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</t-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
AddIcon,
|
|
||||||
DeleteIcon,
|
|
||||||
Edit2Icon,
|
|
||||||
ServerFilledIcon,
|
|
||||||
CopyIcon,
|
|
||||||
BrowseOffIcon,
|
|
||||||
BrowseIcon,
|
|
||||||
Wifi1Icon,
|
|
||||||
} from 'tdesign-icons-vue-next';
|
|
||||||
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
|
|
||||||
import emitter from '@/ts/event-bus';
|
|
||||||
import {
|
|
||||||
mergeNetworkDefaultConfig,
|
|
||||||
mergeOneBotConfigs,
|
|
||||||
NetworkConfig,
|
|
||||||
OneBotConfig,
|
|
||||||
} from '../../../src/onebot/config/config';
|
|
||||||
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
|
||||||
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
|
|
||||||
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
|
|
||||||
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
|
|
||||||
const showToken = ref<boolean>(false);
|
|
||||||
const infoOneCol = ref<boolean>(true);
|
|
||||||
const tabsWidth = ref<number>(0);
|
|
||||||
const menuWidth = ref<number>(0);
|
|
||||||
const cardWidth = ref<number>(0);
|
|
||||||
const cardHeight = ref<number>(0);
|
|
||||||
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
|
|
||||||
const largeScreen = window.matchMedia('(min-width: 1025px)');
|
|
||||||
const headerBox = ref<HTMLDivElement | null>(null);
|
|
||||||
const setting = ref<HTMLDivElement | null>(null);
|
|
||||||
const loadPage = ref<boolean>(false);
|
|
||||||
const visibleBody = ref<boolean>(false);
|
|
||||||
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
|
|
||||||
const dialogTitle = ref<string>('');
|
|
||||||
|
|
||||||
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
|
|
||||||
|
|
||||||
const componentMap: Record<
|
|
||||||
ComponentKey,
|
|
||||||
| typeof HttpServerComponent
|
|
||||||
| typeof HttpClientComponent
|
|
||||||
| typeof WebsocketServerComponent
|
|
||||||
| typeof WebsocketClientComponent
|
|
||||||
> = {
|
|
||||||
httpServers: HttpServerComponent,
|
|
||||||
httpClients: HttpClientComponent,
|
|
||||||
websocketServers: WebsocketServerComponent,
|
|
||||||
websocketClients: WebsocketClientComponent,
|
|
||||||
};
|
|
||||||
|
|
||||||
//操作类型
|
|
||||||
const operateType = ref<string>('');
|
|
||||||
//配置项索引
|
|
||||||
const configIndex = ref<number>(0);
|
|
||||||
//保存时所用数据
|
|
||||||
const networkConfig: NetworkConfig & { [key: string]: any } = {
|
|
||||||
websocketClients: [],
|
|
||||||
websocketServers: [],
|
|
||||||
httpClients: [],
|
|
||||||
httpServers: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
//挂载的数据
|
|
||||||
const WebConfg = ref(
|
|
||||||
new Map<string, Array<null>>([
|
|
||||||
['all', []],
|
|
||||||
['httpServers', []],
|
|
||||||
['httpClients', []],
|
|
||||||
['websocketServers', []],
|
|
||||||
['websocketClients', []],
|
|
||||||
])
|
|
||||||
);
|
|
||||||
const typeCh: Record<ComponentKey, string> = {
|
|
||||||
httpServers: 'HTTP 服务器',
|
|
||||||
httpClients: 'HTTP 客户端',
|
|
||||||
websocketServers: 'WebSocket 服务器',
|
|
||||||
websocketClients: 'WebSocket 客户端',
|
|
||||||
};
|
|
||||||
const cardConfig = ref<any>([]);
|
|
||||||
const getComponent = (type: ComponentKey) => {
|
|
||||||
return componentMap[type];
|
|
||||||
};
|
|
||||||
const getKeyByValue = (obj: typeof typeCh, value: string): string | undefined => {
|
|
||||||
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
const addConfig = () => {
|
|
||||||
dialogTitle.value = '添加配置';
|
|
||||||
newTab.value = { name: '', data: {}, type: '' };
|
|
||||||
operateType.value = 'add';
|
|
||||||
visibleBody.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const editConfig = (item: any) => {
|
|
||||||
dialogTitle.value = '修改配置';
|
|
||||||
const type = getKeyByValue(typeCh, item.type);
|
|
||||||
if (type) {
|
|
||||||
newTab.value = { name: item.name, data: item, type: type };
|
|
||||||
}
|
|
||||||
operateType.value = 'edit';
|
|
||||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
|
||||||
visibleBody.value = true;
|
|
||||||
};
|
|
||||||
const toggleProperty = async (item: any, tagData: string) => {
|
|
||||||
const type = getKeyByValue(typeCh, item.type);
|
|
||||||
const newData = { ...item };
|
|
||||||
newData[tagData] = !item[tagData];
|
|
||||||
if (type) {
|
|
||||||
newTab.value = { name: item.name, data: newData, type: type };
|
|
||||||
}
|
|
||||||
operateType.value = 'edit';
|
|
||||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
|
||||||
await saveConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
const delConfig = (item: any) => {
|
|
||||||
const type = getKeyByValue(typeCh, item.type);
|
|
||||||
if (type) {
|
|
||||||
newTab.value = { name: item.name, data: item, type: type };
|
|
||||||
}
|
|
||||||
configIndex.value = configIndex.value = networkConfig[newTab.value.type].findIndex(
|
|
||||||
(obj: any) => obj.name === item.name
|
|
||||||
);
|
|
||||||
operateType.value = 'delete';
|
|
||||||
saveConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectType = (key: ComponentKey) => {
|
|
||||||
cardConfig.value = WebConfg.value.get(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onloadDefault = (key: ComponentKey) => {
|
|
||||||
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
|
||||||
};
|
|
||||||
//检测重名
|
|
||||||
const checkName = (name: string) => {
|
|
||||||
const allConfigs = WebConfg.value.get('all')?.findIndex((obj: any) => obj.name === name);
|
|
||||||
if (newTab.value.name === '' || newTab.value.type === '') {
|
|
||||||
MessagePlugin.error('请填写完整信息');
|
|
||||||
return false;
|
|
||||||
} else if (allConfigs === -1 || newTab.value.data.name === name) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('名称已存在');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
//保存
|
|
||||||
const saveConfig = async () => {
|
|
||||||
if (operateType.value == 'add') {
|
|
||||||
if (!checkName(newTab.value.name)) return;
|
|
||||||
newTab.value.data.name = newTab.value.name;
|
|
||||||
networkConfig[newTab.value.type].push(newTab.value.data);
|
|
||||||
} else if (operateType.value == 'edit') {
|
|
||||||
if (!checkName(newTab.value.name)) return;
|
|
||||||
newTab.value.data.name = newTab.value.name;
|
|
||||||
networkConfig[newTab.value.type][configIndex.value] = newTab.value.data;
|
|
||||||
} else if (operateType.value == 'delete') {
|
|
||||||
networkConfig[newTab.value.type].splice(configIndex.value, 1);
|
|
||||||
}
|
|
||||||
const userConfig = await getOB11Config();
|
|
||||||
if (!userConfig) return;
|
|
||||||
userConfig.network = networkConfig;
|
|
||||||
const success = await setOB11Config(userConfig);
|
|
||||||
if (success) {
|
|
||||||
operateType.value = '';
|
|
||||||
configIndex.value = 0;
|
|
||||||
MessagePlugin.success('配置保存成功');
|
|
||||||
await loadConfig();
|
|
||||||
visibleBody.value = false;
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('配置保存失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
|
||||||
const storedCredential = localStorage.getItem('auth');
|
|
||||||
if (!storedCredential) {
|
|
||||||
console.error('No stored credential found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loginManager = new QQLoginManager(storedCredential);
|
|
||||||
return await loginManager.GetOB11Config();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
|
||||||
const storedCredential = localStorage.getItem('auth');
|
|
||||||
if (!storedCredential) {
|
|
||||||
console.error('No stored credential found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const loginManager = new QQLoginManager(storedCredential);
|
|
||||||
return await loginManager.SetOB11Config(config);
|
|
||||||
};
|
|
||||||
|
|
||||||
//获取卡片数据
|
|
||||||
const getAllData = (data: NetworkConfig) => {
|
|
||||||
cardConfig.value = [];
|
|
||||||
WebConfg.value.set('all', []);
|
|
||||||
for (const key in data) {
|
|
||||||
const configs = data[key as keyof NetworkConfig];
|
|
||||||
if (key in mergeNetworkDefaultConfig) {
|
|
||||||
networkConfig[key] = [...configs];
|
|
||||||
const newConfigsArray = configs.map((config: any) => ({
|
|
||||||
...config,
|
|
||||||
type: typeCh[key as ComponentKey],
|
|
||||||
}));
|
|
||||||
WebConfg.value.set(key, newConfigsArray);
|
|
||||||
const allConfigs = WebConfg.value.get('all');
|
|
||||||
if (allConfigs) {
|
|
||||||
const newAllConfigs = [...allConfigs, ...newConfigsArray];
|
|
||||||
WebConfg.value.set('all', newAllConfigs);
|
|
||||||
}
|
|
||||||
cardConfig.value = WebConfg.value.get('all');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
|
||||||
try {
|
|
||||||
const userConfig = await getOB11Config();
|
|
||||||
if (!userConfig) return;
|
|
||||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
|
||||||
getAllData(mergedConfig.network);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading config:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = async (text: string) => {
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = text;
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
MessagePlugin.success('复制成功');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('复制失败', err);
|
|
||||||
} finally {
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResize = () => {
|
|
||||||
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
|
||||||
if (mediumScreen.matches) {
|
|
||||||
cardWidth.value = (tabsWidth.value - 20) / 2;
|
|
||||||
} else if (largeScreen.matches) {
|
|
||||||
cardWidth.value = (tabsWidth.value - 40) / 3;
|
|
||||||
} else {
|
|
||||||
cardWidth.value = tabsWidth.value;
|
|
||||||
}
|
|
||||||
loadPage.value = true;
|
|
||||||
setTimeout(()=>{
|
|
||||||
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
|
||||||
},300)
|
|
||||||
};
|
|
||||||
emitter.on('sendWidth', (width) => {
|
|
||||||
if (typeof width === 'string') {
|
|
||||||
const strWidth = width as string;
|
|
||||||
menuWidth.value = parseInt(strWidth);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
watch(menuWidth, (newValue, oldValue) => {
|
|
||||||
loadPage.value = false;
|
|
||||||
setTimeout(()=>{
|
|
||||||
handleResize();
|
|
||||||
},300)
|
|
||||||
});
|
|
||||||
onMounted(() => {
|
|
||||||
loadConfig();
|
|
||||||
const cachedWidth = localStorage.getItem('menuWidth');
|
|
||||||
if (cachedWidth) {
|
|
||||||
menuWidth.value = parseInt(cachedWidth);
|
|
||||||
setTimeout(()=>{
|
|
||||||
handleResize();
|
|
||||||
},300)
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', ()=>{
|
|
||||||
setTimeout(()=>{
|
|
||||||
handleResize();
|
|
||||||
},300)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', ()=>{
|
|
||||||
setTimeout(()=>{
|
|
||||||
handleResize();
|
|
||||||
},300)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.title {
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting {
|
|
||||||
margin: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-box {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-card {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-address svg {
|
|
||||||
fill: var(--td-brand-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.local-box {
|
|
||||||
display: flex;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.local-icon {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.local {
|
|
||||||
flex: 6;
|
|
||||||
margin: 0 10px 0 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
flex: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-view {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-view span {
|
|
||||||
flex: 5;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-item-on{
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
|
|
||||||
}
|
|
||||||
.tag-item-off{
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
|
|
||||||
}
|
|
||||||
.browse-icon {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
:global(.t-dialog__ctx .t-dialog__position) {
|
|
||||||
padding: 48px 10px;
|
|
||||||
}
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.setting-box {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 786px) {
|
|
||||||
.setting-box {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-box {
|
|
||||||
margin: 10px 20px 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-none {
|
|
||||||
line-height: 400px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-body {
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style>
|
|
||||||
.setting-card .t-card__title {
|
|
||||||
text-align: left !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-card .t-card__description {
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-base-info .t-descriptions__header {
|
|
||||||
font-size: 15px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-base-info .t-descriptions__label {
|
|
||||||
padding: 0 var(--td-comp-paddingLR-l) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-base-info tr > td:last-child {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-coll .t-collapse-panel__wrapper .t-collapse-panel__content {
|
|
||||||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="title">
|
|
||||||
<t-divider content="其余配置" align="left">
|
|
||||||
<template #content>
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center">
|
|
||||||
<setting-icon />
|
|
||||||
<div style="margin-left: 5px">其余配置</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</t-divider>
|
|
||||||
</div>
|
|
||||||
<t-card class="card">
|
|
||||||
<div class="other-config-container">
|
|
||||||
<div class="other-config">
|
|
||||||
<t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
|
|
||||||
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
|
||||||
<t-input v-model="otherConfig.musicSignUrl" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
|
||||||
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
|
|
||||||
<t-switch v-model="otherConfig.parseMultMsg" />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
<div class="button-container">
|
|
||||||
<t-button @click="saveConfig">保存</t-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
import { SettingIcon } from 'tdesign-icons-vue-next';
|
|
||||||
|
|
||||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
|
||||||
musicSignUrl: '',
|
|
||||||
enableLocalFile2Url: false,
|
|
||||||
parseMultMsg: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const labelAlign = ref<string>();
|
|
||||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
|
||||||
const storedCredential = localStorage.getItem('auth');
|
|
||||||
if (!storedCredential) {
|
|
||||||
console.error('No stored credential found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const loginManager = new QQLoginManager(storedCredential);
|
|
||||||
return await loginManager.GetOB11Config();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
|
||||||
const storedCredential = localStorage.getItem('auth');
|
|
||||||
if (!storedCredential) {
|
|
||||||
console.error('No stored credential found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const loginManager = new QQLoginManager(storedCredential);
|
|
||||||
return await loginManager.SetOB11Config(config);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadConfig = async () => {
|
|
||||||
try {
|
|
||||||
const userConfig = await getOB11Config();
|
|
||||||
if (userConfig) {
|
|
||||||
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
|
||||||
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
|
||||||
otherConfig.value.parseMultMsg = userConfig.parseMultMsg;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading config:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
try {
|
|
||||||
const userConfig = await getOB11Config();
|
|
||||||
if (userConfig) {
|
|
||||||
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
|
||||||
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
|
||||||
userConfig.parseMultMsg = otherConfig.value.parseMultMsg ?? true;
|
|
||||||
const success = await setOB11Config(userConfig);
|
|
||||||
if (success) {
|
|
||||||
MessagePlugin.success('配置保存成功');
|
|
||||||
} else {
|
|
||||||
MessagePlugin.error('配置保存失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving config:', error);
|
|
||||||
MessagePlugin.error('配置保存失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
onMounted(() => {
|
|
||||||
loadConfig();
|
|
||||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
|
||||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
|
||||||
if (e.matches) {
|
|
||||||
labelAlign.value = 'top';
|
|
||||||
} else {
|
|
||||||
labelAlign.value = 'left';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mediaQuery.addEventListener('change', handleMediaChange);
|
|
||||||
const event = new Event('change');
|
|
||||||
Object.defineProperty(event, 'matches', {
|
|
||||||
value: mediaQuery.matches,
|
|
||||||
writable: false,
|
|
||||||
});
|
|
||||||
mediaQuery.dispatchEvent(event);
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.title {
|
|
||||||
padding: 20px 20px 0 20px;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
margin: 0 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
.other-config-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.other-config {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<t-form labelAlign="left">
|
|
||||||
<t-form-item label="启用">
|
|
||||||
<t-switch 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-switch 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-switch v-model="config.debug" />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
config: HttpClientConfig;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const messageFormatOptions = ref([
|
|
||||||
{ label: 'Array', value: 'array' },
|
|
||||||
{ label: 'String', value: 'string' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.config.messagePostFormat,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== 'array' && newValue !== 'string') {
|
|
||||||
props.config.messagePostFormat = 'array';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<t-form labelAlign="left">
|
|
||||||
<t-form-item label="启用">
|
|
||||||
<t-switch 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-switch v-model="config.enableCors" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="启用 WS">
|
|
||||||
<t-switch 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-switch v-model="config.debug" />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
config: HttpServerConfig;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const messageFormatOptions = ref([
|
|
||||||
{ label: 'Array', value: 'array' },
|
|
||||||
{ label: 'String', value: 'string' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.config.messagePostFormat,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== 'array' && newValue !== 'string') {
|
|
||||||
props.config.messagePostFormat = 'array';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<t-form labelAlign="left">
|
|
||||||
<t-form-item label="启用">
|
|
||||||
<t-switch 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-switch 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-switch v-model="config.debug" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="心跳间隔">
|
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
config: WebsocketClientConfig;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const messageFormatOptions = ref([
|
|
||||||
{ label: 'Array', value: 'array' },
|
|
||||||
{ label: 'String', value: 'string' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.config.messagePostFormat,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== 'array' && newValue !== 'string') {
|
|
||||||
props.config.messagePostFormat = 'array';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<t-form labelAlign="left">
|
|
||||||
<t-form-item label="启用">
|
|
||||||
<t-switch 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-switch 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-switch v-model="config.enableForcePushEvent" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="调试模式">
|
|
||||||
<t-switch v-model="config.debug" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="心跳间隔">
|
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
config: WebsocketServerConfig;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const messageFormatOptions = ref([
|
|
||||||
{ label: 'Array', value: 'array' },
|
|
||||||
{ label: 'String', value: 'string' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.config.messagePostFormat,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue !== 'array' && newValue !== 'string') {
|
|
||||||
props.config.messagePostFormat = 'array';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
|
|
||||||
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' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHashHistory(),
|
|
||||||
routes,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
|
||||||
const isPublicRoute = ['/webui', '/qqlogin'].includes(to.path);
|
|
||||||
const token = localStorage.getItem('auth');
|
|
||||||
|
|
||||||
if (!isPublicRoute) {
|
|
||||||
if (!token) {
|
|
||||||
MessagePlugin.error('请先登录');
|
|
||||||
return next('/webui');
|
|
||||||
}
|
|
||||||
const login = await new QQLoginManager(token).checkWebUiLogined();
|
|
||||||
if (!login) {
|
|
||||||
MessagePlugin.error('请先登录');
|
|
||||||
return next('/webui');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import mitt from 'mitt';
|
|
||||||
const emitter = mitt();
|
|
||||||
export default emitter;
|
|
||||||
1
napcat.webui/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext",
|
|
||||||
"jsx": "preserve",
|
|
||||||
"jsxImportSource": "vue",
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
||||||
"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" }]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strictNullChecks": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
|
||||||
import legacy from '@vitejs/plugin-legacy';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
legacy({
|
|
||||||
targets: ['defaults', 'not IE 11'],
|
|
||||||
modernPolyfills: ['web.structured-clone'],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
base: './',
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, 'src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
proxy: {
|
|
||||||
'/api': 'http://localhost:6099',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
chunkSizeWarningLimit: 4000,
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
chunkFileNames: 'static/js/[name]-[hash].js',
|
|
||||||
entryFileNames: 'static/js/[name]-[hash].js',
|
|
||||||
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
|
|
||||||
manualChunks(id: string) {
|
|
||||||
if (id.includes('node_modules')) {
|
|
||||||
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
75
package.json
@@ -2,63 +2,32 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.2.65",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
"build:shell": "pnpm --filter napcat-shell run build || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
"build:shell:dev": "pnpm --filter napcat-shell run build:dev || exit 1",
|
||||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
"build:framework": "pnpm --filter napcat-framework run build || exit 1",
|
||||||
"build:webui": "cd napcat.webui && vite build",
|
"build:webui": "pnpm --filter napcat-webui-frontend run build || exit 1",
|
||||||
"dev:universal": "vite build --mode universal",
|
"dev:shell": "pnpm --filter napcat-develop run dev || exit 1",
|
||||||
"dev:framework": "vite build --mode framework",
|
"typecheck": "pnpm -r --if-present run typecheck",
|
||||||
"dev:shell": "vite build --mode shell",
|
"test": "pnpm --filter napcat-test run test",
|
||||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
"test:ui": "pnpm --filter napcat-test run test:ui",
|
||||||
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
"lint": "eslint .",
|
||||||
"depend": "cd dist && npm install --omit=dev",
|
"lint:fix": "eslint . --fix"
|
||||||
"dev:depend": "npm i && cd napcat.webui && npm i"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "0.24.0",
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"@eslint/compat": "^1.2.2",
|
"@vitest/ui": "^4.0.9",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"eslint": "^9.39.1",
|
||||||
"@eslint/js": "^9.14.0",
|
"neostandard": "^0.12.2",
|
||||||
"@log4js-node/log4js-api": "^1.0.2",
|
"typescript": "^5.3.0",
|
||||||
"@napneko/nap-proto-core": "^0.0.4",
|
"vite": "^6.4.1",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"vite-plugin-cp": "^6.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"vitest": "^4.0.9"
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@sinclair/typebox": "^0.34.9",
|
|
||||||
"@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": "^13.0.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",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"typescript-eslint": "^8.13.0",
|
|
||||||
"vite": "^6.0.1",
|
|
||||||
"vite-plugin-cp": "^4.0.8",
|
|
||||||
"vite-tsconfig-paths": "^5.1.0",
|
|
||||||
"winston": "^3.17.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"ws": "^8.18.3"
|
||||||
"piscina": "^4.7.0",
|
|
||||||
"qrcode-terminal": "^0.12.0",
|
|
||||||
"silk-wasm": "^3.6.1",
|
|
||||||
"ws": "^8.18.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
packages/napcat-common/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"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.13.0",
|
||||||
|
"file-type": "^21.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
79
packages/napcat-common/src/cancel-task.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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 })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
229
packages/napcat-common/src/clean-task.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_NAPCAT_VERSION: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
42
packages/napcat-common/src/fall-back.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
121
packages/napcat-common/src/file-uuid.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
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();
|
||||||
209
packages/napcat-common/src/file.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import { stat } from 'fs/promises';
|
||||||
|
import crypto, { randomUUID } from 'crypto';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { solveProblem } from '@/napcat-common/src/helper';
|
||||||
|
|
||||||
|
export interface HttpDownloadOptions {
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string> | 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 headers: Record<string, string> = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
|
||||||
|
};
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
url = options;
|
||||||
|
headers['Host'] = new URL(url).hostname;
|
||||||
|
} else {
|
||||||
|
url = options.url;
|
||||||
|
if (options.headers) {
|
||||||
|
if (typeof options.headers === 'string') {
|
||||||
|
headers = JSON.parse(options.headers);
|
||||||
|
} else {
|
||||||
|
headers = options.headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useReferer && !headers['Referer']) {
|
||||||
|
headers['Referer'] = url;
|
||||||
|
}
|
||||||
|
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
||||||
|
if (err.cause) {
|
||||||
|
throw err.cause;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
return fetchRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function httpDownload (options: string | HttpDownloadOptions): Promise<Buffer> {
|
||||||
|
const useReferer = typeof options === 'string';
|
||||||
|
let resp = await tryDownload(options);
|
||||||
|
if (resp.status === 403 && useReferer) {
|
||||||
|
resp = await tryDownload(options, true);
|
||||||
|
}
|
||||||
|
if (!resp.ok) throw new Error(`下载文件失败: ${resp.statusText}`);
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const buffer = await blob.arrayBuffer();
|
||||||
|
return Buffer.from(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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>): 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 ?? {} });
|
||||||
|
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: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
280
packages/napcat-common/src/health.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
240
packages/napcat-common/src/helper.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
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 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/, '');
|
||||||
|
}
|
||||||
24
packages/napcat-common/src/log-interface.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
43
packages/napcat-common/src/lru-cache.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
packages/napcat-common/src/message-unique.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageUnique: MessageUniqueWrapper = new MessageUniqueWrapper();
|
||||||
1106
packages/napcat-common/src/mirror.ts
Normal file
41
packages/napcat-common/src/path.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import path, { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
export class NapCatPathWrapper {
|
||||||
|
binaryPath: string;
|
||||||
|
logsPath: string;
|
||||||
|
configPath: string;
|
||||||
|
cachePath: string;
|
||||||
|
staticPath: string;
|
||||||
|
pluginPath: string;
|
||||||
|
|
||||||
|
constructor (mainPath: string = dirname(fileURLToPath(import.meta.url))) {
|
||||||
|
this.binaryPath = mainPath;
|
||||||
|
let writePath: string;
|
||||||
|
|
||||||
|
if (process.env['NAPCAT_WORKDIR']) {
|
||||||
|
writePath = process.env['NAPCAT_WORKDIR'];
|
||||||
|
} else if (os.platform() === 'darwin') {
|
||||||
|
writePath = path.join(os.homedir(), 'Library', 'Application Support', 'QQ', 'NapCat');
|
||||||
|
} else {
|
||||||
|
writePath = this.binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logsPath = path.join(writePath, 'logs');
|
||||||
|
this.configPath = path.join(writePath, 'config');
|
||||||
|
this.pluginPath = path.join(writePath, 'plugins');// dynamic load
|
||||||
|
this.cachePath = path.join(writePath, 'cache');
|
||||||
|
this.staticPath = path.join(this.binaryPath, 'static');
|
||||||
|
if (!fs.existsSync(this.logsPath)) {
|
||||||
|
fs.mkdirSync(this.logsPath, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(this.configPath)) {
|
||||||
|
fs.mkdirSync(this.configPath, { recursive: true });
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(this.cachePath)) {
|
||||||
|
fs.mkdirSync(this.cachePath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
packages/napcat-common/src/request.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import https from 'node:https';
|
||||||
|
import http from 'node:http';
|
||||||
|
|
||||||
|
export class RequestUtil {
|
||||||
|
// 适用于获取服务器下发cookies时获取,仅GET
|
||||||
|
static async HttpsGetCookies (url: string): Promise<{ [key: string]: string; }> {
|
||||||
|
const client = url.startsWith('https') ? https : http;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = client.get(url, (res) => {
|
||||||
|
const cookies: { [key: string]: string; } = {};
|
||||||
|
|
||||||
|
res.on('data', () => { }); // Necessary to consume the stream
|
||||||
|
res.on('end', () => {
|
||||||
|
this.handleRedirect(res, url, cookies)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.headers['set-cookie']) {
|
||||||
|
this.extractCookies(res.headers['set-cookie'], cookies);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async handleRedirect (res: http.IncomingMessage, url: string, cookies: { [key: string]: string; }): Promise<{ [key: string]: string; }> {
|
||||||
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||||
|
if (res.headers.location) {
|
||||||
|
const redirectUrl = new URL(res.headers.location, url);
|
||||||
|
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
|
||||||
|
// 合并重定向过程中的cookies
|
||||||
|
return { ...cookies, ...redirectCookies };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractCookies (setCookieHeaders: string[], cookies: { [key: string]: string; }) {
|
||||||
|
setCookieHeaders.forEach((cookie) => {
|
||||||
|
const parts = cookie.split(';')[0]?.split('=');
|
||||||
|
if (parts) {
|
||||||
|
const key = parts[0];
|
||||||
|
const value = parts[1];
|
||||||
|
if (key && value && key.length > 0 && value.length > 0) {
|
||||||
|
cookies[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求和回复都是JSON data传原始内容 自动编码json
|
||||||
|
// 支持 301/302 重定向(最多 5 次)
|
||||||
|
static async HttpGetJson<T> (url: string, method: string = 'GET', data?: any, headers: {
|
||||||
|
[key: string]: string;
|
||||||
|
} = {}, isJsonRet: boolean = true, isArgJson: boolean = true, maxRedirects: number = 5): Promise<T> {
|
||||||
|
const option = new URL(url);
|
||||||
|
const protocol = url.startsWith('https://') ? https : http;
|
||||||
|
const options = {
|
||||||
|
hostname: option.hostname,
|
||||||
|
port: option.port,
|
||||||
|
path: option.pathname + option.search,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'application/json',
|
||||||
|
// 'Content-Length': Buffer.byteLength(postData),
|
||||||
|
// },
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = protocol.request(options, (res: http.IncomingMessage) => {
|
||||||
|
// 处理重定向
|
||||||
|
if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307 || res.statusCode === 308) && res.headers.location) {
|
||||||
|
if (maxRedirects <= 0) {
|
||||||
|
reject(new Error('Too many redirects'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const redirectUrl = new URL(res.headers.location, url).href;
|
||||||
|
// 递归跟随重定向
|
||||||
|
this.HttpGetJson<T>(redirectUrl, method, data, headers, isJsonRet, isArgJson, maxRedirects - 1)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseBody = '';
|
||||||
|
res.on('data', (chunk: string | Buffer) => {
|
||||||
|
responseBody += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
if (isJsonRet) {
|
||||||
|
const responseJson = JSON.parse(responseBody);
|
||||||
|
resolve(responseJson as T);
|
||||||
|
} else {
|
||||||
|
resolve(responseBody as T);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
} catch (parseError: unknown) {
|
||||||
|
reject(new Error((parseError as Error).message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error: Error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
||||||
|
if (isArgJson) {
|
||||||
|
req.write(JSON.stringify(data));
|
||||||
|
} else {
|
||||||
|
req.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求返回都是原始内容
|
||||||
|
static async HttpGetText (url: string, method: string = 'GET', data?: any, headers: { [key: string]: string; } = {}) {
|
||||||
|
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/napcat-common/src/status-interface.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface SystemStatus {
|
||||||
|
cpu: {
|
||||||
|
model: string,
|
||||||
|
speed: string;
|
||||||
|
usage: {
|
||||||
|
system: string;
|
||||||
|
qq: string;
|
||||||
|
},
|
||||||
|
core: number;
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
total: string;
|
||||||
|
usage: {
|
||||||
|
system: string;
|
||||||
|
qq: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
export interface IStatusHelperSubscription {
|
||||||
|
on (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||||
|
off (event: 'statusUpdate', listener: (status: SystemStatus) => void): this;
|
||||||
|
emit (event: 'statusUpdate', status: SystemStatus): boolean;
|
||||||
|
}
|
||||||
22
packages/napcat-common/src/store.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class Store {
|
||||||
|
private store = new Map<string, any>();
|
||||||
|
|
||||||
|
set<T> (key: string, value: T, ttl?: number): void {
|
||||||
|
this.store.set(key, value);
|
||||||
|
if (ttl) {
|
||||||
|
setTimeout(() => this.store.delete(key), ttl * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T> (key: string): T | null {
|
||||||
|
return this.store.get(key) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
exists (...keys: string[]): number {
|
||||||
|
return keys.filter(key => this.store.has(key)).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
export default store;
|
||||||
6
packages/napcat-common/src/subscription-interface.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type LogListener = (msg: string) => void;
|
||||||
|
export interface ISubscription {
|
||||||
|
subscribe (listener: LogListener): void;
|
||||||
|
unsubscribe (listener: LogListener): void;
|
||||||
|
notify (msg: string): void;
|
||||||
|
}
|
||||||
@@ -5,12 +5,11 @@ import path from 'node:path';
|
|||||||
let osName: string;
|
let osName: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
osName = os.hostname();
|
osName = os.hostname();
|
||||||
} catch (e) {
|
} catch {
|
||||||
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
|
osName = 'NapCat'; // + crypto.randomUUID().substring(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
|
|
||||||
export const systemPlatform = os.platform();
|
export const systemPlatform = os.platform();
|
||||||
28
packages/napcat-common/src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// QQVersionType
|
||||||
|
export type QQPackageInfoType = {
|
||||||
|
version: string;
|
||||||
|
buildVersion: string;
|
||||||
|
platform: string;
|
||||||
|
eleArch: string;
|
||||||
|
};
|
||||||
|
export type QQVersionConfigType = {
|
||||||
|
baseVersion: string;
|
||||||
|
curVersion: string;
|
||||||
|
prevVersion: string;
|
||||||
|
onErrorVersions: Array<unknown>;
|
||||||
|
buildId: string;
|
||||||
|
};
|
||||||
|
export type QQAppidTableType = {
|
||||||
|
[key: string]: { appid: string, qua: string };
|
||||||
|
};
|
||||||
|
export interface Peer {
|
||||||
|
chatType: number; // 聊天类型
|
||||||
|
peerUid: string; // 对等方的唯一标识符
|
||||||
|
guildId?: string; // 可选的频道ID
|
||||||
|
}
|
||||||
|
export interface QQLevel {
|
||||||
|
crownNum: number;
|
||||||
|
sunNum: number;
|
||||||
|
moonNum: number;
|
||||||
|
starNum: number;
|
||||||
|
}
|
||||||
120
packages/napcat-common/src/version.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
export const napCatVersion = (typeof import.meta?.env !== 'undefined' && import.meta.env.VITE_NAPCAT_VERSION) || '1.0.0-dev';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SemVer 2.0 正则表达式
|
||||||
|
* 格式: 主版本号.次版本号.修订号[-先行版本号][+版本编译信息]
|
||||||
|
* 参考: https://semver.org/lang/zh-CN/
|
||||||
|
*/
|
||||||
|
const SEMVER_REGEX = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
||||||
|
|
||||||
|
export interface SemVerInfo {
|
||||||
|
valid: boolean;
|
||||||
|
normalized: string;
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
prerelease: string | null;
|
||||||
|
buildmetadata: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析并验证版本号是否符合 SemVer 2.0 规范
|
||||||
|
* @param version - 版本字符串 (支持 v 前缀)
|
||||||
|
* @returns SemVer 解析结果
|
||||||
|
*/
|
||||||
|
export function parseSemVer (version: string | undefined | null): SemVerInfo {
|
||||||
|
if (!version || typeof version !== 'string') {
|
||||||
|
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = version.trim().match(SEMVER_REGEX);
|
||||||
|
if (match) {
|
||||||
|
const major = parseInt(match[1]!, 10);
|
||||||
|
const minor = parseInt(match[2]!, 10);
|
||||||
|
const patch = parseInt(match[3]!, 10);
|
||||||
|
const prerelease = match[4] || null;
|
||||||
|
const buildmetadata = match[5] || null;
|
||||||
|
|
||||||
|
// 构建标准化版本号(不带 v 前缀)
|
||||||
|
let normalized = `${major}.${minor}.${patch}`;
|
||||||
|
if (prerelease) normalized += `-${prerelease}`;
|
||||||
|
if (buildmetadata) normalized += `+${buildmetadata}`;
|
||||||
|
|
||||||
|
return { valid: true, normalized, major, minor, patch, prerelease, buildmetadata };
|
||||||
|
}
|
||||||
|
return { valid: false, normalized: '1.0.0-dev', major: 1, minor: 0, patch: 0, prerelease: 'dev', buildmetadata: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证版本号是否符合 SemVer 2.0 规范
|
||||||
|
* @param version - 版本字符串
|
||||||
|
* @returns 是否有效
|
||||||
|
*/
|
||||||
|
export function isValidSemVer (version: string | undefined | null): boolean {
|
||||||
|
return parseSemVer(version).valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个 SemVer 版本号
|
||||||
|
* @param v1 - 版本号1
|
||||||
|
* @param v2 - 版本号2
|
||||||
|
* @returns -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
|
||||||
|
*/
|
||||||
|
export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 {
|
||||||
|
const a = parseSemVer(v1);
|
||||||
|
const b = parseSemVer(v2);
|
||||||
|
|
||||||
|
if (!a.valid && !b.valid) {
|
||||||
|
return v1.localeCompare(v2) as -1 | 0 | 1;
|
||||||
|
}
|
||||||
|
if (!a.valid) return -1;
|
||||||
|
if (!b.valid) return 1;
|
||||||
|
|
||||||
|
// 比较主版本号
|
||||||
|
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
||||||
|
// 比较次版本号
|
||||||
|
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
|
||||||
|
// 比较修订号
|
||||||
|
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
|
||||||
|
|
||||||
|
// 有先行版本号的版本优先级较低
|
||||||
|
if (a.prerelease && !b.prerelease) return -1;
|
||||||
|
if (!a.prerelease && b.prerelease) return 1;
|
||||||
|
|
||||||
|
// 两者都有先行版本号时,按字典序比较
|
||||||
|
if (a.prerelease && b.prerelease) {
|
||||||
|
const aParts = a.prerelease.split('.');
|
||||||
|
const bParts = b.prerelease.split('.');
|
||||||
|
const len = Math.max(aParts.length, bParts.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const aPart = aParts[i];
|
||||||
|
const bPart = bParts[i];
|
||||||
|
|
||||||
|
if (aPart === undefined) return -1;
|
||||||
|
if (bPart === undefined) return 1;
|
||||||
|
|
||||||
|
const aNum = /^\d+$/.test(aPart) ? parseInt(aPart, 10) : NaN;
|
||||||
|
const bNum = /^\d+$/.test(bPart) ? parseInt(bPart, 10) : NaN;
|
||||||
|
|
||||||
|
// 数字 vs 数字
|
||||||
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||||
|
if (aNum !== bNum) return aNum > bNum ? 1 : -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 数字优先级低于字符串
|
||||||
|
if (!isNaN(aNum)) return -1;
|
||||||
|
if (!isNaN(bNum)) return 1;
|
||||||
|
// 字符串 vs 字符串
|
||||||
|
if (aPart !== bPart) return aPart > bPart ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取解析后的当前版本信息
|
||||||
|
*/
|
||||||
|
export const napCatVersionInfo = parseSemVer(napCatVersion);
|
||||||
34
packages/napcat-common/src/worker.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Worker } from 'worker_threads';
|
||||||
|
|
||||||
|
export async function runTask<T, R> (workerScript: string, taskData: T): Promise<R> {
|
||||||
|
const worker = new Worker(workerScript);
|
||||||
|
try {
|
||||||
|
return await new Promise<R>((resolve, reject) => {
|
||||||
|
worker.on('message', (result: R) => {
|
||||||
|
if ((result as any)?.log) {
|
||||||
|
console.error('Worker Log--->:', (result as { log: string }).log);
|
||||||
|
}
|
||||||
|
if ((result as any)?.error) {
|
||||||
|
reject(new Error('Worker error: ' + (result as { error: string }).error));
|
||||||
|
}
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('error', (error) => {
|
||||||
|
reject(new Error(`Worker error: ${error.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on('exit', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
worker.postMessage(taskData);
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new Error(`Failed to run task: ${(error as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
// Ensure the worker is terminated after the promise is settled
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
packages/napcat-common/tsconfig.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"lib": [
|
||||||
|
"ES2021"
|
||||||
|
],
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types"
|
||||||
|
],
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": ".",
|
||||||
|
"noEmit": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"../*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
27
packages/napcat-core/adapters/NodeIDependsAdapter.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { MsfChangeReasonType, MsfStatusType } from '@/napcat-core/types/adapter';
|
||||||
|
|
||||||
|
export class NodeIDependsAdapter {
|
||||||
|
onMSFStatusChange (_statusType: MsfStatusType, _changeReasonType: MsfChangeReasonType) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onMSFSsoError (_code: number, _desc: string) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupCode (_args: unknown) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// onSendMsfReply (_seq: string, _cmd: string, _uk1: number, _uk2: string, _rsp: {
|
||||||
|
// ssoRetCode: 0,
|
||||||
|
// trpcRetCode: 0,
|
||||||
|
// trpcFuncCode: 0,
|
||||||
|
// errorMsg: '',
|
||||||
|
// pbBuffer: Uint8Array,
|
||||||
|
// transInfoMap: Map<unknown, unknown>;
|
||||||
|
// }) {
|
||||||
|
|
||||||
|
// console.log('[NodeIDependsAdapter] onSendMsfReply', _seq, _cmd, _uk1, _uk2, Buffer.from(_rsp.pbBuffer).toString('hex'));
|
||||||
|
// }
|
||||||
|
}
|
||||||
10
packages/napcat-core/adapters/NodeIDispatcherAdapter.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class NodeIDispatcherAdapter {
|
||||||
|
dispatchRequest (_arg: unknown) {
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchCall (_arg: unknown) {
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchCallWithJson (_arg: unknown) {
|
||||||
|
}
|
||||||
|
}
|
||||||