mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
488 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4d4b1ad26c | ||
![]() |
e42fbea918 | ||
![]() |
48b648b0fb | ||
![]() |
68e86b07c7 | ||
![]() |
12cb500818 | ||
![]() |
9ffaab178a | ||
![]() |
d4fbbd6711 | ||
![]() |
ded53cd348 | ||
![]() |
be9e80c87b | ||
![]() |
e9fe6f28cc | ||
![]() |
0b8bf739e9 | ||
![]() |
0222664db8 | ||
![]() |
a88792e452 | ||
![]() |
ad45400742 | ||
![]() |
53e5ba03be | ||
![]() |
b587d6b91d | ||
![]() |
5e750d4ee9 | ||
![]() |
50fb32f81c | ||
![]() |
6c46cdd947 | ||
![]() |
372452fbee | ||
![]() |
417ef5d335 | ||
![]() |
9c534f8afd | ||
![]() |
ecd426bb80 | ||
![]() |
f74ef273de | ||
![]() |
f913e0b027 | ||
![]() |
f7268c30ca | ||
![]() |
0f5ef03d63 | ||
![]() |
745276d0f0 | ||
![]() |
2e108a4bd6 | ||
![]() |
666da80ef5 | ||
![]() |
cc73104d62 | ||
![]() |
3c10b82bab | ||
![]() |
9a65dae6a2 | ||
![]() |
f26cd8cdc9 | ||
![]() |
eeec905df0 | ||
![]() |
0c6aac7f66 | ||
![]() |
86d22db141 | ||
![]() |
48a5d0eef3 | ||
![]() |
bda174bed4 | ||
![]() |
caf98b8655 | ||
![]() |
c9833c5988 | ||
![]() |
55ef7e529e | ||
![]() |
9b04ddcefd | ||
![]() |
6dc4f38581 | ||
![]() |
93ce8bfb85 | ||
![]() |
e7d138448a | ||
![]() |
02c4a468cb | ||
![]() |
d392e653e1 | ||
![]() |
e8faa09f1d | ||
![]() |
e80ed3b33e | ||
![]() |
41a346e1cf | ||
![]() |
5e19fc112a | ||
![]() |
2f7aff2b56 | ||
![]() |
ccb0e1fb4f | ||
![]() |
d4163c913a | ||
![]() |
8087ba0e4a | ||
![]() |
6700523b61 | ||
![]() |
49f1c3f9ba | ||
![]() |
575ab4f1d1 | ||
![]() |
3658547731 | ||
![]() |
eb6590e9e2 | ||
![]() |
83f28795f2 | ||
![]() |
e98bfaac11 | ||
![]() |
4f4bd3c6e0 | ||
![]() |
bd1faccaa8 | ||
![]() |
25751b8149 | ||
![]() |
e34b60315c | ||
![]() |
046afc0c23 | ||
![]() |
2f61ba7f25 | ||
![]() |
8981f12b1a | ||
![]() |
34e96b1089 | ||
![]() |
41db435ef5 | ||
![]() |
b525fa81bb | ||
![]() |
6382b29da8 | ||
![]() |
8bc0403139 | ||
![]() |
9f261e78c3 | ||
![]() |
15d9390ee4 | ||
![]() |
572b8809a5 | ||
![]() |
623799c049 | ||
![]() |
4271acc6ab | ||
![]() |
609e83a824 | ||
![]() |
e98910c9ff | ||
![]() |
c432799580 | ||
![]() |
fa87f7c8c3 | ||
![]() |
4a44062814 | ||
![]() |
fe0bda11d3 | ||
![]() |
1ec1040e43 | ||
![]() |
e44595334a | ||
![]() |
f40de023b0 | ||
![]() |
9799d02ad2 | ||
![]() |
bec88fee04 | ||
![]() |
1a94e20691 | ||
![]() |
3690307d0b | ||
![]() |
2d5b4bc90a | ||
![]() |
cc93ed3567 | ||
![]() |
dce4988767 | ||
![]() |
5c81b60b58 | ||
![]() |
a668bfbc13 | ||
![]() |
bc0fc96b9b | ||
![]() |
ae14692d5b | ||
![]() |
d445dc6644 | ||
![]() |
db3d435402 | ||
![]() |
7ee48f1443 | ||
![]() |
a54f30acc1 | ||
![]() |
75e7bc7275 | ||
![]() |
f1b2c8b1cf | ||
![]() |
50079e7a96 | ||
![]() |
6d37868ae8 | ||
![]() |
543961e980 | ||
![]() |
1e2c76bb47 | ||
![]() |
ddc0ed066d | ||
![]() |
6708903c65 | ||
![]() |
5ee0afb604 | ||
![]() |
9b20e9db29 | ||
![]() |
74b4d9bf49 | ||
![]() |
89f7892681 | ||
![]() |
f83bf197d2 | ||
![]() |
5bcc130dd7 | ||
![]() |
4be6d8ec01 | ||
![]() |
aad5ed55d2 | ||
![]() |
86da417c17 | ||
![]() |
ae57ab78f3 | ||
![]() |
4487db4e0a | ||
![]() |
a0a50755d3 | ||
![]() |
621e41cc96 | ||
![]() |
96b1f71437 | ||
![]() |
5e0b3b2f35 | ||
![]() |
6829fad5bd | ||
![]() |
7af0d9e87b | ||
![]() |
c089ebea99 | ||
![]() |
d2a2c1c39c | ||
![]() |
ce9b09e8d1 | ||
![]() |
2f6dfe51f5 | ||
![]() |
bd227cd0b8 | ||
![]() |
96003724ab | ||
![]() |
6a08b15095 | ||
![]() |
dab0f9ab45 | ||
![]() |
e733a6b69a | ||
![]() |
9aca98bf13 | ||
![]() |
b7c95e53dc | ||
![]() |
f762c450ca | ||
![]() |
d58bbe53da | ||
![]() |
f32edd8af7 | ||
![]() |
c747a86e5b | ||
![]() |
abfda0dd58 | ||
![]() |
f66d7b11a8 | ||
![]() |
f425c9478e | ||
![]() |
756dea71fc | ||
![]() |
71a6c4ccc5 | ||
![]() |
ae2f4777ec | ||
![]() |
dcd9b8168a | ||
![]() |
4bb03ae5ba | ||
![]() |
8bd6f8397b | ||
![]() |
096e52d93e | ||
![]() |
037065291d | ||
![]() |
4cf52e1b13 | ||
![]() |
21b228552d | ||
![]() |
76b404cdd8 | ||
![]() |
937c594ff7 | ||
![]() |
b463140de7 | ||
![]() |
f518fb9214 | ||
![]() |
1092831718 | ||
![]() |
6b377416da | ||
![]() |
8f5baa47ec | ||
![]() |
5494ff0553 | ||
![]() |
7a4805b464 | ||
![]() |
8435375810 | ||
![]() |
c893ec6030 | ||
![]() |
e8bf6fa0a6 | ||
![]() |
f228129c19 | ||
![]() |
cbf98ffb89 | ||
![]() |
f6067b002f | ||
![]() |
636d1103e3 | ||
![]() |
bede517f7e | ||
![]() |
16e4891b7d | ||
![]() |
3bcd79fbb7 | ||
![]() |
aacf6c2917 | ||
![]() |
92d720cd57 | ||
![]() |
2ea025047f | ||
![]() |
f7f7e09cab | ||
![]() |
75866b435e | ||
![]() |
f07941685b | ||
![]() |
60a0539216 | ||
![]() |
3dd4b6549f | ||
![]() |
0802c35dc1 | ||
![]() |
7d9d7226ec | ||
![]() |
b5ef6ce6b0 | ||
![]() |
49ec6181b0 | ||
![]() |
783a534768 | ||
![]() |
704ac11cbb | ||
![]() |
aa9663d85e | ||
![]() |
05291f34fb | ||
![]() |
2260fe32a1 | ||
![]() |
2c398a6832 | ||
![]() |
3e1f566699 | ||
![]() |
4f89f184b8 | ||
![]() |
787685c937 | ||
![]() |
ed9cd2fe38 | ||
![]() |
740d80e851 | ||
![]() |
4520a20bd4 | ||
![]() |
98c65c4923 | ||
![]() |
e287906a9d | ||
![]() |
8bae789020 | ||
![]() |
ce57b7b725 | ||
![]() |
1d9872195d | ||
![]() |
98d1f8e29f | ||
![]() |
221b3fb730 | ||
![]() |
90a834495a | ||
![]() |
8bfd102232 | ||
![]() |
65e784f169 | ||
![]() |
0fc81c672f | ||
![]() |
62ae0f4321 | ||
![]() |
a01a0a1a18 | ||
![]() |
4c30cc69ad | ||
![]() |
1d43b75df4 | ||
![]() |
d02afdfc3e | ||
![]() |
5d6dee9fd0 | ||
![]() |
60c67ef41c | ||
![]() |
917d7c1f19 | ||
![]() |
ad19f2c99e | ||
![]() |
8a61f5a03f | ||
![]() |
8c164910f6 | ||
![]() |
a560d3d266 | ||
![]() |
532f739272 | ||
![]() |
a120727f2d | ||
![]() |
a9bcb830a8 | ||
![]() |
56e5f0033f | ||
![]() |
101106996a | ||
![]() |
41a81534dc | ||
![]() |
1425e8f229 | ||
![]() |
75bb1d2193 | ||
![]() |
2a23820f9b | ||
![]() |
2ee0fed047 | ||
![]() |
40be6b9c43 | ||
![]() |
a06b3f0246 | ||
![]() |
4787fa53b4 | ||
![]() |
a06158bf01 | ||
![]() |
314e7485b8 | ||
![]() |
aed5d2d9f0 | ||
![]() |
f44e48a28b | ||
![]() |
38be90450c | ||
![]() |
2dd57d7676 | ||
![]() |
6b3b163fa8 | ||
![]() |
9792ebafdc | ||
![]() |
d10e7c37cb | ||
![]() |
d38f1853a4 | ||
![]() |
bdec16266e | ||
![]() |
49ca698ab9 | ||
![]() |
3efd8163c9 | ||
![]() |
cc2d11449c | ||
![]() |
7e9c19ca5b | ||
![]() |
3b01b6827f | ||
![]() |
8d9ef851ba | ||
![]() |
b070bc59bc | ||
![]() |
8d663946e1 | ||
![]() |
2a2328b029 | ||
![]() |
efc9064abb | ||
![]() |
dd70adf071 | ||
![]() |
0f427375cb | ||
![]() |
4001270b93 | ||
![]() |
e7f5ed3bcc | ||
![]() |
05cdc37d0a | ||
![]() |
27920e0bee | ||
![]() |
ae409b7249 | ||
![]() |
8276258348 | ||
![]() |
1bf96a97a5 | ||
![]() |
d672680c4c | ||
![]() |
b89f2805e7 | ||
![]() |
78b4aa9295 | ||
![]() |
0a06637e78 | ||
![]() |
13afa2c7ab | ||
![]() |
51d34d17cc | ||
![]() |
18a99341d5 | ||
![]() |
f01c8f0110 | ||
![]() |
d8070eee2a | ||
![]() |
8519b7f4df | ||
![]() |
591ab1b1df | ||
![]() |
393815b11e | ||
![]() |
341a397bc4 | ||
![]() |
e46d274a75 | ||
![]() |
ad6f21980c | ||
![]() |
017b8b7f15 | ||
![]() |
9b448b17e6 | ||
![]() |
f9996a9987 | ||
![]() |
000ef55273 | ||
![]() |
e1ac0f02b4 | ||
![]() |
b9297e3f1d | ||
![]() |
34d0669ca8 | ||
![]() |
25e42720cf | ||
![]() |
f7c1951191 | ||
![]() |
479b971b0c | ||
![]() |
347ba5f354 | ||
![]() |
81dbb9d980 | ||
![]() |
c4e1a3ab04 | ||
![]() |
90ec774a21 | ||
![]() |
db7a27e624 | ||
![]() |
f7d965eda2 | ||
![]() |
74ca2e2e16 | ||
![]() |
8ab550f2f5 | ||
![]() |
018aca4db2 | ||
![]() |
d4327166c1 | ||
![]() |
fa25d2e779 | ||
![]() |
3ce1c3f0ec | ||
![]() |
96dff5141e | ||
![]() |
78d85d9965 | ||
![]() |
37ec455b02 | ||
![]() |
6ab82739a6 | ||
![]() |
a36917e7c0 | ||
![]() |
21f3428b36 | ||
![]() |
f8a487db25 | ||
![]() |
73a859be04 | ||
![]() |
63bcee01a1 | ||
![]() |
85b4966ba8 | ||
![]() |
36c2c567b7 | ||
![]() |
7b1ac224f6 | ||
![]() |
34d9f04f15 | ||
![]() |
be5da7cc6f | ||
![]() |
8d32ccb5d4 | ||
![]() |
6acceb884c | ||
![]() |
4c834fd640 | ||
![]() |
301278c7a9 | ||
![]() |
42ee83c54f | ||
![]() |
e631f69621 | ||
![]() |
ce8760a39a | ||
![]() |
ff952956de | ||
![]() |
28f3ff4971 | ||
![]() |
19e728c3cb | ||
![]() |
269773ed6b | ||
![]() |
e0d32417e1 | ||
![]() |
9fa6083bed | ||
![]() |
4d2fccdfb4 | ||
![]() |
c1c4bdfe94 | ||
![]() |
8a0e9e8b61 | ||
![]() |
1190e14171 | ||
![]() |
00292b177a | ||
![]() |
88de57f984 | ||
![]() |
61ddf38892 | ||
![]() |
52b3540ec3 | ||
![]() |
5f831958c3 | ||
![]() |
c3d4698af3 | ||
![]() |
bd6e83217d | ||
![]() |
50ec49d9a2 | ||
![]() |
dc3a089070 | ||
![]() |
530e380178 | ||
![]() |
10e4387add | ||
![]() |
e925bc3aa8 | ||
![]() |
427b3a7560 | ||
![]() |
c8da950725 | ||
![]() |
743c5b8196 | ||
![]() |
5e62abea57 | ||
![]() |
6bfc545582 | ||
![]() |
411108a2d2 | ||
![]() |
308a6fa9e4 | ||
![]() |
2dc7b785d0 | ||
![]() |
0e69e9e839 | ||
![]() |
b83229b5da | ||
![]() |
6f053f5f7d | ||
![]() |
c3dc53eaaf | ||
![]() |
ffdc34cfe2 | ||
![]() |
4825a0e341 | ||
![]() |
95a00d7f35 | ||
![]() |
d885bab426 | ||
![]() |
e2a6a0bc02 | ||
![]() |
ff7d8609ce | ||
![]() |
7507b90e03 | ||
![]() |
2b226a4b27 | ||
![]() |
8b0232c4fe | ||
![]() |
0728ee9ad6 | ||
![]() |
8c6f04d0bc | ||
![]() |
c67fad789e | ||
![]() |
4072339d70 | ||
![]() |
3a244f5804 | ||
![]() |
f12cf59137 | ||
![]() |
c76f556a11 | ||
![]() |
e0f3d07b98 | ||
![]() |
378d85dc67 | ||
![]() |
875e91fc0e | ||
![]() |
15f7cd9814 | ||
![]() |
1eb5cd6237 | ||
![]() |
ad2f843c8f | ||
![]() |
8e550e216e | ||
![]() |
9f07b07c82 | ||
![]() |
0be6effc32 | ||
![]() |
7ab6a10fc9 | ||
![]() |
fb09af0e64 | ||
![]() |
0d99d30b2d | ||
![]() |
0000ec8b5b | ||
![]() |
0085bd8a1f | ||
![]() |
617139dfa4 | ||
![]() |
4eb4a612d0 | ||
![]() |
cda5e784f6 | ||
![]() |
d93a280ab3 | ||
![]() |
f7e2b3a4a7 | ||
![]() |
39d9c8fa74 | ||
![]() |
8823895a03 | ||
![]() |
b44a9e696c | ||
![]() |
cf28a3dc17 | ||
![]() |
7416e6caf6 | ||
![]() |
90f6896f3c | ||
![]() |
eebcd0700d | ||
![]() |
133eee0c66 | ||
![]() |
640fb75f74 | ||
![]() |
51dcc1add6 | ||
![]() |
730c928f91 | ||
![]() |
c3b7e111b9 | ||
![]() |
1874e48925 | ||
![]() |
e7a082c91c | ||
![]() |
5d4f45407e | ||
![]() |
17c37ec32f | ||
![]() |
b5f8140c79 | ||
![]() |
63f746c237 | ||
![]() |
dac6709f27 | ||
![]() |
470c8d0b29 | ||
![]() |
b0d35e803b | ||
![]() |
a71475be8b | ||
![]() |
b9f2cc5142 | ||
![]() |
2d46e55b9b | ||
![]() |
684e254996 | ||
![]() |
a2f7903960 | ||
![]() |
c0c757d6bd | ||
![]() |
da0fad743d | ||
![]() |
80b10d6025 | ||
![]() |
a27c2a69c4 | ||
![]() |
9ed2a2fd19 | ||
![]() |
aa9d96718c | ||
![]() |
aa67a2b71c | ||
![]() |
d3405edd42 | ||
![]() |
3612098d62 | ||
![]() |
2f08b72d69 | ||
![]() |
ab66904c1a | ||
![]() |
55542a3dbe | ||
![]() |
8569a45114 | ||
![]() |
c790311fc3 | ||
![]() |
3c45c8bd80 | ||
![]() |
d5b7b3ae31 | ||
![]() |
43e73a5f24 | ||
![]() |
698947ed97 | ||
![]() |
f3d967ae07 | ||
![]() |
dbe72fa07e | ||
![]() |
801a97d85b | ||
![]() |
9f8f938c47 | ||
![]() |
8fe37d1c1e | ||
![]() |
5cca8457e7 | ||
![]() |
e9332e7646 | ||
![]() |
31365505d8 | ||
![]() |
b3fbe9e34a | ||
![]() |
4082b651c5 | ||
![]() |
0081000ef0 | ||
![]() |
ad4d6a1070 | ||
![]() |
5190b26399 | ||
![]() |
29a8db96f4 | ||
![]() |
1a4c2cabfd | ||
![]() |
ef9189055c | ||
![]() |
5cc3719125 | ||
![]() |
5d46f41348 | ||
![]() |
3c2c1963f4 | ||
![]() |
4896ca9279 | ||
![]() |
f0afba6cd9 | ||
![]() |
bd717c298a | ||
![]() |
baaa8a70dc | ||
![]() |
6d561c6e6f | ||
![]() |
e6b6947d49 | ||
![]() |
52e99a2175 | ||
![]() |
052d17a46f | ||
![]() |
1aa1f4c212 | ||
![]() |
c3a48e3344 | ||
![]() |
1d5483dc28 | ||
![]() |
54277fa0df | ||
![]() |
ab04bd262f | ||
![]() |
fb23087b65 | ||
![]() |
846fee7ac8 | ||
![]() |
977eacc679 | ||
![]() |
dacfefe644 | ||
![]() |
345e941e11 | ||
![]() |
6cb7d45464 | ||
![]() |
e7222653fa | ||
![]() |
014f0758f5 | ||
![]() |
0e8b416f6d | ||
![]() |
09a60a2204 | ||
![]() |
b0eae307c2 | ||
![]() |
f5d2b54cca | ||
![]() |
3eefec3899 | ||
![]() |
b6a8094554 | ||
![]() |
4083b35436 | ||
![]() |
bb72d70baf | ||
![]() |
95d1a77f52 | ||
![]() |
051729886e | ||
![]() |
0f00123dc7 |
@@ -1,64 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
'env': {
|
|
||||||
'browser': true,
|
|
||||||
'es2021': true,
|
|
||||||
'node': true
|
|
||||||
},
|
|
||||||
'ignorePatterns': ['src/core/proto/'],
|
|
||||||
'extends': [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended'
|
|
||||||
],
|
|
||||||
'overrides': [
|
|
||||||
{
|
|
||||||
'env': {
|
|
||||||
'node': true
|
|
||||||
},
|
|
||||||
'files': [
|
|
||||||
'.eslintrc.{js,cjs}'
|
|
||||||
],
|
|
||||||
'parserOptions': {
|
|
||||||
'sourceType': 'script'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'parser': '@typescript-eslint/parser',
|
|
||||||
'parserOptions': {
|
|
||||||
'ecmaVersion': 'latest',
|
|
||||||
'sourceType': 'module'
|
|
||||||
},
|
|
||||||
'plugins': [
|
|
||||||
'@typescript-eslint',
|
|
||||||
'import'
|
|
||||||
],
|
|
||||||
'settings': {
|
|
||||||
'import/parsers': {
|
|
||||||
'@typescript-eslint/parser': ['.ts']
|
|
||||||
},
|
|
||||||
'import/resolver': {
|
|
||||||
'typescript': {
|
|
||||||
'alwaysTryTypes': true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'rules': {
|
|
||||||
'indent': [
|
|
||||||
'error',
|
|
||||||
4
|
|
||||||
],
|
|
||||||
'linebreak-style': [
|
|
||||||
'error',
|
|
||||||
'unix'
|
|
||||||
],
|
|
||||||
'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'],
|
|
||||||
}
|
|
||||||
};
|
|
80
.github/workflows/build.yml
vendored
80
.github/workflows/build.yml
vendored
@@ -1,5 +1,7 @@
|
|||||||
name: "Build Action"
|
name: "Build Action"
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
@@ -8,54 +10,38 @@ jobs:
|
|||||||
Build-LiteLoader:
|
Build-LiteLoader:
|
||||||
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:
|
- 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.Framework
|
||||||
- name: Use Node.js 20.X
|
run: |
|
||||||
uses: actions/setup-node@v4
|
npm i && cd napcat.webui && npm i && cd ..
|
||||||
with:
|
npm run build:framework && npm run depend
|
||||||
node-version: 20.x
|
|
||||||
- name: Build NuCat Framework
|
|
||||||
run: |
|
|
||||||
npm i
|
|
||||||
npm run build:framework
|
|
||||||
cd dist
|
|
||||||
npm i --omit=dev
|
|
||||||
rm package-lock.json
|
rm package-lock.json
|
||||||
cd ..
|
- 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: 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:
|
- 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
|
||||||
- name: Use Node.js 20.X
|
run: |
|
||||||
uses: actions/setup-node@v4
|
npm i && cd napcat.webui && npm i && cd ..
|
||||||
with:
|
npm run build:shell && npm run depend
|
||||||
node-version: 20.x
|
rm package-lock.json
|
||||||
- name: Build NuCat LiteLoader
|
- name: Upload Artifact
|
||||||
run: |
|
uses: actions/upload-artifact@v4
|
||||||
npm i
|
with:
|
||||||
npm run build:shell
|
name: NapCat.Shell
|
||||||
cd dist
|
path: dist
|
||||||
npm i --omit=dev
|
|
||||||
rm package-lock.json
|
|
||||||
cd ..
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: NapCat.Shell
|
|
||||||
path: dist
|
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -49,6 +49,9 @@ jobs:
|
|||||||
- name: Build NuCat Framework
|
- name: Build NuCat Framework
|
||||||
run: |
|
run: |
|
||||||
npm i
|
npm i
|
||||||
|
cd napcat.webui
|
||||||
|
npm i
|
||||||
|
cd ..
|
||||||
npm run build:framework
|
npm run build:framework
|
||||||
cd dist
|
cd dist
|
||||||
npm i --omit=dev
|
npm i --omit=dev
|
||||||
@@ -78,6 +81,9 @@ jobs:
|
|||||||
- name: Build NuCat Shell
|
- name: Build NuCat Shell
|
||||||
run: |
|
run: |
|
||||||
npm i
|
npm i
|
||||||
|
cd napcat.webui
|
||||||
|
npm i
|
||||||
|
cd ..
|
||||||
npm run build:shell
|
npm run build:shell
|
||||||
cd dist
|
cd dist
|
||||||
npm i --omit=dev
|
npm i --omit=dev
|
||||||
|
53
README.md
53
README.md
@@ -1,52 +1,57 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
|
|
||||||
|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
## 欢迎回来
|
## 欢迎回家
|
||||||
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
|
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||||
|
|
||||||
## 猫猫技能
|
## 特性介绍
|
||||||
- [x] **超高性能**:轻松数千群聊 独创消息队列
|
- [x] **安装简单**:就算是笨蛋也能使用
|
||||||
- [x] **启动方式**:支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
|
- [x] **性能友好**:就算是低内存也能使用
|
||||||
- [x] **覆盖平台**: 覆盖 Windows / Linux (可选 Docker) / Android Termux / MacOS
|
- [x] **接口丰富**:就算是没有也能使用
|
||||||
- [x] **安装简单**: 支持一键脚本/程序自动部署/镜像部署等多种覆盖范围
|
- [x] **稳定好用**:就算是被捉也能使用
|
||||||
- [x] **超低占用**:无头模式占用资源极低,适合在服务器上运行
|
|
||||||
- [x] **超多接口**:实现大部分 OneBot 和 go-cqhttp 接口,超多扩展 API
|
|
||||||
- [x] **远程管理**:自带 WebUI 支持,远程管理更加便捷
|
|
||||||
- [x] **扩展支持**:基于 MoeHoo 的Native 可实现发包与收包
|
|
||||||
|
|
||||||
## 使用猫猫
|
## 使用框架
|
||||||
|
|
||||||
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
|
||||||
|
|
||||||
**首次使用**请务必查看如下文档看使用教程
|
**首次使用**请务必查看如下文档看使用教程
|
||||||
|
|
||||||
### 文档地址
|
### 文档地址
|
||||||
[Github.IO](https://napneko.github.io/)
|
|
||||||
|
|
||||||
[Cloudflare.Worker](https://doc.napneko.icu/)
|
[Cloudflare.Worker](https://doc.napneko.icu/)
|
||||||
|
|
||||||
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
[Cloudflare.HKServer](https://napcat.napneko.icu/)
|
||||||
|
|
||||||
|
[Github.IO](https://napneko.github.io/)
|
||||||
|
|
||||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||||
|
|
||||||
|
[Server.Other](https://napcat.cyou/)
|
||||||
|
|
||||||
|
|
||||||
## 回家旅途
|
## 回家旅途
|
||||||
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
|
[QQ Group](https://qm.qq.com/q/haLGHixZ74)
|
||||||
|
|
||||||
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
|
## 感谢他们
|
||||||
|
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||||
|
|
||||||
## 猫猫朋友
|
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
|
||||||
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
|
|
||||||
|
|
||||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持
|
|
||||||
|
|
||||||
不过最最重要的 还是需要感谢屏幕前的你哦~
|
不过最最重要的 还是需要感谢屏幕前的你哦~
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 约法三章
|
## 延缓Native模块与NapCat对新版QQ适配
|
||||||
> [!CAUTION]\
|
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
|
||||||
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本项目存在相关性的信息**
|
|
||||||
|
|
||||||
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经授权二次分发或基于 NapCat 代码开发。**
|
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
|
||||||
|
|
||||||
|
因此此时推荐使用release指定版本
|
||||||
|
|
||||||
|
## 开源附加
|
||||||
|
|
||||||
|
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**
|
||||||
|
70
eslint.config.mjs
Normal file
70
eslint.config.mjs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||||
|
import _import from "eslint-plugin-import";
|
||||||
|
import { fixupPluginRules } from "@eslint/compat";
|
||||||
|
import globals from "globals";
|
||||||
|
import tsParser from "@typescript-eslint/parser";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
ignores: ["src/core/proto/"],
|
||||||
|
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": typescriptEslint,
|
||||||
|
import: fixupPluginRules(_import),
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: tsParser,
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
"import/parsers": {
|
||||||
|
"@typescript-eslint/parser": [".ts"],
|
||||||
|
},
|
||||||
|
|
||||||
|
"import/resolver": {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
indent: ["error", 4],
|
||||||
|
semi: ["error", "always"],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-async-promise-executor": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
files: ["**/.eslintrc.{js,cjs}"],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
ecmaVersion: 5,
|
||||||
|
sourceType: "commonjs",
|
||||||
|
},
|
||||||
|
}];
|
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
32
launcher/launcher-user.bat
Normal file
32
launcher/launcher-user.bat
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001
|
||||||
|
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||||
|
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||||
|
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||||
|
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||||
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
|
:loop_read
|
||||||
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
|
set RetString=%%b
|
||||||
|
goto :napcat_boot
|
||||||
|
)
|
||||||
|
|
||||||
|
:napcat_boot
|
||||||
|
for %%a in ("%RetString%") do (
|
||||||
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
|
)
|
||||||
|
|
||||||
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
|
if not exist "%QQpath%" (
|
||||||
|
echo provided QQ path is invalid: %QQpath%
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||||
|
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||||
|
|
||||||
|
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||||
|
|
||||||
|
pause
|
33
launcher/launcher-win10-user.bat
Normal file
33
launcher/launcher-win10-user.bat
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001
|
||||||
|
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
|
||||||
|
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
|
||||||
|
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
|
||||||
|
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
|
||||||
|
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
|
||||||
|
:loop_read
|
||||||
|
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||||
|
set RetString=%%b
|
||||||
|
goto :napcat_boot
|
||||||
|
)
|
||||||
|
|
||||||
|
:napcat_boot
|
||||||
|
for %%a in ("%RetString%") do (
|
||||||
|
set "pathWithoutUninstall=%%~dpa"
|
||||||
|
)
|
||||||
|
|
||||||
|
SET QQPath=%pathWithoutUninstall%QQ.exe
|
||||||
|
|
||||||
|
if not exist "%QQpath%" (
|
||||||
|
echo provided QQ path is invalid: %QQpath%
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
|
||||||
|
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
|
||||||
|
|
||||||
|
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
|
||||||
|
|
||||||
|
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
|
||||||
|
|
||||||
|
pause
|
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "qq-chat",
|
"name": "qq-chat",
|
||||||
"version": "9.9.16-28788",
|
"version": "9.9.16-29456",
|
||||||
"verHash": "73b0c8f6",
|
"verHash": "dd395162",
|
||||||
"linuxVersion": "3.2.13-28788",
|
"linuxVersion": "3.2.13-29456",
|
||||||
"linuxVerHash": "55fb6434",
|
"linuxVerHash": "e379390a",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "QQ",
|
"description": "QQ",
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
"qd": "externals/devtools/cli/index.js"
|
"qd": "externals/devtools/cli/index.js"
|
||||||
},
|
},
|
||||||
"main": "./loadNapCat.js",
|
"main": "./loadNapCat.js",
|
||||||
"buildVersion": "28788",
|
"buildVersion": "29456",
|
||||||
"isPureShell": true,
|
"isPureShell": true,
|
||||||
"isByteCodeShell": true,
|
"isByteCodeShell": true,
|
||||||
"platform": "win32",
|
"platform": "win32",
|
||||||
"eleArch": "x64"
|
"eleArch": "x64"
|
||||||
}
|
}
|
||||||
|
BIN
logo.png
BIN
logo.png
Binary file not shown.
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 335 KiB |
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "3.0.1",
|
"version": "4.17",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
24
napcat.webui/.gitignore
vendored
Normal file
24
napcat.webui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
3
napcat.webui/.vscode/extensions.json
vendored
Normal file
3
napcat.webui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
5
napcat.webui/README.md
Normal file
5
napcat.webui/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
52
napcat.webui/eslint.config.mjs
Normal file
52
napcat.webui/eslint.config.mjs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import globals from 'globals';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
import prettier from 'eslint-plugin-prettier/recommended';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...ts.configs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-var-requires': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...vue.configs['flat/base'],
|
||||||
|
{
|
||||||
|
files: ['*.vue', '**/*.vue'],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
indent: ['error', 4],
|
||||||
|
semi: ['error', 'always'],
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
|
'@typescript-eslint/no-var-requires': 'warn',
|
||||||
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
'vue/v-for-delimiter-style': ['error', 'in'],
|
||||||
|
'vue/require-name-property': 'warn',
|
||||||
|
'vue/prefer-true-attribute-shorthand': 'warn',
|
||||||
|
'prefer-arrow-callback': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
13
napcat.webui/index.html
Normal file
13
napcat.webui/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>NapCat WebUI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
31
napcat.webui/package.json
Normal file
31
napcat.webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "napcat.webui",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||||
|
"webui:dev": "vite",
|
||||||
|
"webui:build": "vue-tsc -b && vite build",
|
||||||
|
"webui:preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"tdesign-vue-next": "^1.10.3",
|
||||||
|
"vue": "^3.5.12",
|
||||||
|
"vue-router": "^4.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.4",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-vue": "^9.31.0",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vue-tsc": "^2.1.8"
|
||||||
|
}
|
||||||
|
}
|
BIN
napcat.webui/public/logo.png
Normal file
BIN
napcat.webui/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 335 KiB |
1
napcat.webui/public/vite.svg
Normal file
1
napcat.webui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
7
napcat.webui/src/App.vue
Normal file
7
napcat.webui/src/App.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
BIN
napcat.webui/src/assets/Sotheby.ttf
Normal file
BIN
napcat.webui/src/assets/Sotheby.ttf
Normal file
Binary file not shown.
BIN
napcat.webui/src/assets/logo.png
Normal file
BIN
napcat.webui/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 335 KiB |
1
napcat.webui/src/assets/vue.svg
Normal file
1
napcat.webui/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
185
napcat.webui/src/backend/shell.ts
Normal file
185
napcat.webui/src/backend/shell.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||||
|
|
||||||
|
export class QQLoginManager {
|
||||||
|
private retCredential: string;
|
||||||
|
private readonly apiPrefix: string;
|
||||||
|
|
||||||
|
//调试时http://127.0.0.1:6099/api 打包时 ../api
|
||||||
|
constructor(retCredential: string, apiPrefix: string = '../api') {
|
||||||
|
this.retCredential = retCredential;
|
||||||
|
this.apiPrefix = apiPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
public async GetOB11Config(): Promise<OneBotConfig> {
|
||||||
|
try {
|
||||||
|
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (ConfigResponse.status == 200) {
|
||||||
|
const ConfigResponseJson = await ConfigResponse.json();
|
||||||
|
if (ConfigResponseJson.code == 0) {
|
||||||
|
return ConfigResponseJson?.data as OneBotConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting OB11 config:', error);
|
||||||
|
}
|
||||||
|
return {} as OneBotConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ config: JSON.stringify(config) }),
|
||||||
|
});
|
||||||
|
if (ConfigResponse.status == 200) {
|
||||||
|
const ConfigResponseJson = await ConfigResponse.json();
|
||||||
|
if (ConfigResponseJson.code == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting OB11 config:', error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkQQLoginStatus(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (QQLoginResponse.status == 200) {
|
||||||
|
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||||
|
if (QQLoginResponseJson.code == 0) {
|
||||||
|
return QQLoginResponseJson.data.isLogin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking QQ login status:', error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkWebUiLogined(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (LoginResponse.status == 200) {
|
||||||
|
const LoginResponseJson = await LoginResponse.json();
|
||||||
|
if (LoginResponseJson.code == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking web UI login status:', error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loginWithToken(token: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: token }),
|
||||||
|
});
|
||||||
|
const loginResponseJson = await loginResponse.json();
|
||||||
|
const retCode = loginResponseJson.code;
|
||||||
|
if (retCode === 0) {
|
||||||
|
this.retCredential = loginResponseJson.data.Credential;
|
||||||
|
return this.retCredential;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error logging in with token:', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQQLoginQrcode(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (QQLoginResponse.status == 200) {
|
||||||
|
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||||
|
if (QQLoginResponseJson.code == 0) {
|
||||||
|
return QQLoginResponseJson.data.qrcode || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting QQ login QR code:', error);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getQQQuickLoginList(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (QQLoginResponse.status == 200) {
|
||||||
|
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||||
|
if (QQLoginResponseJson.code == 0) {
|
||||||
|
return QQLoginResponseJson.data || [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting QQ quick login list:', error);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
|
||||||
|
try {
|
||||||
|
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + this.retCredential,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ uin: uin }),
|
||||||
|
});
|
||||||
|
if (QQLoginResponse.status == 200) {
|
||||||
|
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||||
|
if (QQLoginResponseJson.code == 0) {
|
||||||
|
return { result: true, errMsg: '' };
|
||||||
|
} else {
|
||||||
|
return { result: false, errMsg: QQLoginResponseJson.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting quick login:', error);
|
||||||
|
}
|
||||||
|
return { result: false, errMsg: '接口异常' };
|
||||||
|
}
|
||||||
|
}
|
55
napcat.webui/src/components/Dashboard.vue
Normal file
55
napcat.webui/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
||||||
|
<div class="content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import SidebarMenu from './webui/Nav.vue';
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
value: string;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
route: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = ref<MenuItem[]>([
|
||||||
|
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
|
||||||
|
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
|
||||||
|
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
|
||||||
|
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
||||||
|
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-menu {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
/* padding: 20px; */
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
167
napcat.webui/src/components/QQLogin.vue
Normal file
167
napcat.webui/src/components/QQLogin.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2 class="sotheby-font">QQ Login</h2>
|
||||||
|
<div class="login-methods">
|
||||||
|
<t-button
|
||||||
|
id="quick-login"
|
||||||
|
class="login-method"
|
||||||
|
:class="{ active: loginMethod === 'quick' }"
|
||||||
|
@click="loginMethod = 'quick'"
|
||||||
|
>Quick Login</t-button
|
||||||
|
>
|
||||||
|
<t-button
|
||||||
|
id="qrcode-login"
|
||||||
|
class="login-method"
|
||||||
|
:class="{ active: loginMethod === 'qrcode' }"
|
||||||
|
@click="loginMethod = 'qrcode'"
|
||||||
|
>QR Code</t-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||||
|
<t-select
|
||||||
|
id="quick-login-select"
|
||||||
|
v-model="selectedAccount"
|
||||||
|
placeholder="Select Account"
|
||||||
|
@change="selectAccount"
|
||||||
|
>
|
||||||
|
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
||||||
|
</t-select>
|
||||||
|
</div>
|
||||||
|
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
||||||
|
<canvas ref="qrcodeCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loginMethod = ref<'quick' | 'qrcode'>('quick');
|
||||||
|
const quickLoginList = ref<string[]>([]);
|
||||||
|
const selectedAccount = ref<string>('');
|
||||||
|
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
||||||
|
let heartBeatTimer: number | null = null;
|
||||||
|
|
||||||
|
const selectAccount = async (accountName: string): Promise<void> => {
|
||||||
|
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||||
|
if (result) {
|
||||||
|
await MessagePlugin.success('登录成功即将跳转');
|
||||||
|
await router.push({ path: '/dashboard/basic-info' });
|
||||||
|
} else {
|
||||||
|
await MessagePlugin.error('登录失败,' + errMsg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
|
||||||
|
if (!canvas) {
|
||||||
|
console.error('Canvas element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
|
||||||
|
if (error) {
|
||||||
|
console.error('Error generating QR Code:', error);
|
||||||
|
} else {
|
||||||
|
console.log('QR Code generated!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeartBeat = async (): Promise<void> => {
|
||||||
|
const isLogined = await qqLoginManager.checkQQLoginStatus();
|
||||||
|
if (isLogined) {
|
||||||
|
if (heartBeatTimer) {
|
||||||
|
clearInterval(heartBeatTimer);
|
||||||
|
}
|
||||||
|
await router.push({ path: '/dashboard/basic-info' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const InitPages = async (): Promise<void> => {
|
||||||
|
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||||
|
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
|
||||||
|
generateQrCode(qrcodeData, qrcodeCanvas.value);
|
||||||
|
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
InitPages();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: white;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.login-container {
|
||||||
|
width: 90%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-methods {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-method {
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-method.active {
|
||||||
|
background-color: #e6f0ff;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form,
|
||||||
|
.qrcode {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sotheby-font {
|
||||||
|
font-family: Sotheby, Helvetica, monospace;
|
||||||
|
font-size: 3.125rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #888;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
151
napcat.webui/src/components/WebUiLogin.vue
Normal file
151
napcat.webui/src/components/WebUiLogin.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2 class="sotheby-font">WebUi Login</h2>
|
||||||
|
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
||||||
|
<t-form-item name="password">
|
||||||
|
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
||||||
|
<template #prefix-icon>
|
||||||
|
<lock-on-icon />
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item>
|
||||||
|
<t-button theme="primary" type="submit" block>登录</t-button>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
<div class="footer">Power By NapCat.WebUi</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import '../css/style.css';
|
||||||
|
import '../css/font.css';
|
||||||
|
import { reactive, onMounted } from 'vue';
|
||||||
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
|
import { LockOnIcon } from 'tdesign-icons-vue-next';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData: FormData = reactive({
|
||||||
|
token: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLoginSuccess = async (credential: string) => {
|
||||||
|
localStorage.setItem('auth', credential);
|
||||||
|
await checkLoginStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginFailure = (message: string) => {
|
||||||
|
MessagePlugin.error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkLoginStatus = async () => {
|
||||||
|
const storedCredential = localStorage.getItem('auth');
|
||||||
|
if (!storedCredential) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loginManager = new QQLoginManager(storedCredential);
|
||||||
|
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
|
||||||
|
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
|
||||||
|
if (!isWenUiLoggedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
|
||||||
|
if (isQQLoggedIn) {
|
||||||
|
await router.push({ path: '/dashboard/basic-info' });
|
||||||
|
} else {
|
||||||
|
await router.push({ path: '/qqlogin' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginWithToken = async (token: string) => {
|
||||||
|
const loginManager = new QQLoginManager('');
|
||||||
|
const credential = await loginManager.loginWithToken(token);
|
||||||
|
if (credential) {
|
||||||
|
await handleLoginSuccess(credential);
|
||||||
|
} else {
|
||||||
|
handleLoginFailure('登录失败,请检查Token');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const token = url.searchParams.get('token');
|
||||||
|
if (token) {
|
||||||
|
loginWithToken(token);
|
||||||
|
}
|
||||||
|
checkLoginStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
||||||
|
if (validateResult) {
|
||||||
|
await loginWithToken(formData.token);
|
||||||
|
} else {
|
||||||
|
handleLoginFailure('请填写Token');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: white;
|
||||||
|
max-width: 400px;
|
||||||
|
min-width: 300px;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.login-container {
|
||||||
|
width: 90%;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdesign-demo-block-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdesign-demo-block-column-large {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdesign-demo-block-row {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sotheby-font {
|
||||||
|
font-family: Sotheby, Helvetica, monospace;
|
||||||
|
font-size: 3.125rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #888;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
71
napcat.webui/src/components/webui/Nav.vue
Normal file
71
napcat.webui/src/components/webui/Nav.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
||||||
|
<template #logo> </template>
|
||||||
|
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||||
|
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||||
|
<template #icon>
|
||||||
|
<t-icon :name="item.icon" />
|
||||||
|
</template>
|
||||||
|
{{ item.label }}
|
||||||
|
</t-menu-item>
|
||||||
|
</router-link>
|
||||||
|
<template #operations>
|
||||||
|
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
|
||||||
|
<template #icon><t-icon :name="iconName" /></template>
|
||||||
|
</t-button>
|
||||||
|
</template>
|
||||||
|
</t-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps } from 'vue';
|
||||||
|
|
||||||
|
type MenuItem = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
route: string;
|
||||||
|
icon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
menuItems: MenuItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
||||||
|
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
||||||
|
|
||||||
|
const changeCollapsed = (): void => {
|
||||||
|
collapsed.value = !collapsed.value;
|
||||||
|
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
||||||
|
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 200px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-menu {
|
||||||
|
width: 100px; /* 移动端侧边栏宽度 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
6
napcat.webui/src/css/font.css
Normal file
6
napcat.webui/src/css/font.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Sotheby';
|
||||||
|
src: url('../assets/Sotheby.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
84
napcat.webui/src/css/style.css
Normal file
84
napcat.webui/src/css/style.css
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
62
napcat.webui/src/main.ts
Normal file
62
napcat.webui/src/main.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import {
|
||||||
|
Button as TButton,
|
||||||
|
Input as TInput,
|
||||||
|
Form as TForm,
|
||||||
|
FormItem as TFormItem,
|
||||||
|
Select as TSelect,
|
||||||
|
Option as TOption,
|
||||||
|
Menu as TMenu,
|
||||||
|
MenuItem as TMenuItem,
|
||||||
|
Icon as TIcon,
|
||||||
|
Submenu as TSubmenu,
|
||||||
|
Col as TCol,
|
||||||
|
Row as TRow,
|
||||||
|
Card as TCard,
|
||||||
|
Divider as TDivider,
|
||||||
|
Link as TLink,
|
||||||
|
List as TList,
|
||||||
|
Alert as TAlert,
|
||||||
|
Tag as TTag,
|
||||||
|
ListItem as TListItem,
|
||||||
|
Tabs as TTabs,
|
||||||
|
TabPanel as TTabPanel,
|
||||||
|
Space as TSpace,
|
||||||
|
Checkbox as TCheckbox,
|
||||||
|
Popup as TPopup,
|
||||||
|
Dialog as TDialog,
|
||||||
|
Switch as TSwitch,
|
||||||
|
} from 'tdesign-vue-next';
|
||||||
|
import { router } from './router';
|
||||||
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.use(router);
|
||||||
|
app.use(TButton);
|
||||||
|
app.use(TInput);
|
||||||
|
app.use(TForm);
|
||||||
|
app.use(TFormItem);
|
||||||
|
app.use(TSelect);
|
||||||
|
app.use(TOption);
|
||||||
|
app.use(TMenu);
|
||||||
|
app.use(TMenuItem);
|
||||||
|
app.use(TIcon);
|
||||||
|
app.use(TSubmenu);
|
||||||
|
app.use(TCol);
|
||||||
|
app.use(TRow);
|
||||||
|
app.use(TCard);
|
||||||
|
app.use(TDivider);
|
||||||
|
app.use(TLink);
|
||||||
|
app.use(TList);
|
||||||
|
app.use(TAlert);
|
||||||
|
app.use(TTag);
|
||||||
|
app.use(TListItem);
|
||||||
|
app.use(TTabs);
|
||||||
|
app.use(TTabPanel);
|
||||||
|
app.use(TSpace);
|
||||||
|
app.use(TCheckbox);
|
||||||
|
app.use(TPopup);
|
||||||
|
app.use(TDialog);
|
||||||
|
app.use(TSwitch);
|
||||||
|
app.mount('#app');
|
66
napcat.webui/src/pages/AboutUs.vue
Normal file
66
napcat.webui/src/pages/AboutUs.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about-us">
|
||||||
|
<div>
|
||||||
|
<t-divider content="面板关于信息" align="left" />
|
||||||
|
<t-alert theme="success" message="NapCat.WebUi is running" />
|
||||||
|
<t-list class="list">
|
||||||
|
<t-list-item class="list-item">
|
||||||
|
<span class="item-label">开发人员:</span>
|
||||||
|
<span class="item-content">
|
||||||
|
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
|
||||||
|
</span>
|
||||||
|
</t-list-item>
|
||||||
|
<t-list-item class="list-item">
|
||||||
|
<span class="item-label">版本信息:</span>
|
||||||
|
<span class="item-content">
|
||||||
|
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
|
||||||
|
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
|
||||||
|
<t-tag class="tag-item" theme="success">
|
||||||
|
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
|
||||||
|
</t-tag>
|
||||||
|
</span>
|
||||||
|
</t-list-item>
|
||||||
|
</t-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import pkg from '../../package.json';
|
||||||
|
import { napCatVersion } from '../../../src/common/version';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.about-us {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
6
napcat.webui/src/pages/BasicInfo.vue
Normal file
6
napcat.webui/src/pages/BasicInfo.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div class="basic-info">
|
||||||
|
<h1>面板基础信息</h1>
|
||||||
|
<p>这里显示面板的基础信息。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
6
napcat.webui/src/pages/Log.vue
Normal file
6
napcat.webui/src/pages/Log.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<div class="log-view">
|
||||||
|
<h1>面板日志信息</h1>
|
||||||
|
<p>这里显示面板的日志信息。</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
249
napcat.webui/src/pages/NetWork.vue
Normal file
249
napcat.webui/src/pages/NetWork.vue
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<template>
|
||||||
|
<t-space class="full-space">
|
||||||
|
<template v-if="clientPanelData.length > 0">
|
||||||
|
<t-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
:addable="true"
|
||||||
|
theme="card"
|
||||||
|
@add="showAddTabDialog"
|
||||||
|
@remove="removeTab"
|
||||||
|
class="full-tabs"
|
||||||
|
>
|
||||||
|
<t-tab-panel
|
||||||
|
v-for="(config, idx) in clientPanelData"
|
||||||
|
:key="idx"
|
||||||
|
:label="config.name"
|
||||||
|
:removable="true"
|
||||||
|
:value="idx"
|
||||||
|
class="full-tab-panel"
|
||||||
|
>
|
||||||
|
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
|
||||||
|
<div class="button-container">
|
||||||
|
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
|
||||||
|
</div>
|
||||||
|
</t-tab-panel>
|
||||||
|
</t-tabs>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
|
||||||
|
</template>
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="isDialogVisible"
|
||||||
|
header="添加网络配置"
|
||||||
|
@close="isDialogVisible = false"
|
||||||
|
@confirm="addTab"
|
||||||
|
>
|
||||||
|
<t-form ref="form" :model="newTab">
|
||||||
|
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
|
||||||
|
<t-input v-model="newTab.name" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
|
||||||
|
<t-select v-model="newTab.type">
|
||||||
|
<t-option value="httpServers">HTTP 服务器</t-option>
|
||||||
|
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||||
|
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
||||||
|
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
||||||
|
</t-select>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</t-dialog>
|
||||||
|
</t-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue';
|
||||||
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
|
import {
|
||||||
|
httpServerDefaultConfigs,
|
||||||
|
httpClientDefaultConfigs,
|
||||||
|
websocketServerDefaultConfigs,
|
||||||
|
websocketClientDefaultConfigs,
|
||||||
|
HttpClientConfig,
|
||||||
|
HttpServerConfig,
|
||||||
|
WebsocketClientConfig,
|
||||||
|
WebsocketServerConfig,
|
||||||
|
NetworkConfig,
|
||||||
|
OneBotConfig,
|
||||||
|
mergeOneBotConfigs,
|
||||||
|
} from '../../../src/onebot/config/config';
|
||||||
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
||||||
|
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
|
||||||
|
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
|
||||||
|
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
|
||||||
|
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
|
||||||
|
|
||||||
|
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
|
||||||
|
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
|
||||||
|
type ComponentUnion =
|
||||||
|
| typeof HttpServerComponent
|
||||||
|
| typeof HttpClientComponent
|
||||||
|
| typeof WebsocketServerComponent
|
||||||
|
| typeof WebsocketClientComponent;
|
||||||
|
|
||||||
|
const componentMap: Record<ConfigKey, ComponentUnion> = {
|
||||||
|
httpServers: HttpServerComponent,
|
||||||
|
httpClients: HttpClientComponent,
|
||||||
|
websocketServers: WebsocketServerComponent,
|
||||||
|
websocketClients: WebsocketClientComponent,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
|
||||||
|
httpServers: httpServerDefaultConfigs,
|
||||||
|
httpClients: httpClientDefaultConfigs,
|
||||||
|
websocketServers: websocketServerDefaultConfigs,
|
||||||
|
websocketClients: websocketClientDefaultConfigs,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ConfigMap {
|
||||||
|
httpServers: HttpServerConfig;
|
||||||
|
httpClients: HttpClientConfig;
|
||||||
|
websocketServers: WebsocketServerConfig;
|
||||||
|
websocketClients: WebsocketClientConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientPanel<K extends ConfigKey = ConfigKey> {
|
||||||
|
name: string;
|
||||||
|
key: K;
|
||||||
|
data: ConfigMap[K];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTab = ref<number>(0);
|
||||||
|
const isDialogVisible = ref(false);
|
||||||
|
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
|
||||||
|
const clientPanelData: Ref<ClientPanel[]> = ref([]);
|
||||||
|
|
||||||
|
const getComponent = (type: ConfigKey) => {
|
||||||
|
return componentMap[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||||
|
const storedCredential = localStorage.getItem('auth');
|
||||||
|
if (!storedCredential) {
|
||||||
|
console.error('No stored credential found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loginManager = new QQLoginManager(storedCredential);
|
||||||
|
return await loginManager.GetOB11Config();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||||
|
const storedCredential = localStorage.getItem('auth');
|
||||||
|
if (!storedCredential) {
|
||||||
|
console.error('No stored credential found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const loginManager = new QQLoginManager(storedCredential);
|
||||||
|
return await loginManager.SetOB11Config(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToPanel = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => {
|
||||||
|
configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addConfigDataToPanel = (data: NetworkConfig) => {
|
||||||
|
(Object.keys(data) as ConfigKey[]).forEach((key) => {
|
||||||
|
addToPanel(data[key], key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePanelData = (): NetworkConfig => {
|
||||||
|
const result: NetworkConfig = {
|
||||||
|
httpServers: [],
|
||||||
|
httpClients: [],
|
||||||
|
websocketServers: [],
|
||||||
|
websocketClients: [],
|
||||||
|
};
|
||||||
|
clientPanelData.value.forEach((panel) => {
|
||||||
|
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const userConfig = await getOB11Config();
|
||||||
|
if (!userConfig) return;
|
||||||
|
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||||
|
addConfigDataToPanel(mergedConfig.network);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
const config = parsePanelData();
|
||||||
|
const userConfig = await getOB11Config();
|
||||||
|
if (!userConfig) {
|
||||||
|
await MessagePlugin.error('无法获取配置!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userConfig.network = config;
|
||||||
|
const success = await setOB11Config(userConfig);
|
||||||
|
if (success) {
|
||||||
|
await MessagePlugin.success('配置保存成功');
|
||||||
|
} else {
|
||||||
|
await MessagePlugin.error('配置保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAddTabDialog = () => {
|
||||||
|
newTab.value = { name: '', type: 'httpServers' };
|
||||||
|
isDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTab = async () => {
|
||||||
|
const { name, type } = newTab.value;
|
||||||
|
if (clientPanelData.value.some((panel) => panel.name === name)) {
|
||||||
|
await MessagePlugin.error('选项卡名称已存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const defaultConfig = structuredClone(defaultConfigMap[type]);
|
||||||
|
defaultConfig.name = name;
|
||||||
|
clientPanelData.value.push({ name, data: defaultConfig, key: type });
|
||||||
|
isDialogVisible.value = false;
|
||||||
|
await nextTick();
|
||||||
|
activeTab.value = clientPanelData.value.length - 1;
|
||||||
|
await MessagePlugin.success('选项卡添加成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
|
||||||
|
clientPanelData.value.splice(payload.index, 1);
|
||||||
|
activeTab.value = Math.max(0, activeTab.value - 1);
|
||||||
|
await saveConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.full-space {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-tabs {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-tab-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
134
napcat.webui/src/pages/OtherConfig.vue
Normal file
134
napcat.webui/src/pages/OtherConfig.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<t-divider content="其余配置" align="left" />
|
||||||
|
</div>
|
||||||
|
<div class="other-config-container">
|
||||||
|
<div class="other-config">
|
||||||
|
<t-form ref="form" :model="otherConfig" class="form">
|
||||||
|
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
||||||
|
<t-input v-model="otherConfig.musicSignUrl" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
||||||
|
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
<div class="button-container">
|
||||||
|
<t-button @click="saveConfig">保存</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
|
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||||
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
|
||||||
|
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||||
|
musicSignUrl: '',
|
||||||
|
enableLocalFile2Url: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||||
|
const storedCredential = localStorage.getItem('auth');
|
||||||
|
if (!storedCredential) {
|
||||||
|
console.error('No stored credential found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loginManager = new QQLoginManager(storedCredential);
|
||||||
|
return await loginManager.GetOB11Config();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||||
|
const storedCredential = localStorage.getItem('auth');
|
||||||
|
if (!storedCredential) {
|
||||||
|
console.error('No stored credential found');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const loginManager = new QQLoginManager(storedCredential);
|
||||||
|
return await loginManager.SetOB11Config(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
try {
|
||||||
|
const userConfig = await getOB11Config();
|
||||||
|
if (userConfig) {
|
||||||
|
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
||||||
|
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
try {
|
||||||
|
const userConfig = await getOB11Config();
|
||||||
|
if (userConfig) {
|
||||||
|
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
||||||
|
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
||||||
|
const success = await setOB11Config(userConfig);
|
||||||
|
if (success) {
|
||||||
|
MessagePlugin.success('配置保存成功');
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error('配置保存失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving config:', error);
|
||||||
|
MessagePlugin.error('配置保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadConfig();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.other-config-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-config {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.form-item {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item t-input,
|
||||||
|
.form-item t-switch {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
22
napcat.webui/src/pages/network/EmptyStateComponent.vue
Normal file
22
napcat.webui/src/pages/network/EmptyStateComponent.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>当前没有网络配置</p>
|
||||||
|
<t-button @click="showAddTabDialog">添加网络配置</t-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from 'vue';
|
||||||
|
defineProps<{ showAddTabDialog: () => void }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
68
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
68
napcat.webui/src/pages/network/HttpClientComponent.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<h3>HTTP Client 配置</h3>
|
||||||
|
<t-form>
|
||||||
|
<t-form-item label="启用">
|
||||||
|
<t-checkbox v-model="config.enable" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="URL">
|
||||||
|
<t-input v-model="config.url" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="消息格式">
|
||||||
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="报告自身消息">
|
||||||
|
<t-checkbox v-model="config.reportSelfMessage" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="Token">
|
||||||
|
<t-input v-model="config.token" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="调试模式">
|
||||||
|
<t-checkbox v-model="config.debug" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, watch } from 'vue';
|
||||||
|
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: HttpClientConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const messageFormatOptions = ref([
|
||||||
|
{ label: 'Array', value: 'array' },
|
||||||
|
{ label: 'String', value: 'string' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.config.messagePostFormat,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== 'array' && newValue !== 'string') {
|
||||||
|
props.config.messagePostFormat = 'array';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
74
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
74
napcat.webui/src/pages/network/HttpServerComponent.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<h3>HTTP Server 配置</h3>
|
||||||
|
<t-form>
|
||||||
|
<t-form-item label="启用">
|
||||||
|
<t-checkbox v-model="config.enable" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="端口">
|
||||||
|
<t-input v-model.number="config.port" type="number" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="主机">
|
||||||
|
<t-input v-model="config.host" type="text" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="启用 CORS">
|
||||||
|
<t-checkbox v-model="config.enableCors" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="启用 WS">
|
||||||
|
<t-checkbox v-model="config.enableWebsocket" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="消息格式">
|
||||||
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="Token">
|
||||||
|
<t-input v-model="config.token" type="text" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="调试模式">
|
||||||
|
<t-checkbox v-model="config.debug" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, watch } from 'vue';
|
||||||
|
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: HttpServerConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const messageFormatOptions = ref([
|
||||||
|
{ label: 'Array', value: 'array' },
|
||||||
|
{ label: 'String', value: 'string' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.config.messagePostFormat,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== 'array' && newValue !== 'string') {
|
||||||
|
props.config.messagePostFormat = 'array';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
71
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
71
napcat.webui/src/pages/network/WebsocketClientComponent.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<h3>WebSocket Client 配置</h3>
|
||||||
|
<t-form>
|
||||||
|
<t-form-item label="启用">
|
||||||
|
<t-checkbox v-model="config.enable" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="URL">
|
||||||
|
<t-input v-model="config.url" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="消息格式">
|
||||||
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="报告自身消息">
|
||||||
|
<t-checkbox v-model="config.reportSelfMessage" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="Token">
|
||||||
|
<t-input v-model="config.token" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="调试模式">
|
||||||
|
<t-checkbox v-model="config.debug" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="心跳间隔">
|
||||||
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, watch } from 'vue';
|
||||||
|
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: WebsocketClientConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const messageFormatOptions = ref([
|
||||||
|
{ label: 'Array', value: 'array' },
|
||||||
|
{ label: 'String', value: 'string' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.config.messagePostFormat,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== 'array' && newValue !== 'string') {
|
||||||
|
props.config.messagePostFormat = 'array';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
77
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
77
napcat.webui/src/pages/network/WebsocketServerComponent.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="form-container">
|
||||||
|
<h3>WebSocket Server 配置</h3>
|
||||||
|
<t-form>
|
||||||
|
<t-form-item label="启用">
|
||||||
|
<t-checkbox v-model="config.enable" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="主机">
|
||||||
|
<t-input v-model="config.host" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="端口">
|
||||||
|
<t-input v-model.number="config.port" type="number" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="消息格式">
|
||||||
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="上报自身消息">
|
||||||
|
<t-checkbox v-model="config.reportSelfMessage" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="Token">
|
||||||
|
<t-input v-model="config.token" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="强制推送事件">
|
||||||
|
<t-checkbox v-model="config.enableForcePushEvent" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="调试模式">
|
||||||
|
<t-checkbox v-model="config.debug" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="心跳间隔">
|
||||||
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref, watch } from 'vue';
|
||||||
|
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: WebsocketServerConfig;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const messageFormatOptions = ref([
|
||||||
|
{ label: 'Array', value: 'array' },
|
||||||
|
{ label: 'String', value: 'string' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.config.messagePostFormat,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue !== 'array' && newValue !== 'string') {
|
||||||
|
props.config.messagePostFormat = 'array';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
32
napcat.webui/src/router/index.ts
Normal file
32
napcat.webui/src/router/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
|
||||||
|
import Dashboard from '../components/Dashboard.vue';
|
||||||
|
import BasicInfo from '../pages/BasicInfo.vue';
|
||||||
|
import AboutUs from '../pages/AboutUs.vue';
|
||||||
|
import LogView from '../pages/Log.vue';
|
||||||
|
import NetWork from '../pages/NetWork.vue';
|
||||||
|
import QQLogin from '../components/QQLogin.vue';
|
||||||
|
import WebUiLogin from '../components/WebUiLogin.vue';
|
||||||
|
import OtherConfig from '../pages/OtherConfig.vue';
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
{ path: '/', redirect: '/webui' },
|
||||||
|
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
|
||||||
|
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
component: Dashboard,
|
||||||
|
children: [
|
||||||
|
{ path: '', redirect: 'basic-info' },
|
||||||
|
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
|
||||||
|
{ path: 'network-config', component: NetWork, name: 'NetWork' },
|
||||||
|
{ path: 'log-view', component: LogView, name: 'LogView' },
|
||||||
|
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
|
||||||
|
{ path: 'about-us', component: AboutUs, name: 'AboutUs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
1
napcat.webui/src/vite-env.d.ts
vendored
Normal file
1
napcat.webui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
34
napcat.webui/tsconfig.json
Normal file
34
napcat.webui/tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "vue",
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"references": [{"path": "./tsconfig.node.json"}]
|
||||||
|
}
|
11
napcat.webui/tsconfig.node.json
Normal file
11
napcat.webui/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strictNullChecks": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
30
napcat.webui/vite.config.ts
Normal file
30
napcat.webui/vite.config.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
base: './',
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:6099',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
52
package.json
52
package.json
@@ -2,17 +2,24 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.0.1",
|
"version": "4.17",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:framework": "vite build --mode framework",
|
"build:framework": "npm run build:webui && vite build --mode framework",
|
||||||
"build:shell": "vite build --mode shell",
|
"build:shell": "npm run build:webui && vite build --mode shell",
|
||||||
"build:webui": "cd ./src/webui && vite build",
|
"build:webui": "cd napcat.webui && vite build",
|
||||||
"lint": "eslint --fix src/**/*.{js,ts}",
|
"dev:framework": "vite build --mode framework",
|
||||||
|
"dev:shell": "vite build --mode shell",
|
||||||
|
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||||
|
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||||
"depend": "cd dist && npm install --omit=dev"
|
"depend": "cd dist && npm install --omit=dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
|
"@eslint/compat": "^1.2.2",
|
||||||
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
"@log4js-node/log4js-api": "^1.0.2",
|
"@log4js-node/log4js-api": "^1.0.2",
|
||||||
|
"@napneko/nap-proto-core": "^0.0.4",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
@@ -23,30 +30,31 @@
|
|||||||
"@types/ws": "^8.5.12",
|
"@types/ws": "^8.5.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||||
"@typescript-eslint/parser": "^8.3.0",
|
"@typescript-eslint/parser": "^8.3.0",
|
||||||
"eslint": "^8.57.0",
|
"ajv": "^8.13.0",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"eslint": "^9.14.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"fast-xml-parser": "^4.3.6",
|
||||||
|
"file-type": "^19.0.0",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"image-size": "^1.1.1",
|
||||||
|
"json-schema-to-ts": "^3.1.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.6",
|
||||||
"vite-plugin-cp": "^4.0.8",
|
"vite-plugin-cp": "^4.0.8",
|
||||||
"vite-tsconfig-paths": "^4.3.2",
|
"vite-tsconfig-paths": "^5.1.0",
|
||||||
"@protobuf-ts/runtime": "^2.9.4",
|
"winston": "^3.17.0"
|
||||||
"ajv": "^8.13.0",
|
|
||||||
"fast-xml-parser": "^4.3.6",
|
|
||||||
"chalk": "^5.3.0",
|
|
||||||
"commander": "^12.1.0",
|
|
||||||
"async-mutex": "^0.5.0",
|
|
||||||
"file-type": "^19.0.0",
|
|
||||||
"json-schema-to-ts": "^3.1.0",
|
|
||||||
"image-size": "^1.1.1",
|
|
||||||
"cors": "^2.8.5"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"qrcode-terminal": "^0.12.0",
|
"express": "^5.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"express": "^5.0.0-beta.2",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"log4js": "^6.9.1",
|
|
||||||
"silk-wasm": "^3.6.1",
|
"silk-wasm": "^3.6.1",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0",
|
||||||
|
"piscina": "^4.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,16 +4,27 @@ const process = require("process");
|
|||||||
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
|
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
|
||||||
try {
|
try {
|
||||||
const packageJson = require("../package.json");
|
const packageJson = require("../package.json");
|
||||||
|
const manifsetJson = require("../manifest.json");
|
||||||
|
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
const targetVersion = process.env.VERSION;
|
const targetVersion = process.env.VERSION;
|
||||||
|
|
||||||
|
const manifestCurrentVersion = manifsetJson.version;
|
||||||
|
const manifestTargetVersion = process.env.VERSION;
|
||||||
|
|
||||||
console.log("[NapCat] [CheckVersion] currentVersion:", currentVersion, "targetVersion:", targetVersion);
|
console.log("[NapCat] [CheckVersion] currentVersion:", currentVersion, "targetVersion:", targetVersion);
|
||||||
|
console.log("[NapCat] [CheckVersion] manifestCurrentVersion:", manifestCurrentVersion, "manifestTargetVersion:", manifestTargetVersion);
|
||||||
|
|
||||||
// 验证 targetVersion 格式
|
// 验证 targetVersion 格式
|
||||||
if (!targetVersion || typeof targetVersion !== 'string') {
|
if (!targetVersion || typeof targetVersion !== 'string') {
|
||||||
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
|
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 验证 manifestTargetVersion 格式
|
||||||
|
if (!manifestTargetVersion || typeof manifestTargetVersion !== 'string') {
|
||||||
|
console.log("[NapCat] [CheckVersion] manifest目标版本格式不正确或未设置!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 写入脚本文件的统一函数
|
// 写入脚本文件的统一函数
|
||||||
const writeScriptToFile = (content) => {
|
const writeScriptToFile = (content) => {
|
||||||
@@ -21,7 +32,7 @@ try {
|
|||||||
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
|
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentVersion === targetVersion) {
|
if (currentVersion === targetVersion && manifestCurrentVersion === manifestTargetVersion) {
|
||||||
// 不需要更新版本,写入一个简单的脚本
|
// 不需要更新版本,写入一个简单的脚本
|
||||||
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
|
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
|
||||||
writeScriptToFile(simpleScript);
|
writeScriptToFile(simpleScript);
|
||||||
@@ -29,11 +40,13 @@ try {
|
|||||||
// 更新版本,构建安全的sed命令
|
// 更新版本,构建安全的sed命令
|
||||||
const safeScriptContent = `
|
const safeScriptContent = `
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
git config --global user.email "bot@test.wumiao.wang"
|
git config --global user.email "nanaeonn@outlook.com"
|
||||||
git config --global user.name "Version"
|
git config --global user.name "Mlikiowa"
|
||||||
sed -i "s/\\\"version\\\": \\\"${currentVersion}\\\"/\\\"version\\\": \\\"${targetVersion}\\\"/g" package.json
|
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
|
||||||
|
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
|
||||||
|
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore:version change"
|
git commit -m "release: v${targetVersion}"
|
||||||
git push -u origin main`;
|
git push -u origin main`;
|
||||||
writeScriptToFile(safeScriptContent);
|
writeScriptToFile(safeScriptContent);
|
||||||
}
|
}
|
||||||
|
9
src/common/audio-worker.ts
Normal file
9
src/common/audio-worker.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { encode } from "silk-wasm";
|
||||||
|
|
||||||
|
export interface EncodeArgs {
|
||||||
|
input: ArrayBufferView | ArrayBuffer
|
||||||
|
sampleRate: number
|
||||||
|
}
|
||||||
|
export default async ({ input, sampleRate }: EncodeArgs) => {
|
||||||
|
return await encode(input, sampleRate);
|
||||||
|
};
|
@@ -1,13 +1,23 @@
|
|||||||
|
import Piscina from 'piscina';
|
||||||
import fsPromise from 'fs/promises';
|
import fsPromise from 'fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
import { EncodeArgs } from "@/common/audio-worker";
|
||||||
|
|
||||||
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
|
||||||
const EXIT_CODES = [0, 255];
|
const EXIT_CODES = [0, 255];
|
||||||
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
|
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
|
||||||
|
|
||||||
|
async function getWorkerPath() {
|
||||||
|
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const piscina = new Piscina<EncodeArgs, EncodeResult>({
|
||||||
|
filename: await getWorkerPath(),
|
||||||
|
});
|
||||||
|
|
||||||
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
async function guessDuration(pttPath: string, logger: LogWrapper) {
|
||||||
const pttFileInfo = await fsPromise.stat(pttPath);
|
const pttFileInfo = await fsPromise.stat(pttPath);
|
||||||
@@ -41,8 +51,11 @@ async function convert(filePath: string, pcmPath: string, logger: LogWrapper): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleWavFile(
|
async function handleWavFile(
|
||||||
file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper
|
file: Buffer,
|
||||||
): Promise<{input: Buffer, sampleRate: number}> {
|
filePath: string,
|
||||||
|
pcmPath: string,
|
||||||
|
logger: LogWrapper
|
||||||
|
): Promise<{ input: Buffer; sampleRate: number }> {
|
||||||
const { fmt } = getWavFileInfo(file);
|
const { fmt } = getWavFileInfo(file);
|
||||||
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
|
||||||
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||||
@@ -60,8 +73,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
|||||||
const { input, sampleRate } = isWav(file)
|
const { input, sampleRate } = isWav(file)
|
||||||
? (await handleWavFile(file, filePath, pcmPath, logger))
|
? (await handleWavFile(file, filePath, pcmPath, logger))
|
||||||
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
|
||||||
const silk = await encode(input, sampleRate);
|
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
|
||||||
await fsPromise.writeFile(pttPath, silk.data);
|
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
|
||||||
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
|
||||||
return {
|
return {
|
||||||
converted: true,
|
converted: true,
|
||||||
@@ -86,4 +99,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
|||||||
logger.logError.bind(logger)('convert silk failed', error.stack);
|
logger.logError.bind(logger)('convert silk failed', error.stack);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,12 +8,12 @@ export abstract class ConfigBase<T> {
|
|||||||
configPath: string;
|
configPath: string;
|
||||||
configData: T = {} as T;
|
configData: T = {} as T;
|
||||||
|
|
||||||
protected constructor(name: string, core: NapCatCore, configPath: string) {
|
protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.configPath = configPath;
|
this.configPath = configPath;
|
||||||
fs.mkdirSync(this.configPath, { recursive: true });
|
fs.mkdirSync(this.configPath, { recursive: true });
|
||||||
this.read();
|
this.read(copy_default);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getKeys(): string[] | null {
|
protected getKeys(): string[] | null {
|
||||||
@@ -32,16 +32,18 @@ export abstract class ConfigBase<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read(): T {
|
read(copy_default: boolean = true): T {
|
||||||
const logger = this.core.context.logger;
|
const logger = this.core.context.logger;
|
||||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(configPath) && copy_default) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||||
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||||
}
|
}
|
||||||
|
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||||
|
fs.writeFileSync(configPath, '{}');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||||
|
@@ -9,12 +9,21 @@ interface InternalMapKey {
|
|||||||
checker: ((...args: any[]) => boolean) | undefined;
|
checker: ((...args: any[]) => boolean) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
|
||||||
|
|
||||||
|
type FuncKeys<T> = Extract<
|
||||||
|
{
|
||||||
|
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
|
||||||
|
}[keyof T],
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
export type ListenerClassBase = Record<string, string>;
|
export type ListenerClassBase = Record<string, string>;
|
||||||
|
|
||||||
export class NTEventWrapper {
|
export class NTEventWrapper {
|
||||||
private WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
|
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
|
||||||
private listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
|
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
|
||||||
private EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
|
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
wrapperSession: NodeIQQNTWrapperSession,
|
wrapperSession: NodeIQQNTWrapperSession,
|
||||||
@@ -43,10 +52,8 @@ export class NTEventWrapper {
|
|||||||
|
|
||||||
createEventFunction<
|
createEventFunction<
|
||||||
Service extends keyof ServiceNamingMapping,
|
Service extends keyof ServiceNamingMapping,
|
||||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||||
// eslint-disable-next-line
|
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||||
// @ts-ignore
|
|
||||||
T extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
|
||||||
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
|
||||||
const eventNameArr = eventName.split('/');
|
const eventNameArr = eventName.split('/');
|
||||||
type eventType = {
|
type eventType = {
|
||||||
@@ -98,10 +105,8 @@ export class NTEventWrapper {
|
|||||||
|
|
||||||
async callNoListenerEvent<
|
async callNoListenerEvent<
|
||||||
Service extends keyof ServiceNamingMapping,
|
Service extends keyof ServiceNamingMapping,
|
||||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||||
// eslint-disable-next-line
|
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||||
// @ts-ignore
|
|
||||||
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
|
||||||
>(
|
>(
|
||||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||||
...args: Parameters<EventType>
|
...args: Parameters<EventType>
|
||||||
@@ -111,15 +116,13 @@ export class NTEventWrapper {
|
|||||||
|
|
||||||
async registerListen<
|
async registerListen<
|
||||||
Listener extends keyof ListenerNamingMapping,
|
Listener extends keyof ListenerNamingMapping,
|
||||||
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
|
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||||
// eslint-disable-next-line
|
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
|
||||||
// @ts-ignore
|
|
||||||
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod],
|
|
||||||
>(
|
>(
|
||||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||||
|
checker: (...args: Parameters<ListenerType>) => boolean,
|
||||||
waitTimes = 1,
|
waitTimes = 1,
|
||||||
timeout = 5000,
|
timeout = 5000,
|
||||||
checker: (...args: Parameters<ListenerType>) => boolean,
|
|
||||||
) {
|
) {
|
||||||
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
|
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
|
||||||
const ListenerNameList = listenerAndMethod.split('/');
|
const ListenerNameList = listenerAndMethod.split('/');
|
||||||
@@ -164,15 +167,11 @@ export class NTEventWrapper {
|
|||||||
|
|
||||||
async callNormalEventV2<
|
async callNormalEventV2<
|
||||||
Service extends keyof ServiceNamingMapping,
|
Service extends keyof ServiceNamingMapping,
|
||||||
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
|
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
|
||||||
Listener extends keyof ListenerNamingMapping,
|
Listener extends keyof ListenerNamingMapping,
|
||||||
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
|
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
|
||||||
// eslint-disable-next-line
|
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
|
||||||
// @ts-ignore
|
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
|
||||||
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
|
|
||||||
// eslint-disable-next-line
|
|
||||||
// @ts-ignore
|
|
||||||
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
|
|
||||||
>(
|
>(
|
||||||
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
serviceAndMethod: `${Service}/${ServiceMethod}`,
|
||||||
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
listenerAndMethod: `${Listener}/${ListenerMethod}`,
|
||||||
@@ -182,36 +181,36 @@ export class NTEventWrapper {
|
|||||||
callbackTimesToWait = 1,
|
callbackTimesToWait = 1,
|
||||||
timeout = 5000,
|
timeout = 5000,
|
||||||
) {
|
) {
|
||||||
|
const id = randomUUID();
|
||||||
|
let complete = 0;
|
||||||
|
let retData: Parameters<ListenerType> | undefined = undefined;
|
||||||
|
let retEvent: any = {};
|
||||||
|
|
||||||
|
function sendDataCallback(resolve: any, reject: any) {
|
||||||
|
if (complete == 0) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'Timeout: NTEvent serviceAndMethod:' +
|
||||||
|
serviceAndMethod +
|
||||||
|
' ListenerName:' +
|
||||||
|
listenerAndMethod +
|
||||||
|
' EventRet:\n' +
|
||||||
|
JSON.stringify(retEvent, null, 4) +
|
||||||
|
'\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListenerNameList = listenerAndMethod.split('/');
|
||||||
|
const ListenerMainName = ListenerNameList[0];
|
||||||
|
const ListenerSubName = ListenerNameList[1];
|
||||||
|
|
||||||
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
|
||||||
async (resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const id = randomUUID();
|
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
|
||||||
let complete = 0;
|
|
||||||
let retData: Parameters<ListenerType> | undefined = undefined;
|
|
||||||
let retEvent: any = {};
|
|
||||||
|
|
||||||
function sendDataCallback() {
|
|
||||||
if (complete == 0) {
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
'Timeout: NTEvent serviceAndMethod:' +
|
|
||||||
serviceAndMethod +
|
|
||||||
' ListenerName:' +
|
|
||||||
listenerAndMethod +
|
|
||||||
' EventRet:\n' +
|
|
||||||
JSON.stringify(retEvent, null, 4) +
|
|
||||||
'\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ListenerNameList = listenerAndMethod.split('/');
|
|
||||||
const ListenerMainName = ListenerNameList[0];
|
|
||||||
const ListenerSubName = ListenerNameList[1];
|
|
||||||
|
|
||||||
const timeoutRef = setTimeout(sendDataCallback, timeout);
|
|
||||||
|
|
||||||
const eventCallback = {
|
const eventCallback = {
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
@@ -222,7 +221,7 @@ export class NTEventWrapper {
|
|||||||
retData = args as Parameters<ListenerType>;
|
retData = args as Parameters<ListenerType>;
|
||||||
if (complete >= callbackTimesToWait) {
|
if (complete >= callbackTimesToWait) {
|
||||||
clearTimeout(timeoutRef);
|
clearTimeout(timeoutRef);
|
||||||
sendDataCallback();
|
sendDataCallback(resolve, reject);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -234,23 +233,34 @@ export class NTEventWrapper {
|
|||||||
}
|
}
|
||||||
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
|
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
|
||||||
this.createListenerFunction(ListenerMainName);
|
this.createListenerFunction(ListenerMainName);
|
||||||
const eventFunction = this.createEventFunction(serviceAndMethod);
|
|
||||||
retEvent = await eventFunction!(...(args));
|
|
||||||
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
|
||||||
clearTimeout(timeoutRef);
|
|
||||||
reject(
|
|
||||||
new Error(
|
|
||||||
'EventChecker Failed: NTEvent serviceAndMethod:' +
|
|
||||||
serviceAndMethod +
|
|
||||||
' ListenerName:' +
|
|
||||||
listenerAndMethod +
|
|
||||||
' EventRet:\n' +
|
|
||||||
JSON.stringify(retEvent, null, 4) +
|
|
||||||
'\n',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
|
||||||
|
|
||||||
|
const eventRetHandle = (eventData: any) => {
|
||||||
|
retEvent = eventData;
|
||||||
|
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
|
||||||
|
clearTimeout(timeoutRef);
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
'EventChecker Failed: NTEvent serviceAndMethod:' +
|
||||||
|
serviceAndMethod +
|
||||||
|
' ListenerName:' +
|
||||||
|
listenerAndMethod +
|
||||||
|
' EventRet:\n' +
|
||||||
|
JSON.stringify(retEvent, null, 4) +
|
||||||
|
'\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eventResult instanceof Promise) {
|
||||||
|
eventResult.then((eventResult: any) => {
|
||||||
|
eventRetHandle(eventResult);
|
||||||
|
})
|
||||||
|
.catch(reject);
|
||||||
|
} else {
|
||||||
|
eventRetHandle(eventResult);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -215,7 +215,7 @@ export async function checkUriType(Uri: string) {
|
|||||||
}
|
}
|
||||||
if (uri.startsWith('file://')) {
|
if (uri.startsWith('file://')) {
|
||||||
let filePath: string;
|
let filePath: string;
|
||||||
const pathname = decodeURIComponent(new URL(uri).pathname);
|
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
filePath = pathname.slice(1);
|
filePath = pathname.slice(1);
|
||||||
} else {
|
} else {
|
||||||
@@ -242,7 +242,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
|
|||||||
//解析Http和Https协议
|
//解析Http和Https协议
|
||||||
|
|
||||||
if (UriType == FileUriType.Unknown) {
|
if (UriType == FileUriType.Unknown) {
|
||||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
|
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||||
}
|
}
|
||||||
//解析File协议和本地文件
|
//解析File协议和本地文件
|
||||||
if (UriType == FileUriType.Local) {
|
if (UriType == FileUriType.Local) {
|
||||||
@@ -289,5 +289,5 @@ export async function uri2local(dir: string, uri: string, filename: string | und
|
|||||||
}
|
}
|
||||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||||
}
|
}
|
||||||
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
|
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||||
}
|
}
|
||||||
|
114
src/common/forward-msg-builder.ts
Normal file
114
src/common/forward-msg-builder.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as crypto from "node:crypto";
|
||||||
|
import { PacketMsg } from "@/core/packet/message/message";
|
||||||
|
|
||||||
|
interface ForwardMsgJson {
|
||||||
|
app: string
|
||||||
|
config: ForwardMsgJsonConfig,
|
||||||
|
desc: string,
|
||||||
|
extra: ForwardMsgJsonExtra,
|
||||||
|
meta: ForwardMsgJsonMeta,
|
||||||
|
prompt: string,
|
||||||
|
ver: string,
|
||||||
|
view: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardMsgJsonConfig {
|
||||||
|
autosize: number,
|
||||||
|
forward: number,
|
||||||
|
round: number,
|
||||||
|
type: string,
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardMsgJsonExtra {
|
||||||
|
filename: string,
|
||||||
|
tsum: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardMsgJsonMeta {
|
||||||
|
detail: ForwardMsgJsonMetaDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardMsgJsonMetaDetail {
|
||||||
|
news: {
|
||||||
|
text: string
|
||||||
|
}[],
|
||||||
|
resid: string,
|
||||||
|
source: string,
|
||||||
|
summary: string,
|
||||||
|
uniseq: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardAdaptMsg {
|
||||||
|
senderName?: string;
|
||||||
|
isGroupMsg?: boolean;
|
||||||
|
msg?: ForwardAdaptMsgElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForwardAdaptMsgElement {
|
||||||
|
preview?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForwardMsgBuilder {
|
||||||
|
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const isGroupMsg = msg.some(m => m.isGroupMsg);
|
||||||
|
if (!source) {
|
||||||
|
source = isGroupMsg ? "群聊的聊天记录" : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
|
||||||
|
}
|
||||||
|
if (!news) {
|
||||||
|
news = msg.length === 0 ? [{
|
||||||
|
text: "Nya~ This message is send from NapCat.Packet!",
|
||||||
|
}] : msg.map(m => ({
|
||||||
|
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (!summary) {
|
||||||
|
summary = `查看${msg.length}条转发消息`;
|
||||||
|
}
|
||||||
|
if (!prompt) {
|
||||||
|
prompt = "[聊天记录]";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
app: "com.tencent.multimsg",
|
||||||
|
config: {
|
||||||
|
autosize: 1,
|
||||||
|
forward: 1,
|
||||||
|
round: 1,
|
||||||
|
type: "normal",
|
||||||
|
width: 300
|
||||||
|
},
|
||||||
|
desc: prompt,
|
||||||
|
extra: {
|
||||||
|
filename: id,
|
||||||
|
tsum: msg.length,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
detail: {
|
||||||
|
news,
|
||||||
|
resid: resId,
|
||||||
|
source,
|
||||||
|
summary,
|
||||||
|
uniseq: id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
prompt,
|
||||||
|
ver: "0.0.0.5",
|
||||||
|
view: "contact",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromResId(resId: string): ForwardMsgJson {
|
||||||
|
return this.build(resId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
|
||||||
|
return this.build(resId, packetMsg.map(msg => ({
|
||||||
|
senderName: msg.senderName,
|
||||||
|
isGroupMsg: msg.groupId !== undefined,
|
||||||
|
msg: msg.msg.map(m => ({
|
||||||
|
preview: m.valid ? m.toPreview() : "[该消息类型暂不支持查看]",
|
||||||
|
}))
|
||||||
|
})), source, news, summary, prompt);
|
||||||
|
}
|
||||||
|
}
|
@@ -52,7 +52,7 @@ export class FileNapCatOneBotUUID {
|
|||||||
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
||||||
return {
|
return {
|
||||||
peer: {
|
peer: {
|
||||||
chatType: chatType as any,
|
chatType: +chatType,
|
||||||
peerUid: peerUid,
|
peerUid: peerUid,
|
||||||
},
|
},
|
||||||
modelId,
|
modelId,
|
||||||
@@ -89,7 +89,7 @@ export class FileNapCatOneBotUUID {
|
|||||||
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
||||||
return {
|
return {
|
||||||
peer: {
|
peer: {
|
||||||
chatType: chatType as any,
|
chatType: +chatType,
|
||||||
peerUid: peerUid,
|
peerUid: peerUid,
|
||||||
},
|
},
|
||||||
msgId,
|
msgId,
|
||||||
@@ -239,3 +239,42 @@ export function calcQQLevel(level?: QQLevel) {
|
|||||||
const { crownNum, sunNum, moonNum, starNum } = level;
|
const { crownNum, sunNum, moonNum, starNum } = level;
|
||||||
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stringifyWithBigInt(obj: any) {
|
||||||
|
return JSON.stringify(obj, (key, value) =>
|
||||||
|
typeof value === 'bigint' ? value.toString() : value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
|
||||||
|
const hexSequence = "A4 09 00 00 00 35";
|
||||||
|
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
|
||||||
|
const filePath = path.resolve(nodeMajor);
|
||||||
|
const fileContent = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
let searchPosition = 0;
|
||||||
|
while (true) {
|
||||||
|
const index = fileContent.indexOf(sequenceBytes, searchPosition);
|
||||||
|
if (index === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = index + sequenceBytes.length - 1;
|
||||||
|
const end = fileContent.indexOf(0x00, start);
|
||||||
|
if (end === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const content = fileContent.subarray(start, end);
|
||||||
|
if (!content.every(byte => byte === 0x00)) {
|
||||||
|
try {
|
||||||
|
return content.toString("utf-8");
|
||||||
|
} catch (error) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchPosition = end + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
import log4js, { Configuration } from 'log4js';
|
import winston, { format, transports } from 'winston';
|
||||||
import { truncateString } from '@/common/helper';
|
import { truncateString } from '@/common/helper';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import chalk from 'chalk';
|
import fs from 'node:fs';
|
||||||
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
@@ -27,97 +27,137 @@ function getFormattedTimestamp() {
|
|||||||
export class LogWrapper {
|
export class LogWrapper {
|
||||||
fileLogEnabled = true;
|
fileLogEnabled = true;
|
||||||
consoleLogEnabled = true;
|
consoleLogEnabled = true;
|
||||||
logConfig: Configuration;
|
logger: winston.Logger;
|
||||||
loggerConsole: log4js.Logger;
|
|
||||||
loggerFile: log4js.Logger;
|
|
||||||
loggerDefault: log4js.Logger;
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
colorEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g;
|
|
||||||
|
|
||||||
constructor(logDir: string) {
|
constructor(logDir: string) {
|
||||||
const filename = `${getFormattedTimestamp()}.log`;
|
const filename = `${getFormattedTimestamp()}.log`;
|
||||||
const logPath = path.join(logDir, filename);
|
const logPath = path.join(logDir, filename);
|
||||||
this.logConfig = {
|
|
||||||
appenders: {
|
this.logger = winston.createLogger({
|
||||||
FileAppender: { // 输出到文件的appender
|
level: 'debug',
|
||||||
type: 'file',
|
format: format.combine(
|
||||||
filename: logPath, // 指定日志文件的位置和文件名
|
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
|
||||||
maxLogSize: 10485760, // 日志文件的最大大小(单位:字节),这里设置为10MB
|
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||||
layout: {
|
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||||
type: 'pattern',
|
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||||
pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %X{userInfo} | %m',
|
})
|
||||||
},
|
),
|
||||||
},
|
transports: [
|
||||||
ConsoleAppender: { // 输出到控制台的appender
|
new transports.File({
|
||||||
type: 'console',
|
filename: logPath,
|
||||||
layout: {
|
level: 'debug',
|
||||||
type: 'pattern',
|
maxsize: 5 * 1024 * 1024, // 5MB
|
||||||
pattern: `%d{yyyy-MM-dd hh:mm:ss} [%[%p%]] ${chalk.magenta('%X{userInfo}')} | %m`,
|
maxFiles: 5
|
||||||
},
|
}),
|
||||||
},
|
new transports.Console({
|
||||||
},
|
format: format.combine(
|
||||||
categories: {
|
format.colorize(),
|
||||||
default: { appenders: ['FileAppender', 'ConsoleAppender'], level: 'debug' }, // 默认情况下同时输出到文件和控制台
|
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||||
file: { appenders: ['FileAppender'], level: 'debug' },
|
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||||
console: { appenders: ['ConsoleAppender'], level: 'debug' },
|
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||||
},
|
})
|
||||||
};
|
)
|
||||||
log4js.configure(this.logConfig);
|
})
|
||||||
this.loggerConsole = log4js.getLogger('console');
|
]
|
||||||
this.loggerFile = log4js.getLogger('file');
|
});
|
||||||
this.loggerDefault = log4js.getLogger('default');
|
|
||||||
this.setLogSelfInfo({ nick: '', uin: '', uid: '' });
|
this.setLogSelfInfo({ nick: '', uid: '' });
|
||||||
|
this.cleanOldLogs(logDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanOldLogs(logDir: string) {
|
||||||
|
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||||
|
fs.readdir(logDir, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error('Failed to read log directory', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
files.forEach(file => {
|
||||||
|
const filePath = path.join(logDir, file);
|
||||||
|
this.deleteOldLogFile(filePath, oneWeekAgo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
||||||
|
fs.stat(filePath, (err, stats) => {
|
||||||
|
if (err) {
|
||||||
|
this.logger.error('Failed to get file stats', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stats.mtime.getTime() < oneWeekAgo) {
|
||||||
|
fs.unlink(filePath, err => {
|
||||||
|
if (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
this.logger.warn(`File already deleted: ${filePath}`);
|
||||||
|
} else {
|
||||||
|
this.logger.error('Failed to delete old log file', err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.info(`Deleted old log file: ${filePath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
|
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
|
||||||
this.logConfig.categories.file.level = fileLogLevel;
|
this.logger.transports.forEach((transport) => {
|
||||||
this.logConfig.categories.console.level = consoleLogLevel;
|
if (transport instanceof transports.File) {
|
||||||
log4js.configure(this.logConfig);
|
transport.level = fileLogLevel;
|
||||||
|
} else if (transport instanceof transports.Console) {
|
||||||
|
transport.level = consoleLogLevel;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) {
|
setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
|
||||||
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`;
|
const userInfo = `${selfInfo.nick}`;
|
||||||
this.loggerConsole.addContext('userInfo', userInfo);
|
this.logger.defaultMeta = { userInfo };
|
||||||
this.loggerFile.addContext('userInfo', userInfo);
|
|
||||||
this.loggerDefault.addContext('userInfo', userInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileLogEnabled(isEnabled: boolean) {
|
setFileLogEnabled(isEnabled: boolean) {
|
||||||
this.fileLogEnabled = isEnabled;
|
this.fileLogEnabled = isEnabled;
|
||||||
|
this.logger.transports.forEach((transport) => {
|
||||||
|
if (transport instanceof transports.File) {
|
||||||
|
transport.silent = !isEnabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setConsoleLogEnabled(isEnabled: boolean) {
|
setConsoleLogEnabled(isEnabled: boolean) {
|
||||||
this.consoleLogEnabled = isEnabled;
|
this.consoleLogEnabled = isEnabled;
|
||||||
|
this.logger.transports.forEach((transport) => {
|
||||||
|
if (transport instanceof transports.Console) {
|
||||||
|
transport.silent = !isEnabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMsg(msg: any[]) {
|
formatMsg(msg: any[]) {
|
||||||
let logMsg = '';
|
return msg.map(msgItem => {
|
||||||
for (const msgItem of msg) {
|
if (msgItem instanceof Error) {
|
||||||
if (msgItem instanceof Error) { // 判断是否是错误
|
return msgItem.stack;
|
||||||
logMsg += msgItem.stack + ' ';
|
} else if (typeof msgItem === 'object') {
|
||||||
continue;
|
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
||||||
} else if (typeof msgItem === 'object') { // 判断是否是对象
|
|
||||||
const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
|
|
||||||
logMsg += JSON.stringify(truncateString(obj)) + ' ';
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
logMsg += msgItem + ' ';
|
return msgItem;
|
||||||
}
|
}).join(' ');
|
||||||
return logMsg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_log(level: LogLevel, ...args: any[]) {
|
_log(level: LogLevel, ...args: any[]) {
|
||||||
if (this.consoleLogEnabled) {
|
const message = this.formatMsg(args);
|
||||||
this.loggerConsole[level](this.formatMsg(args));
|
if (this.consoleLogEnabled && this.fileLogEnabled) {
|
||||||
}
|
this.logger.log(level, message);
|
||||||
if (this.fileLogEnabled) {
|
} else if (this.consoleLogEnabled) {
|
||||||
this.loggerFile[level](this.formatMsg(args).replace(this.colorEscape, ''));
|
this.logger.log(level, message);
|
||||||
|
} else if (this.fileLogEnabled) {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
// info 等级
|
|
||||||
this._log(LogLevel.INFO, ...args);
|
this._log(LogLevel.INFO, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,12 +180,11 @@ export class LogWrapper {
|
|||||||
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
|
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
|
||||||
const isSelfSent = msg.senderUin === selfInfo.uin;
|
const isSelfSent = msg.senderUin === selfInfo.uin;
|
||||||
|
|
||||||
// Intercept grey tip
|
|
||||||
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
|
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
|
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,86 +202,93 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
|
|||||||
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
|
||||||
}
|
}
|
||||||
if (msg.senderUin !== '0') {
|
if (msg.senderUin !== '0') {
|
||||||
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
|
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
|
||||||
}
|
}
|
||||||
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
|
||||||
tokens.push('移动设备');
|
tokens.push('移动设备');
|
||||||
} else /* temp */ {
|
} else {
|
||||||
tokens.push(`临时消息 (${msg.peerUin})`);
|
tokens.push(`临时消息 (${msg.peerUin})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// message content
|
|
||||||
|
|
||||||
function msgElementToText(element: MessageElement) {
|
|
||||||
if (element.textElement) {
|
|
||||||
if (element.textElement.atType === AtType.notAt) {
|
|
||||||
const originalContentLines = element.textElement.content.split('\n');
|
|
||||||
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
|
||||||
} else if (element.textElement.atType === AtType.atAll) {
|
|
||||||
return `@全体成员`;
|
|
||||||
} else if (element.textElement.atType === AtType.atUser) {
|
|
||||||
return `${element.textElement.content} (${element.textElement.atUid})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.replyElement) {
|
|
||||||
const recordMsgOrNull = msg.records.find(
|
|
||||||
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
|
|
||||||
);
|
|
||||||
return `[回复消息 ${recordMsgOrNull &&
|
|
||||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
|
|
||||||
?
|
|
||||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
|
||||||
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
|
|
||||||
}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.picElement) {
|
|
||||||
return '[图片]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.fileElement) {
|
|
||||||
return `[文件 ${element.fileElement.fileName}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.videoElement) {
|
|
||||||
return '[视频]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.pttElement) {
|
|
||||||
return `[语音 ${element.pttElement.duration}s]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.arkElement) {
|
|
||||||
return '[卡片消息]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.faceElement) {
|
|
||||||
return `[表情 ${element.faceElement.faceText ?? ''}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.marketFaceElement) {
|
|
||||||
return element.marketFaceElement.faceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.markdownElement) {
|
|
||||||
return '[Markdown 消息]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.multiForwardMsgElement) {
|
|
||||||
return '[转发消息]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.elementType === ElementType.GreyTip) {
|
|
||||||
return '[灰条消息]';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `[未实现 (ElementType = ${element.elementType})]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of msg.elements) {
|
for (const element of msg.elements) {
|
||||||
tokens.push(msgElementToText(element));
|
tokens.push(msgElementToText(element, msg, recursiveLevel));
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens.join(' ');
|
return tokens.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
|
||||||
|
if (element.textElement) {
|
||||||
|
return textElementToText(element.textElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.replyElement) {
|
||||||
|
return replyElementToText(element.replyElement, msg, recursiveLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.picElement) {
|
||||||
|
return '[图片]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.fileElement) {
|
||||||
|
return `[文件 ${element.fileElement.fileName}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.videoElement) {
|
||||||
|
return '[视频]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.pttElement) {
|
||||||
|
return `[语音 ${element.pttElement.duration}s]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.arkElement) {
|
||||||
|
return '[卡片消息]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.faceElement) {
|
||||||
|
return `[表情 ${element.faceElement.faceText ?? ''}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.marketFaceElement) {
|
||||||
|
return element.marketFaceElement.faceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.markdownElement) {
|
||||||
|
return '[Markdown 消息]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.multiForwardMsgElement) {
|
||||||
|
return '[转发消息]';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.elementType === ElementType.GreyTip) {
|
||||||
|
return '[灰条消息]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `[未实现 (ElementType = ${element.elementType})]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textElementToText(textElement: any): string {
|
||||||
|
if (textElement.atType === AtType.notAt) {
|
||||||
|
const originalContentLines = textElement.content.split('\n');
|
||||||
|
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
|
||||||
|
} else if (textElement.atType === AtType.atAll) {
|
||||||
|
return `@全体成员`;
|
||||||
|
} else if (textElement.atType === AtType.atUser) {
|
||||||
|
return `${textElement.content} (${textElement.atUid})`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
|
||||||
|
const recordMsgOrNull = msg.records.find(
|
||||||
|
record => replyElement.sourceMsgIdInRecords === record.msgId,
|
||||||
|
);
|
||||||
|
return `[回复消息 ${recordMsgOrNull &&
|
||||||
|
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
||||||
|
?
|
||||||
|
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
||||||
|
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
||||||
|
}]`;
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
export class LRUCache<K, V> {
|
export class LRUCache<K, V> {
|
||||||
private capacity: number;
|
private capacity: number;
|
||||||
private cache: Map<K, V>;
|
public cache: Map<K, V>;
|
||||||
|
|
||||||
constructor(capacity: number) {
|
constructor(capacity: number) {
|
||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
@@ -30,4 +30,13 @@ export class LRUCache<K, V> {
|
|||||||
}
|
}
|
||||||
this.cache.set(key, value);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -2,8 +2,8 @@ import { Peer } from '@/core';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
export class LimitedHashTable<K, V> {
|
export class LimitedHashTable<K, V> {
|
||||||
private keyToValue: Map<K, V> = new Map();
|
private readonly keyToValue: Map<K, V> = new Map();
|
||||||
private valueToKey: Map<V, K> = new Map();
|
private readonly valueToKey: Map<V, K> = new Map();
|
||||||
private maxSize: number;
|
private maxSize: number;
|
||||||
|
|
||||||
constructor(maxSize: number) {
|
constructor(maxSize: number) {
|
||||||
@@ -23,10 +23,10 @@ export class LimitedHashTable<K, V> {
|
|||||||
}
|
}
|
||||||
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
|
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
|
||||||
const oldestKey = this.keyToValue.keys().next().value;
|
const oldestKey = this.keyToValue.keys().next().value;
|
||||||
// @ts-ignore
|
if (oldestKey !== undefined) {
|
||||||
this.valueToKey.delete(this.keyToValue.get(oldestKey)!);
|
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
|
||||||
// @ts-ignore
|
this.keyToValue.delete(oldestKey);
|
||||||
this.keyToValue.delete(oldestKey);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +75,8 @@ export class LimitedHashTable<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MessageUniqueWrapper {
|
class MessageUniqueWrapper {
|
||||||
private msgDataMap: LimitedHashTable<string, number>;
|
private readonly msgDataMap: LimitedHashTable<string, number>;
|
||||||
private msgIdMap: LimitedHashTable<string, number>;
|
private readonly msgIdMap: LimitedHashTable<string, number>;
|
||||||
|
|
||||||
constructor(maxMap: number = 1000) {
|
constructor(maxMap: number = 1000) {
|
||||||
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { systemPlatform } from '@/common/system';
|
import { systemPlatform } from '@/common/system';
|
||||||
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath } from './helper';
|
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
|
||||||
import AppidTable from '@/core/external/appid.json';
|
import AppidTable from '@/core/external/appid.json';
|
||||||
import { LogWrapper } from './log';
|
import { LogWrapper } from './log';
|
||||||
|
import { getMajorPath } from '@/core';
|
||||||
|
|
||||||
export class QQBasicInfoWrapper {
|
export class QQBasicInfoWrapper {
|
||||||
QQMainPath: string | undefined;
|
QQMainPath: string | undefined;
|
||||||
@@ -72,6 +73,7 @@ export class QQBasicInfoWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAppidV2(): { appid: string; qua: string } {
|
getAppidV2(): { appid: string; qua: string } {
|
||||||
|
// 通过已有表 性能好
|
||||||
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
const appidTbale = AppidTable as unknown as QQAppidTableType;
|
||||||
const fullVersion = this.getFullQQVesion();
|
const fullVersion = this.getFullQQVesion();
|
||||||
if (fullVersion) {
|
if (fullVersion) {
|
||||||
@@ -80,10 +82,25 @@ export class QQBasicInfoWrapper {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 通过Major拉取 性能差
|
||||||
// else
|
try {
|
||||||
|
const majorAppid = this.getAppidV2ByMajor(fullVersion);
|
||||||
|
if (majorAppid) {
|
||||||
|
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
|
||||||
|
return { appid: majorAppid, qua: this.getQUAFallback() };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||||
|
}
|
||||||
|
// 最终兜底为老版本
|
||||||
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
|
||||||
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
|
||||||
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
|
||||||
}
|
}
|
||||||
|
getAppidV2ByMajor(QQVersion: string) {
|
||||||
|
const majorPath = getMajorPath(QQVersion);
|
||||||
|
const appid = parseAppidFromMajor(majorPath);
|
||||||
|
return appid;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,49 +8,48 @@ export class RequestUtil {
|
|||||||
const client = url.startsWith('https') ? https : http;
|
const client = url.startsWith('https') ? https : http;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = client.get(url, (res) => {
|
const req = client.get(url, (res) => {
|
||||||
let cookies: { [key: string]: string } = {};
|
const cookies: { [key: string]: string } = {};
|
||||||
const handleRedirect = (res: http.IncomingMessage) => {
|
|
||||||
//console.log(res.headers.location);
|
res.on('data', () => { }); // Necessary to consume the stream
|
||||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
||||||
if (res.headers.location) {
|
|
||||||
const redirectUrl = new URL(res.headers.location, url);
|
|
||||||
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
|
|
||||||
// 合并重定向过程中的cookies
|
|
||||||
cookies = { ...cookies, ...redirectCookies };
|
|
||||||
resolve(cookies);
|
|
||||||
}).catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(cookies);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(cookies);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
res.on('data', () => {
|
|
||||||
}); // Necessary to consume the stream
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
handleRedirect(res);
|
this.handleRedirect(res, url, cookies)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.headers['set-cookie']) {
|
if (res.headers['set-cookie']) {
|
||||||
//console.log(res.headers['set-cookie']);
|
this.extractCookies(res.headers['set-cookie'], cookies);
|
||||||
res.headers['set-cookie'].forEach((cookie) => {
|
|
||||||
const parts = cookie.split(';')[0].split('=');
|
|
||||||
const key = parts[0];
|
|
||||||
const value = parts[1];
|
|
||||||
if (key && value && key.length > 0 && value.length > 0) {
|
|
||||||
cookies[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
req.on('error', (error: any) => {
|
|
||||||
|
req.on('error', (error: Error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async handleRedirect(res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
|
||||||
|
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||||
|
if (res.headers.location) {
|
||||||
|
const redirectUrl = new URL(res.headers.location, url);
|
||||||
|
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
|
||||||
|
// 合并重定向过程中的cookies
|
||||||
|
return { ...cookies, ...redirectCookies };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
|
||||||
|
setCookieHeaders.forEach((cookie) => {
|
||||||
|
const parts = cookie.split(';')[0].split('=');
|
||||||
|
const key = parts[0];
|
||||||
|
const value = parts[1];
|
||||||
|
if (key && value && key.length > 0 && value.length > 0) {
|
||||||
|
cookies[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 请求和回复都是JSON data传原始内容 自动编码json
|
// 请求和回复都是JSON data传原始内容 自动编码json
|
||||||
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
|
||||||
@@ -88,13 +87,13 @@ export class RequestUtil {
|
|||||||
} else {
|
} else {
|
||||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError: unknown) {
|
||||||
reject(parseError);
|
reject(new Error((parseError as Error).message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
req.on('error', (error: any) => {
|
req.on('error', (error: Error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
||||||
@@ -133,62 +132,4 @@ export class RequestUtil {
|
|||||||
Buffer.from(footer, 'utf8'),
|
Buffer.from(footer, 'utf8'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async uploadImageForOpenPlatform(filePath: string, cookies: string): Promise<string> {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
type retType = { retcode: number, result?: { url: string } };
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
hostname: 'cgi.connect.qq.com',
|
|
||||||
port: 443,
|
|
||||||
path: '/qqconnectopen/upload_share_image',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Referer': 'https://cgi.connect.qq.com',
|
|
||||||
'Cookie': cookies,
|
|
||||||
'Accept': '*/*',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const req = https.request(options, async (res) => {
|
|
||||||
let responseBody = '';
|
|
||||||
|
|
||||||
res.on('data', (chunk: string | Buffer) => {
|
|
||||||
responseBody += chunk.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
||||||
const responseJson = JSON.parse(responseBody) as retType;
|
|
||||||
resolve(responseJson.result!.url!);
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Unexpected status code: ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
reject(parseError);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', (error) => {
|
|
||||||
reject(error);
|
|
||||||
console.log('Error during upload:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await RequestUtil.createFormData('WebKitFormBoundary7MA4YWxkTrZu0gW', filePath);
|
|
||||||
// req.setHeader('Content-Length', Buffer.byteLength(body));
|
|
||||||
// console.log(`Prepared data size: ${Buffer.byteLength(body)} bytes`);
|
|
||||||
req.write(body);
|
|
||||||
req.end();
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '3.0.1';
|
export const napCatVersion = '4.17';
|
||||||
|
@@ -20,7 +20,7 @@ export async function getVideoInfo(filePath: string, logger: LogWrapper) {
|
|||||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||||
ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => {
|
ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(new Error('无法获取视频信息。'));
|
||||||
} else {
|
} else {
|
||||||
const videoStream = metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video');
|
const videoStream = metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video');
|
||||||
if (videoStream) {
|
if (videoStream) {
|
||||||
|
@@ -6,8 +6,10 @@ export class NodeIDependsAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMSFSsoError(args: unknown) {
|
onMSFSsoError(args: unknown) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getGroupCode(args: unknown) {
|
getGroupCode(args: unknown) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
Peer,
|
Peer,
|
||||||
PicElement,
|
PicElement,
|
||||||
PicType,
|
PicType,
|
||||||
|
RawMessage,
|
||||||
SendFileElement,
|
SendFileElement,
|
||||||
SendPicElement,
|
SendPicElement,
|
||||||
SendPttElement,
|
SendPttElement,
|
||||||
@@ -30,12 +31,12 @@ export class NTQQFileApi {
|
|||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
rkeyManager: RkeyManager;
|
rkeyManager: RkeyManager;
|
||||||
packetRkey: Array<{ rkey: string; time: number; type: number; }> | undefined;
|
packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
|
||||||
|
|
||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.rkeyManager = new RkeyManager(['https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
|
this.rkeyManager = new RkeyManager(['https://rkey.napneko.icu/rkeys', 'https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyFile(filePath: string, destPath: string) {
|
async copyFile(filePath: string, destPath: string) {
|
||||||
@@ -174,14 +175,18 @@ export class NTQQFileApi {
|
|||||||
const thumbPath = pathLib.join(thumb, thumbFileName);
|
const thumbPath = pathLib.join(thumb, thumbFileName);
|
||||||
ffmpeg(filePath)
|
ffmpeg(filePath)
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
logger.logDebug('获取视频封面失败,使用默认封面', err);
|
try {
|
||||||
if (diyThumbPath) {
|
logger.logDebug('获取视频封面失败,使用默认封面', err);
|
||||||
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
if (diyThumbPath) {
|
||||||
|
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
||||||
|
resolve(thumbPath);
|
||||||
|
}).catch(reject);
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
||||||
resolve(thumbPath);
|
resolve(thumbPath);
|
||||||
}).catch(reject);
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
|
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
|
||||||
resolve(thumbPath);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.screenshots({
|
.screenshots({
|
||||||
@@ -238,7 +243,7 @@ export class NTQQFileApi {
|
|||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
filePath: path,
|
filePath: path,
|
||||||
md5HexStr: md5,
|
md5HexStr: md5,
|
||||||
fileSize: fileSize,
|
fileSize: fileSize.toString(),
|
||||||
duration: duration ?? 1,
|
duration: duration ?? 1,
|
||||||
formatType: 1,
|
formatType: 1,
|
||||||
voiceType: 1,
|
voiceType: 1,
|
||||||
@@ -267,6 +272,53 @@ export class NTQQFileApi {
|
|||||||
return fileTransNotifyInfo.filePath;
|
return fileTransNotifyInfo.filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadRawMsgMedia(msg: RawMessage[]) {
|
||||||
|
const res = await Promise.all(
|
||||||
|
msg.map(m =>
|
||||||
|
Promise.all(
|
||||||
|
m.elements
|
||||||
|
.filter(element =>
|
||||||
|
element.elementType === ElementType.PIC ||
|
||||||
|
element.elementType === ElementType.VIDEO ||
|
||||||
|
element.elementType === ElementType.PTT ||
|
||||||
|
element.elementType === ElementType.FILE
|
||||||
|
)
|
||||||
|
.map(element =>
|
||||||
|
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
msg.forEach((m, msgIndex) => {
|
||||||
|
const elementResults = res[msgIndex];
|
||||||
|
let elementIndex = 0;
|
||||||
|
m.elements.forEach(element => {
|
||||||
|
if (
|
||||||
|
element.elementType === ElementType.PIC ||
|
||||||
|
element.elementType === ElementType.VIDEO ||
|
||||||
|
element.elementType === ElementType.PTT ||
|
||||||
|
element.elementType === ElementType.FILE
|
||||||
|
) {
|
||||||
|
switch (element.elementType) {
|
||||||
|
case ElementType.PIC:
|
||||||
|
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||||
|
break;
|
||||||
|
case ElementType.VIDEO:
|
||||||
|
element.videoElement!.filePath = elementResults[elementIndex];
|
||||||
|
break;
|
||||||
|
case ElementType.PTT:
|
||||||
|
element.pttElement!.filePath = elementResults[elementIndex];
|
||||||
|
break;
|
||||||
|
case ElementType.FILE:
|
||||||
|
element.fileElement!.filePath = elementResults[elementIndex];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
elementIndex++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
|
||||||
// 用于下载收到的消息中的图片等
|
// 用于下载收到的消息中的图片等
|
||||||
if (sourcePath && fs.existsSync(sourcePath)) {
|
if (sourcePath && fs.existsSync(sourcePath)) {
|
||||||
@@ -296,7 +348,7 @@ export class NTQQFileApi {
|
|||||||
filePath: thumbPath,
|
filePath: thumbPath,
|
||||||
}],
|
}],
|
||||||
() => true,
|
() => true,
|
||||||
(arg) => arg.msgId === msgId,
|
(arg) => arg.msgElementId === elementId && arg.msgId === msgId,
|
||||||
1,
|
1,
|
||||||
timeout,
|
timeout,
|
||||||
);
|
);
|
||||||
@@ -305,15 +357,13 @@ export class NTQQFileApi {
|
|||||||
|
|
||||||
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
imageSize(filePath, (err, dimensions) => {
|
imageSize(filePath, (err: Error | null, dimensions) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(new Error(err.message));
|
||||||
|
} else if (!dimensions) {
|
||||||
|
reject(new Error('获取图片尺寸失败'));
|
||||||
} else {
|
} else {
|
||||||
if (!dimensions) {
|
resolve(dimensions);
|
||||||
reject(new Error('获取图片尺寸失败'));
|
|
||||||
} else {
|
|
||||||
resolve(dimensions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -356,68 +406,83 @@ export class NTQQFileApi {
|
|||||||
return fileData.filePath!;
|
return fileData.filePath!;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageUrl(element: PicElement) {
|
async getImageUrl(element: PicElement): Promise<string> {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const url: string = element.originImageUrl ?? '';
|
const url: string = element.originImageUrl ?? '';
|
||||||
const md5HexStr = element.md5HexStr;
|
const md5HexStr = element.md5HexStr;
|
||||||
const fileMd5 = element.md5HexStr;
|
const fileMd5 = element.md5HexStr;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
|
||||||
const urlRkey = parsedUrl.searchParams.get('rkey');
|
const rkeyData = await this.getRkeyData();
|
||||||
const imageAppid = parsedUrl.searchParams.get('appid');
|
return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData);
|
||||||
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
}
|
||||||
const imageFileId = parsedUrl.searchParams.get('fileid');
|
|
||||||
|
|
||||||
const rkeyData = {
|
return this.getImageUrlFromMd5(fileMd5, md5HexStr);
|
||||||
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
}
|
||||||
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
|
||||||
online_rkey: false
|
private async getRkeyData() {
|
||||||
};
|
const rkeyData = {
|
||||||
|
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
|
||||||
|
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
|
||||||
|
online_rkey: false
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.core.apis.PacketApi.available) {
|
||||||
|
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||||
|
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
|
||||||
|
if (rkey_expired_private || rkey_expired_group) {
|
||||||
|
this.packetRkey = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
|
||||||
|
}
|
||||||
|
if (this.packetRkey && this.packetRkey.length > 0) {
|
||||||
|
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
||||||
|
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
||||||
|
rkeyData.online_rkey = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rkeyData.online_rkey) {
|
||||||
try {
|
try {
|
||||||
if (this.core.apis.PacketApi.available) {
|
const tempRkeyData = await this.rkeyManager.getRkey();
|
||||||
if ((!this.packetRkey || this.packetRkey[0].time > Date.now() / 1000)) {
|
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
||||||
this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
|
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||||
}
|
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||||
if (this.packetRkey.length > 0) {
|
} catch (e) {
|
||||||
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
|
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
||||||
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
|
|
||||||
rkeyData.online_rkey = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rkeyData.online_rkey) {
|
|
||||||
try {
|
|
||||||
const tempRkeyData = await this.rkeyManager.getRkey();
|
|
||||||
rkeyData.group_rkey = tempRkeyData.group_rkey;
|
|
||||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
|
||||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
|
||||||
} catch (e) {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isNTV2 && urlRkey) {
|
|
||||||
return IMAGE_HTTP_HOST_NT + urlRkey;
|
|
||||||
} else if (isNTV2 && rkeyData.online_rkey) {
|
|
||||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
|
||||||
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
|
|
||||||
} else if (isNTV2 && imageFileId) {
|
|
||||||
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
|
||||||
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
//到这里说明可能是旧客户端
|
|
||||||
|
return rkeyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string {
|
||||||
|
const imageAppid = parsedUrl.searchParams.get('appid');
|
||||||
|
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
|
||||||
|
const imageFileId = parsedUrl.searchParams.get('fileid');
|
||||||
|
if (isNTV2 && rkeyData.online_rkey) {
|
||||||
|
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||||
|
return IMAGE_HTTP_HOST_NT + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||||
|
} else if (isNTV2 && imageFileId) {
|
||||||
|
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
|
||||||
|
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {
|
||||||
if (fileMd5 || md5HexStr) {
|
if (fileMd5 || md5HexStr) {
|
||||||
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
|
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr ?? '').toUpperCase()}/0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.context.logger.logDebug('图片url获取失败', element);
|
this.context.logger.logDebug('图片url获取失败', { fileMd5, md5HexStr });
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -11,11 +11,11 @@ export class NTQQFriendApi {
|
|||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
async setBuddyRemark(uid: string, remark: string) {
|
async setBuddyRemark(uid: string, remark: string) {
|
||||||
return this.context.session.getBuddyService().setBuddyRemark(uid, remark);
|
return this.context.session.getBuddyService().setBuddyRemark({ uid, remark });
|
||||||
}
|
}
|
||||||
async getBuddyV2SimpleInfoMap(refresh = false) {
|
async getBuddyV2SimpleInfoMap(refresh = false) {
|
||||||
const buddyService = this.context.session.getBuddyService();
|
const buddyService = this.context.session.getBuddyService();
|
||||||
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
|
||||||
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
|
||||||
return await this.core.eventWrapper.callNoListenerEvent(
|
return await this.core.eventWrapper.callNoListenerEvent(
|
||||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||||
@@ -34,15 +34,17 @@ export class NTQQFriendApi {
|
|||||||
data.forEach((value) => retMap.set(value.uin!, value.uid!));
|
data.forEach((value) => retMap.set(value.uin!, value.uid!));
|
||||||
return retMap;
|
return retMap;
|
||||||
}
|
}
|
||||||
|
async delBuudy(uid: string, tempBlock = false, tempBothDel = false) {
|
||||||
async getBuddyV2ExWithCate(refresh = false) {
|
return this.context.session.getBuddyService().delBuddy({
|
||||||
const categoryMap: Map<string, any> = new Map();
|
friendUid: uid,
|
||||||
|
tempBlock: tempBlock,
|
||||||
|
tempBothDel: tempBothDel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async getBuddyV2ExWithCate() {
|
||||||
const buddyService = this.context.session.getBuddyService();
|
const buddyService = this.context.session.getBuddyService();
|
||||||
const buddyListV2 = refresh ? (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data : (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
|
||||||
const uids = buddyListV2.flatMap(item => {
|
const uids = buddyListV2.flatMap(item => {
|
||||||
item.buddyUids.forEach(uid => {
|
|
||||||
categoryMap.set(uid, { categoryId: item.categoryId, categoryName: item.categroyName });
|
|
||||||
});
|
|
||||||
return item.buddyUids;
|
return item.buddyUids;
|
||||||
});
|
});
|
||||||
const data = await this.core.eventWrapper.callNoListenerEvent(
|
const data = await this.core.eventWrapper.callNoListenerEvent(
|
||||||
|
@@ -25,9 +25,10 @@ export class NTQQGroupApi {
|
|||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.initCache().then().catch(context.logger.logError.bind(context.logger));
|
|
||||||
}
|
}
|
||||||
|
async initApi() {
|
||||||
|
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||||
|
}
|
||||||
async initCache() {
|
async initCache() {
|
||||||
this.groups = await this.getGroups();
|
this.groups = await this.getGroups();
|
||||||
for (const group of this.groups) {
|
for (const group of this.groups) {
|
||||||
@@ -54,7 +55,9 @@ export class NTQQGroupApi {
|
|||||||
}, pskey);
|
}, pskey);
|
||||||
}
|
}
|
||||||
async getGroupShutUpMemberList(groupCode: string) {
|
async getGroupShutUpMemberList(groupCode: string) {
|
||||||
return this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
||||||
|
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||||
|
return (await data)[1];
|
||||||
}
|
}
|
||||||
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
||||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
||||||
@@ -256,9 +259,9 @@ export class NTQQGroupApi {
|
|||||||
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
|
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
|
||||||
const Listener = this.core.eventWrapper.registerListen(
|
const Listener = this.core.eventWrapper.registerListen(
|
||||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||||
|
(params, _, members) => params === GroupCode && members.size > 0,
|
||||||
1,
|
1,
|
||||||
forced ? 5000 : 250,
|
forced ? 5000 : 250,
|
||||||
(params, _, members) => params === GroupCode && members.size > 0,
|
|
||||||
);
|
);
|
||||||
const retData = await (
|
const retData = await (
|
||||||
this.core.eventWrapper
|
this.core.eventWrapper
|
||||||
@@ -316,24 +319,83 @@ export class NTQQGroupApi {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
|
||||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
infos: Map<string, GroupMember>;
|
||||||
let once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 1, 2000, (params) => params.sceneId === sceneId)
|
finish: boolean;
|
||||||
.catch();
|
hasNext: boolean | undefined;
|
||||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId!, undefined, num);
|
}> {
|
||||||
|
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||||
|
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||||
|
.catch(() => { });
|
||||||
|
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||||
if (result.errCode !== 0) {
|
if (result.errCode !== 0) {
|
||||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||||
}
|
}
|
||||||
if (result.result.infos.size === 0) {
|
let resMode2;
|
||||||
return (await once)[0].infos;
|
if (modeListener) {
|
||||||
|
const ret = (await once)?.[0];
|
||||||
|
if (ret) {
|
||||||
|
resMode2 = ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result.result.infos;
|
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||||
|
return {
|
||||||
|
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||||
|
finish: result.result.finish,
|
||||||
|
hasNext: resMode2?.hasNext,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
|
||||||
|
infos: Map<string, GroupMember>;
|
||||||
|
finish: boolean;
|
||||||
|
hasNext: boolean | undefined;
|
||||||
|
listenerMode: boolean;
|
||||||
|
}> {
|
||||||
|
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||||
|
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||||
|
.catch(() => { });
|
||||||
|
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||||
|
if (result.errCode !== 0) {
|
||||||
|
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||||
|
}
|
||||||
|
let resMode2;
|
||||||
|
if (result.result.finish && result.result.infos.size === 0) {
|
||||||
|
const ret = (await once)?.[0];
|
||||||
|
if (ret) {
|
||||||
|
resMode2 = ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||||
|
//console.log('GetGroupMembersV3 len :', result.result.infos.size, resMode2?.infos.size, groupQQ);
|
||||||
|
return {
|
||||||
|
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||||
|
finish: result.result.finish,
|
||||||
|
hasNext: resMode2?.hasNext,
|
||||||
|
listenerMode: resMode2?.hasNext !== undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
|
||||||
|
if (no_cache) {
|
||||||
|
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
|
||||||
|
}
|
||||||
|
let res = await this.GetGroupMembersV3(groupQQ, num);
|
||||||
|
let ret = res.infos;
|
||||||
|
if (res.infos.size === 0 && !res.listenerMode) {
|
||||||
|
res = await this.GetGroupMembersV3(groupQQ, num);
|
||||||
|
ret = res.infos;
|
||||||
|
}
|
||||||
|
if (res.infos.size === 0) {
|
||||||
|
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||||
const groupService = this.context.session.getGroupService();
|
const groupService = this.context.session.getGroupService();
|
||||||
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
||||||
const result = await groupService.getNextMemberList(sceneId!, undefined, num);
|
const result = await groupService.getNextMemberList(sceneId, undefined, num);
|
||||||
if (result.errCode !== 0) {
|
if (result.errCode !== 0) {
|
||||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||||
}
|
}
|
||||||
@@ -341,8 +403,8 @@ export class NTQQGroupApi {
|
|||||||
return result.result.infos;
|
return result.result.infos;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupFileCount(Gids: Array<string>) {
|
async getGroupFileCount(group_ids: Array<string>) {
|
||||||
return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids);
|
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArkJsonGroupShare(GroupCode: string) {
|
async getArkJsonGroupShare(GroupCode: string) {
|
||||||
@@ -425,7 +487,7 @@ export class NTQQGroupApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getGroupRemainAtTimes(GroupCode: string) {
|
async getGroupRemainAtTimes(GroupCode: string) {
|
||||||
this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemberExtInfo(groupCode: string, uin: string) {
|
async getMemberExtInfo(groupCode: string, uin: string) {
|
||||||
|
@@ -4,5 +4,4 @@ export * from './group';
|
|||||||
export * from './msg';
|
export * from './msg';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export * from './webapi';
|
export * from './webapi';
|
||||||
export * from './sign';
|
|
||||||
export * from './system';
|
export * from './system';
|
@@ -1,8 +1,11 @@
|
|||||||
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/entities';
|
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/entities';
|
||||||
import { InstanceContext, NapCatCore } from '@/core';
|
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore } from '@/core';
|
||||||
import { GeneralCallResult } from '@/core/services/common';
|
import { GeneralCallResult } from '@/core/services/common';
|
||||||
|
|
||||||
export class NTQQMsgApi {
|
export class NTQQMsgApi {
|
||||||
|
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
|
||||||
|
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
|
||||||
|
}
|
||||||
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
|
||||||
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
|
||||||
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
// 其实以官方文档为准是最好的,https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
|
||||||
@@ -23,6 +26,10 @@ export class NTQQMsgApi {
|
|||||||
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
|
||||||
|
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
|
||||||
|
}
|
||||||
|
|
||||||
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
|
||||||
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
|
||||||
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
|
||||||
@@ -82,6 +89,18 @@ export class NTQQMsgApi {
|
|||||||
pageLimit: 1,
|
pageLimit: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
|
||||||
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
|
chatInfo: peer,
|
||||||
|
filterMsgType: [],
|
||||||
|
filterSendersUid: SendersUid,
|
||||||
|
filterMsgToTime: '0',
|
||||||
|
filterMsgFromTime: '0',
|
||||||
|
isReverseOrder: false,
|
||||||
|
isIncludeCurrent: true,
|
||||||
|
pageLimit: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
|
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
@@ -94,9 +113,9 @@ export class NTQQMsgApi {
|
|||||||
pageLimit: 1,
|
pageLimit: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//@deprecated
|
// 客户端还在用别慌
|
||||||
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
|
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
|
||||||
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
|
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
|
||||||
}
|
}
|
||||||
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
async getMsgExBySeq(peer: Peer, msgSeq: string) {
|
||||||
const DateNow = Math.floor(Date.now() / 1000);
|
const DateNow = Math.floor(Date.now() / 1000);
|
||||||
@@ -119,19 +138,29 @@ export class NTQQMsgApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getGroupFileList(GroupCode: string, params: GetFileListParam) {
|
async getGroupFileList(GroupCode: string, params: GetFileListParam) {
|
||||||
const [, groupFileListResult] = await this.core.eventWrapper.callNormalEventV2(
|
const item: GroupFileInfoUpdateItem[] = [];
|
||||||
'NodeIKernelRichMediaService/getGroupFileList',
|
let index = params.startIndex;
|
||||||
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
|
while (true) {
|
||||||
[
|
params.startIndex = index;
|
||||||
GroupCode,
|
const [, groupFileListResult] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
params,
|
'NodeIKernelRichMediaService/getGroupFileList',
|
||||||
],
|
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
|
||||||
() => true,
|
[
|
||||||
() => true, // Todo: 应当通过 groupFileListResult 判断
|
GroupCode,
|
||||||
1,
|
params,
|
||||||
5000,
|
],
|
||||||
);
|
() => true,
|
||||||
return groupFileListResult.item;
|
() => true, // 应当通过 groupFileListResult 判断
|
||||||
|
1,
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
if (!groupFileListResult?.item?.length) break;
|
||||||
|
item.push(...groupFileListResult.item);
|
||||||
|
if (groupFileListResult.isEnd) break;
|
||||||
|
if (item.length === params.fileCount) break;
|
||||||
|
index = groupFileListResult.nextIndex;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
|
async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {
|
||||||
@@ -177,7 +206,7 @@ export class NTQQMsgApi {
|
|||||||
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
|
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
|
||||||
//唉?!我有个想法
|
//唉?!我有个想法
|
||||||
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
|
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
|
||||||
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid!);
|
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid);
|
||||||
if (member) {
|
if (member) {
|
||||||
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
|
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,9 @@
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import {ChatType, InstanceContext, NapCatCore} from '..';
|
|
||||||
import offset from '@/core/external/offset.json';
|
import offset from '@/core/external/offset.json';
|
||||||
import {PacketClient, RecvPacketData} from '@/core/packet/client';
|
import { InstanceContext, NapCatCore } from "@/core";
|
||||||
import {PacketSession} from "@/core/packet/session";
|
import { LogWrapper } from "@/common/log";
|
||||||
import {PacketHexStr} from "@/core/packet/packer";
|
import { PacketClientSession } from "@/core/packet/clientSession";
|
||||||
import {NapProtoMsg} from '@/core/packet/proto/NapProto';
|
import { napCatVersion } from "@/common/version";
|
||||||
import {OidbSvcTrpcTcp0X9067_202_Rsp_Body} from '@/core/packet/proto/oidb/Oidb.0x9067_202';
|
|
||||||
import {OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp} from '@/core/packet/proto/oidb/OidbBase';
|
|
||||||
import {OidbSvcTrpcTcp0XFE1_2RSP} from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
|
|
||||||
import {LogWrapper} from "@/common/log";
|
|
||||||
import {SendLongMsgResp} from "@/core/packet/proto/message/action";
|
|
||||||
import {PacketMsg} from "@/core/packet/msg/message";
|
|
||||||
import {OidbSvcTrpcTcp0x6D6Response} from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
|
||||||
import {PacketMsgPicElement} from "@/core/packet/msg/element";
|
|
||||||
|
|
||||||
interface OffsetType {
|
interface OffsetType {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -26,118 +17,50 @@ const typedOffset: OffsetType = offset;
|
|||||||
export class NTQQPacketApi {
|
export class NTQQPacketApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
logger: LogWrapper
|
logger: LogWrapper;
|
||||||
serverUrl: string | undefined;
|
|
||||||
qqVersion: string | undefined;
|
qqVersion: string | undefined;
|
||||||
packetSession: PacketSession | undefined;
|
pkt!: PacketClientSession;
|
||||||
|
errStack: string[] = [];
|
||||||
|
|
||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.logger = core.context.logger;
|
this.logger = core.context.logger;
|
||||||
this.packetSession = undefined;
|
|
||||||
const config = this.core.configLoader.configData;
|
|
||||||
if (config && config.packetServer && config.packetServer.length > 0) {
|
|
||||||
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
|
|
||||||
this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion())
|
|
||||||
.then()
|
|
||||||
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
|
|
||||||
} else {
|
|
||||||
this.core.context.logger.logWarn('PacketServer is not set, will not init NapCat.Packet!');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
async initApi() {
|
||||||
|
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||||
|
.then()
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.logError.bind(this.core.context.logger);
|
||||||
|
this.errStack.push(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
get available(): boolean {
|
get available(): boolean {
|
||||||
return this.packetSession?.client.available ?? false;
|
return this.pkt?.available ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async InitSendPacket(serverUrl: string, qqversion: string) {
|
get clientLogStack() {
|
||||||
this.serverUrl = serverUrl;
|
return this.pkt?.clientLogStack + '\n' + this.errStack.join('\n');
|
||||||
this.qqVersion = qqversion;
|
}
|
||||||
const offsetTable: OffsetType = offset;
|
|
||||||
const table = offsetTable[qqversion + '-' + os.arch()];
|
async InitSendPacket(qqVer: string) {
|
||||||
if (!table) return false;
|
this.qqVersion = qqVer;
|
||||||
const url = 'ws://' + this.serverUrl + '/ws';
|
const table = typedOffset[qqVer + '-' + os.arch()];
|
||||||
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
|
if (!table) {
|
||||||
await this.packetSession.client.connect();
|
const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构:${qqVer}-${os.arch()},
|
||||||
await this.packetSession.client.init(process.pid, table.recv, table.send);
|
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本!`;
|
||||||
|
this.logger.logError(err);
|
||||||
|
this.errStack.push(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.core.configLoader.configData.packetBackend === 'disable') {
|
||||||
|
const err = '[Core] [Packet] 已禁用PacketBackend,NapCat.Packet将不会加载!';
|
||||||
|
this.logger.logError(err);
|
||||||
|
this.errStack.push(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.pkt = new PacketClientSession(this.core);
|
||||||
|
await this.pkt.init(process.pid, table.recv, table.send);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
|
||||||
return this.packetSession!.client.sendPacket(cmd, data, rsp);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendPokePacket(group: number, peer: number) {
|
|
||||||
const data = this.packetSession?.packer.packPokePacket(group, peer);
|
|
||||||
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendRkeyPacket() {
|
|
||||||
const packet = this.packetSession?.packer.packRkeyPacket();
|
|
||||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true);
|
|
||||||
if (!ret?.hex_data) return [];
|
|
||||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
|
||||||
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
|
|
||||||
return retData.data.rkeyList;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
|
|
||||||
let status = 0;
|
|
||||||
try {
|
|
||||||
const packet = this.packetSession?.packer.packStatusPacket(uin);
|
|
||||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true);
|
|
||||||
const data = Buffer.from(ret.hex_data, 'hex');
|
|
||||||
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
|
|
||||||
// ext & 0xff00 + ext >> 16 & 0xff
|
|
||||||
const extBigInt = BigInt(ext); // 转换为 BigInt
|
|
||||||
if (extBigInt <= 10n) {
|
|
||||||
return { status: Number(extBigInt) * 10, ext_status: 0 };
|
|
||||||
}
|
|
||||||
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符
|
|
||||||
return { status: 10, ext_status: status };
|
|
||||||
} catch (error) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
|
|
||||||
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
|
|
||||||
await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadResources(msg: PacketMsg[], groupUin: number = 0){
|
|
||||||
const reqList = []
|
|
||||||
for (const m of msg){
|
|
||||||
for (const e of m.msg){
|
|
||||||
if (e instanceof PacketMsgPicElement){
|
|
||||||
reqList.push(this.packetSession?.highwaySession.uploadImage({
|
|
||||||
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
|
|
||||||
peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid
|
|
||||||
}, e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.all(reqList);
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
|
|
||||||
await this.uploadResources(msg, groupUin);
|
|
||||||
const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin);
|
|
||||||
const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true);
|
|
||||||
this.logger.logDebug('sendUploadForwardMsg', ret);
|
|
||||||
const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex'));
|
|
||||||
return resp.result.resId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
|
|
||||||
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
|
|
||||||
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true);
|
|
||||||
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
|
|
||||||
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
|
|
||||||
if (resp.download.retCode !== 0){
|
|
||||||
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
|
|
||||||
}
|
|
||||||
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
import { RequestUtil } from '@/common/request';
|
|
||||||
import { MiniAppLuaJsonType } from '@/core';
|
|
||||||
import { InstanceContext, NapCatCore } from '..';
|
import { InstanceContext, NapCatCore } from '..';
|
||||||
|
|
||||||
export class NTQQMusicSignApi {
|
export class NTQQMusicSignApi {
|
||||||
@@ -10,210 +8,6 @@ export class NTQQMusicSignApi {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
|
|
||||||
async signMiniApp(CardData: MiniAppLuaJsonType) {
|
|
||||||
// {
|
|
||||||
// "app": "com.tencent.miniapp.lua",
|
|
||||||
// "bizsrc": "tianxuan.imgJumpArk",
|
|
||||||
// "view": "miniapp",
|
|
||||||
// "prompt": "hi! 这里有我的日常故事,只想讲给你听",
|
|
||||||
// "config": {
|
|
||||||
// "type": "normal",
|
|
||||||
// "forward": 1,
|
|
||||||
// "autosize": 0
|
|
||||||
// },
|
|
||||||
// "meta": {
|
|
||||||
// "miniapp": {
|
|
||||||
// "title": "hi! 这里有我的日常故事,只想讲给你听",
|
|
||||||
// "preview": "https:\/\/tianquan.gtimg.cn\/qqAIAgent\/item\/7\/square.png",
|
|
||||||
// "jumpUrl": "https:\/\/club.vip.qq.com\/transfer?open_kuikly_info=%7B%22version%22%3A%20%221%22%2C%22src_type%22%3A%20%22web%22%2C%22kr_turbo_display%22%3A%20%221%22%2C%22page_name%22%3A%20%22vas_ai_persona_moments%22%2C%22bundle_name%22%3A%20%22vas_ai_persona_moments%22%7D&page_name=vas_ai_persona_moments&enteranceId=share&robot_uin=3889008584",
|
|
||||||
// "tag": "QQ智能体",
|
|
||||||
// "tagIcon": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png",
|
|
||||||
// "source": "QQ智能体",
|
|
||||||
// "sourcelogo": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// token : function(url,skey){
|
|
||||||
// var str = skey || cookie('skey') || cookie('rv2') || '',
|
|
||||||
// hash = 5381;
|
|
||||||
// if(url){
|
|
||||||
// var hostname = uri(url).hostname;
|
|
||||||
// if(hostname.indexOf('qun.qq.com') > -1 || (hostname.indexOf('qzone.qq.com') > -1 && hostname.indexOf('qun.qzone.qq.com') === -1)){
|
|
||||||
// str = cookie('p_skey') || str;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// for(var i = 0, len = str.length; i < len; ++i){
|
|
||||||
// hash += (hash << 5) + str.charAt(i).charCodeAt();
|
|
||||||
// }
|
|
||||||
// return hash & 0x7fffffff;
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
|
|
||||||
// function signToken(skey: string) {
|
|
||||||
// let hash = 5381;
|
|
||||||
// for (let i = 0, len = skey.length; i < len; ++i) {
|
|
||||||
// hash += (hash << 5) + skey.charCodeAt(i);
|
|
||||||
// }
|
|
||||||
// return hash & 0x7fffffff;
|
|
||||||
// }
|
|
||||||
const signCard = {
|
|
||||||
'app': 'com.tencent.miniapp.lua',
|
|
||||||
'bizsrc': 'tianxuan.imgJumpArk',
|
|
||||||
'view': 'miniapp',
|
|
||||||
'prompt': CardData.prompt,
|
|
||||||
'config': {
|
|
||||||
'type': 'normal',
|
|
||||||
'forward': 1,
|
|
||||||
'autosize': 0,
|
|
||||||
},
|
|
||||||
'meta': {
|
|
||||||
'miniapp': {
|
|
||||||
'title': CardData.title,
|
|
||||||
'preview': (CardData.preview as string).replace(/\\/g, '\\/\\/'),
|
|
||||||
'jumpUrl': (CardData.jumpUrl as string).replace(/\\/g, '\\/\\/'),
|
|
||||||
'tag': CardData.tag,
|
|
||||||
'tagIcon': (CardData.tagIcon as string).replace(/\\/g, '\\/\\/'),
|
|
||||||
'source': CardData.source,
|
|
||||||
'sourcelogo': (CardData.sourcelogo as string).replace(/\\/g, '\\/\\/'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// let signCard = {
|
|
||||||
// "app": "com.tencent.eventshare.lua",
|
|
||||||
// "prompt": "Bot Test",
|
|
||||||
// "bizsrc": "tianxuan.business",
|
|
||||||
// "meta": {
|
|
||||||
// "eventshare": {
|
|
||||||
// "button1URL": "https://www.bilibili.com",
|
|
||||||
// "button1disable": false,
|
|
||||||
// "button1title": "点我前往",
|
|
||||||
// "button2URL": "",
|
|
||||||
// "button2disable": false,
|
|
||||||
// "button2title": "",
|
|
||||||
// "buttonNum": 1,
|
|
||||||
// "jumpURL": "https://www.bilibili.com",
|
|
||||||
// "preview": "https://tianquan.gtimg.cn/shoal/card/9930bc4e-4a92-4da3-814f-8094a2421d9c.png",
|
|
||||||
// "tag": "QQ集卡",
|
|
||||||
// "tagIcon": "https://tianquan.gtimg.cn/shoal/card/c034854b-102d-40be-a545-5ca90a7c49c9.png",
|
|
||||||
// "title": "Bot Test"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "config": {
|
|
||||||
// "autosize": 0,
|
|
||||||
// "collect": 0,
|
|
||||||
// "ctime": 1716568575,
|
|
||||||
// "forward": 1,
|
|
||||||
// "height": 336,
|
|
||||||
// "reply": 0,
|
|
||||||
// "round": 1,
|
|
||||||
// "type": "normal",
|
|
||||||
// "width": 263
|
|
||||||
// },
|
|
||||||
// "view": "eventshare",
|
|
||||||
// "ver": "0.0.0.1"
|
|
||||||
// };
|
|
||||||
const data = (await this.core.apis.UserApi.getQzoneCookies());
|
|
||||||
const Bkn = this.core.apis.WebApi.getBknFromCookie(data.p_skey);
|
|
||||||
|
|
||||||
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + this.core.selfInfo.uin + '; uin=o' + this.core.selfInfo.uin;
|
|
||||||
|
|
||||||
const signurl = 'https://h5.qzone.qq.com/v2/vip/tx/trpc/ark-share/GenNewSignedArk?g_tk=' + Bkn + '&ark=' + encodeURIComponent(JSON.stringify(signCard));
|
|
||||||
let signed_ark = '';
|
|
||||||
try {
|
|
||||||
const retData = await RequestUtil.HttpGetJson<{
|
|
||||||
code: number,
|
|
||||||
data: { signed_ark: string }
|
|
||||||
}>(signurl, 'GET', undefined, { Cookie: CookieValue });
|
|
||||||
//logDebug('MiniApp JSON 消息生成成功', retData);
|
|
||||||
signed_ark = retData.data.signed_ark;
|
|
||||||
} catch (error) {
|
|
||||||
this.context.logger.logDebug('MiniApp JSON 消息生成失败', error);
|
|
||||||
}
|
|
||||||
return signed_ark;
|
|
||||||
}
|
|
||||||
|
|
||||||
async signInternal(songname: string, singer: string, cover: string, songmid: string, songmusic: string) {
|
|
||||||
//curl -X POST 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003' -H 'Content-Type: application/json' -H 'Cookie: uin=o10086' -d '{"app":"com.tencent.qqreader.share","config":{"ctime":1718634110,"forward":1,"token":"9a63343c32d5a16bcde653eb97faa25d","type":"normal"},"extra":{"app_type":1,"appid":100497308,"msg_seq":14386738075403815000.0,"uin":1733139081},"meta":{"music":{"action":"","android_pkg_name":"","app_type":1,"appid":100497308,"ctime":1718634110,"desc":"周杰伦","jumpUrl":"https://i.y.qq.com/v8/playsong.html?songmid=0039MnYb0qxYhV&type=0","musicUrl":"http://ws.stream.qqmusic.qq.com/http://isure6.stream.qqmusic.qq.com/M800002202B43Cq4V4.mp3?fromtag=810033622&guid=br_xzg&trace=23fe7bcbe2336bbf&uin=553&vkey=CF0F5CE8B0FA16F3001F8A88D877A217EB5E4F00BDCEF1021EB6C48969CA33C6303987AEECE9CC840122DD2F917A59D6130D8A8CA4577C87","preview":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","cover":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","sourceMsgId":"0","source_icon":"https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0","source_url":"","tag":"QQ音乐","title":"晴天","uin":10086}},"prompt":"[分享]晴天","ver":"0.0.0.1","view":"music"}'
|
|
||||||
const signurl = 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003';
|
|
||||||
//let = "https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg";
|
|
||||||
const signCard = {
|
|
||||||
app: 'com.tencent.qqreader.share',
|
|
||||||
config: {
|
|
||||||
ctime: 1718634110,
|
|
||||||
forward: 1,
|
|
||||||
token: '9a63343c32d5a16bcde653eb97faa25d',
|
|
||||||
type: 'normal',
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
app_type: 1,
|
|
||||||
appid: 100497308,
|
|
||||||
msg_seq: 14386738075403815000,
|
|
||||||
uin: 1733139081,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
music: {
|
|
||||||
action: '',
|
|
||||||
android_pkg_name: '',
|
|
||||||
app_type: 1,
|
|
||||||
appid: 100497308,
|
|
||||||
ctime: 1718634110,
|
|
||||||
desc: singer,
|
|
||||||
jumpUrl: 'https://i.y.qq.com/v8/playsong.html?songmid=' + songmid + '&type=0',
|
|
||||||
musicUrl: songmusic,
|
|
||||||
preview: cover,
|
|
||||||
cover: cover,
|
|
||||||
sourceMsgId: '0',
|
|
||||||
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
|
|
||||||
source_url: '',
|
|
||||||
tag: 'QQ音乐',
|
|
||||||
title: songname,
|
|
||||||
uin: 10086,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
prompt: '[分享]' + songname,
|
|
||||||
ver: '0.0.0.1',
|
|
||||||
view: 'music',
|
|
||||||
};
|
|
||||||
//console.log(JSON.stringify(signCard, null, 2));
|
|
||||||
const data = await RequestUtil.HttpGetJson<{ code: number, data: { arkResult: string } }>
|
|
||||||
(signurl, 'POST', signCard, { 'Cookie': 'uin=o10086', 'Content-Type': 'application/json' });
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
//注意处理错误
|
|
||||||
async signWay03(id: string = '', mid: string = '') {
|
|
||||||
let signedMid;
|
|
||||||
if (mid == '') {
|
|
||||||
const MusicInfo = await RequestUtil.HttpGetJson<{
|
|
||||||
songinfo?: {
|
|
||||||
data?: {
|
|
||||||
track_info: {
|
|
||||||
mid: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}>(
|
|
||||||
'https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"songinfo":{"method":"get_song_detail_yqq","param":{"song_type":0,"song_mid":"","song_id":' + id + '},"module":"music.pf_song_detail_svr"}}',
|
|
||||||
'GET',
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
signedMid = MusicInfo.songinfo?.data?.track_info.mid;
|
|
||||||
}
|
|
||||||
//第三方接口 存在速率限制 现在勉强用
|
|
||||||
const MusicReal = await RequestUtil.HttpGetJson<{
|
|
||||||
code: number,
|
|
||||||
data?: {
|
|
||||||
name: string,
|
|
||||||
singer: string,
|
|
||||||
url: string,
|
|
||||||
cover: string
|
|
||||||
}
|
|
||||||
}>('https://api.leafone.cn/api/qqmusic?id=' + signedMid + '&type=8', 'GET');
|
|
||||||
//console.log(MusicReal);
|
|
||||||
return { ...MusicReal.data, mid: signedMid };
|
|
||||||
}
|
|
||||||
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
||||||
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
||||||
|
|
||||||
@@ -227,10 +21,5 @@ export class NTQQMusicSignApi {
|
|||||||
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
|
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
|
||||||
|
|
||||||
//还有一处公告上传可以上传高质量图片 持久为qq域名
|
//还有一处公告上传可以上传高质量图片 持久为qq域名
|
||||||
async SignMusicWrapper(id: string = '') {
|
|
||||||
const MusicInfo = await this.signWay03(id)!;
|
|
||||||
return await this.signInternal(MusicInfo.name!, MusicInfo.singer!, MusicInfo.cover!, MusicInfo.mid!, 'https://ws.stream.qqmusic.qq.com/' + MusicInfo.url!);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,7 +18,7 @@ export class NTQQUserApi {
|
|||||||
async getStatusByUid(uid: string) {
|
async getStatusByUid(uid: string) {
|
||||||
return this.context.session.getProfileService().getStatus(uid);
|
return this.context.session.getProfileService().getStatus(uid);
|
||||||
}
|
}
|
||||||
async getProfileLike(uid: string) {
|
async getProfileLike(uid: string, start: number, count: number) {
|
||||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||||
friendUids: [uid],
|
friendUids: [uid],
|
||||||
basic: 1,
|
basic: 1,
|
||||||
@@ -26,8 +26,8 @@ export class NTQQUserApi {
|
|||||||
favorite: 0,
|
favorite: 0,
|
||||||
userProfile: 1,
|
userProfile: 1,
|
||||||
type: 2,
|
type: 2,
|
||||||
start: 0,
|
start: start,
|
||||||
limit: 20,
|
limit: count,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async fetchOtherProfileLike(uid: string) {
|
async fetchOtherProfileLike(uid: string) {
|
||||||
|
@@ -8,6 +8,9 @@ import {
|
|||||||
WebHonorType,
|
WebHonorType,
|
||||||
} from '@/core';
|
} from '@/core';
|
||||||
import { NapCatCore } from '..';
|
import { NapCatCore } from '..';
|
||||||
|
import { createReadStream, readFileSync, statSync } from 'node:fs';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { basename } from 'node:path';
|
||||||
|
|
||||||
export class NTQQWebApi {
|
export class NTQQWebApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -212,108 +215,65 @@ export class NTQQWebApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getDataInternal(cookieObject: any, groupCode: string, type: number) {
|
||||||
|
let resJson;
|
||||||
|
try {
|
||||||
|
const res = await RequestUtil.HttpGetText(
|
||||||
|
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
||||||
|
gc: groupCode,
|
||||||
|
type: type.toString(),
|
||||||
|
}).toString()}`,
|
||||||
|
'GET',
|
||||||
|
'',
|
||||||
|
{ 'Cookie': this.cookieToString(cookieObject) }
|
||||||
|
);
|
||||||
|
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
||||||
|
if (match) {
|
||||||
|
resJson = JSON.parse(match[1].trim());
|
||||||
|
}
|
||||||
|
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
|
||||||
|
} catch (e) {
|
||||||
|
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getHonorList(cookieObject: any, groupCode: string, type: number) {
|
||||||
|
const data = await this.getDataInternal(cookieObject, groupCode, type);
|
||||||
|
if (!data) {
|
||||||
|
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return data.map((item: any) => ({
|
||||||
|
user_id: item?.uin,
|
||||||
|
nickname: item?.name,
|
||||||
|
avatar: item?.avatar,
|
||||||
|
description: item?.desc,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
|
||||||
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
|
||||||
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
|
|
||||||
let resJson;
|
|
||||||
try {
|
|
||||||
const res = await RequestUtil.HttpGetText(
|
|
||||||
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
|
|
||||||
gc: Internal_groupCode,
|
|
||||||
type: Internal_type.toString(),
|
|
||||||
}).toString()}`,
|
|
||||||
'GET',
|
|
||||||
'',
|
|
||||||
{ 'Cookie': this.cookieToString(cookieObject) }
|
|
||||||
);
|
|
||||||
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
|
|
||||||
if (match) {
|
|
||||||
resJson = JSON.parse(match[1].trim());
|
|
||||||
}
|
|
||||||
if (Internal_type === 1) {
|
|
||||||
return resJson?.talkativeList;
|
|
||||||
} else {
|
|
||||||
return resJson?.actorList;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.context.logger.logDebug('获取当前群荣耀失败', e);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const HonorInfo: any = { group_id: groupCode };
|
const HonorInfo: any = { group_id: groupCode };
|
||||||
|
|
||||||
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
|
||||||
const RetInternal = await getDataInternal(groupCode, 1);
|
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
|
||||||
if (RetInternal) {
|
if (talkativeList.length > 0) {
|
||||||
HonorInfo.current_talkative = {
|
HonorInfo.current_talkative = talkativeList[0];
|
||||||
user_id: RetInternal[0]?.uin,
|
HonorInfo.talkative_list = talkativeList;
|
||||||
avatar: RetInternal[0]?.avatar,
|
|
||||||
nickname: RetInternal[0]?.name,
|
|
||||||
day_count: 0,
|
|
||||||
description: RetInternal[0]?.desc,
|
|
||||||
};
|
|
||||||
HonorInfo.talkative_list = [];
|
|
||||||
for (const talkative_ele of RetInternal) {
|
|
||||||
HonorInfo.talkative_list.push({
|
|
||||||
user_id: talkative_ele?.uin,
|
|
||||||
avatar: talkative_ele?.avatar,
|
|
||||||
description: talkative_ele?.desc,
|
|
||||||
day_count: 0,
|
|
||||||
nickname: talkative_ele?.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取龙王信息失败');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
||||||
const RetInternal = await getDataInternal(groupCode, 2);
|
HonorInfo.performer_list = await this.getHonorList(cookieObject, groupCode, 2);
|
||||||
if (RetInternal) {
|
|
||||||
HonorInfo.performer_list = [];
|
|
||||||
for (const performer_ele of RetInternal) {
|
|
||||||
HonorInfo.performer_list.push({
|
|
||||||
user_id: performer_ele?.uin,
|
|
||||||
nickname: performer_ele?.name,
|
|
||||||
avatar: performer_ele?.avatar,
|
|
||||||
description: performer_ele?.desc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
|
|
||||||
const RetInternal = await getDataInternal(groupCode, 3);
|
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
|
||||||
if (RetInternal) {
|
HonorInfo.legend_list = await this.getHonorList(cookieObject, groupCode, 3);
|
||||||
HonorInfo.legend_list = [];
|
|
||||||
for (const legend_ele of RetInternal) {
|
|
||||||
HonorInfo.legend_list.push({
|
|
||||||
user_id: legend_ele?.uin,
|
|
||||||
nickname: legend_ele?.name,
|
|
||||||
avatar: legend_ele?.avatar,
|
|
||||||
desc: legend_ele?.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取群聊炽焰失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
|
||||||
const RetInternal = await getDataInternal(groupCode, 6);
|
HonorInfo.emotion_list = await this.getHonorList(cookieObject, groupCode, 6);
|
||||||
if (RetInternal) {
|
|
||||||
HonorInfo.emotion_list = [];
|
|
||||||
for (const emotion_ele of RetInternal) {
|
|
||||||
HonorInfo.emotion_list.push({
|
|
||||||
user_id: emotion_ele.uin,
|
|
||||||
nickname: emotion_ele.name,
|
|
||||||
avatar: emotion_ele.avatar,
|
|
||||||
desc: emotion_ele.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取快乐源泉失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 冒尖小春笋好像已经被tx扬了 R.I.P.
|
// 冒尖小春笋好像已经被tx扬了 R.I.P.
|
||||||
@@ -338,4 +298,118 @@ export class NTQQWebApi {
|
|||||||
}
|
}
|
||||||
return (hash & 0x7FFFFFFF).toString();
|
return (hash & 0x7FFFFFFF).toString();
|
||||||
}
|
}
|
||||||
|
public getBknFromSKey(sKey: string) {
|
||||||
|
let hash = 5381;
|
||||||
|
for (let i = 0; i < sKey.length; i++) {
|
||||||
|
const code = sKey.charCodeAt(i);
|
||||||
|
hash = hash + (hash << 5) + code;
|
||||||
|
}
|
||||||
|
return (hash & 0x7FFFFFFF).toString();
|
||||||
|
}
|
||||||
|
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) {
|
||||||
|
const img = readFileSync(path);
|
||||||
|
const img_md5 = createHash('md5').update(img).digest('hex');
|
||||||
|
const img_size = img.length;
|
||||||
|
const img_name = basename(path);
|
||||||
|
const time = Math.floor(Date.now() / 1000);
|
||||||
|
const GTK = this.getBknFromSKey(pskey);
|
||||||
|
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
|
||||||
|
const body = {
|
||||||
|
control_req: [{
|
||||||
|
uin: uin,
|
||||||
|
token: {
|
||||||
|
type: 4,
|
||||||
|
data: pskey,
|
||||||
|
appid: 5
|
||||||
|
},
|
||||||
|
appid: "qun",
|
||||||
|
checksum: img_md5,
|
||||||
|
check_type: 0,
|
||||||
|
file_len: img_size,
|
||||||
|
env: {
|
||||||
|
refer: "qzone",
|
||||||
|
deviceInfo: "h5"
|
||||||
|
},
|
||||||
|
model: 0,
|
||||||
|
biz_req: {
|
||||||
|
sPicTitle: img_name,
|
||||||
|
sPicDesc: "",
|
||||||
|
sAlbumName: sAlbumName,
|
||||||
|
sAlbumID: sAlbumID,
|
||||||
|
iAlbumTypeID: 0,
|
||||||
|
iBitmap: 0,
|
||||||
|
iUploadType: 0,
|
||||||
|
iUpPicType: 0,
|
||||||
|
iBatchID: time,
|
||||||
|
sPicPath: "",
|
||||||
|
iPicWidth: 0,
|
||||||
|
iPicHight: 0,
|
||||||
|
iWaterType: 0,
|
||||||
|
iDistinctUse: 0,
|
||||||
|
iNeedFeeds: 1,
|
||||||
|
iUploadTime: time,
|
||||||
|
mapExt: {
|
||||||
|
appid: "qun",
|
||||||
|
userid: gc
|
||||||
|
}
|
||||||
|
},
|
||||||
|
session: "",
|
||||||
|
asy_upload: 0,
|
||||||
|
cmd: "FileUpload"
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
|
||||||
|
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
|
||||||
|
"Cookie": cookie,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
});
|
||||||
|
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
|
||||||
|
const img_size = statSync(path).size;
|
||||||
|
const img_name = basename(path);
|
||||||
|
let seq = 0;
|
||||||
|
let offset = 0;
|
||||||
|
const GTK = this.getBknFromSKey(pskey);
|
||||||
|
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
|
||||||
|
|
||||||
|
const stream = createReadStream(path, { highWaterMark: slice_size });
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const end = Math.min(offset + chunk.length, img_size);
|
||||||
|
const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`;
|
||||||
|
const formData = await RequestUtil.createFormData(boundary, path);
|
||||||
|
|
||||||
|
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
|
||||||
|
const body = {
|
||||||
|
uin: uin,
|
||||||
|
appid: "qun",
|
||||||
|
session: session,
|
||||||
|
offset: offset,
|
||||||
|
data: formData,
|
||||||
|
checksum: "",
|
||||||
|
check_type: 0,
|
||||||
|
retry: 0,
|
||||||
|
seq: seq,
|
||||||
|
end: end,
|
||||||
|
cmd: "FileUpload",
|
||||||
|
slice_size: slice_size,
|
||||||
|
"biz_req.iUploadType": 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
|
||||||
|
"Cookie": cookie,
|
||||||
|
"Content-Type": `multipart/form-data; boundary=${boundary}`
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += chunk.length;
|
||||||
|
seq++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) {
|
||||||
|
const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session;
|
||||||
|
return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -117,7 +117,7 @@ export enum GroupMemberRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupMember {
|
export interface GroupMember {
|
||||||
memberRealLevel: string | undefined;
|
memberRealLevel: number | undefined;
|
||||||
memberSpecialTitle?: string;
|
memberSpecialTitle?: string;
|
||||||
avatarPath: string;
|
avatarPath: string;
|
||||||
cardName: string;
|
cardName: string;
|
||||||
|
@@ -27,94 +27,70 @@ export interface GetFileListParam {
|
|||||||
|
|
||||||
export enum ElementType {
|
export enum ElementType {
|
||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
|
|
||||||
TEXT = 1,
|
TEXT = 1,
|
||||||
|
|
||||||
PIC = 2,
|
PIC = 2,
|
||||||
|
|
||||||
FILE = 3,
|
FILE = 3,
|
||||||
|
|
||||||
PTT = 4,
|
PTT = 4,
|
||||||
|
|
||||||
VIDEO = 5,
|
VIDEO = 5,
|
||||||
|
|
||||||
FACE = 6,
|
FACE = 6,
|
||||||
|
|
||||||
REPLY = 7,
|
REPLY = 7,
|
||||||
|
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
|
||||||
WALLET = 9,
|
WALLET = 9,
|
||||||
|
|
||||||
/**
|
|
||||||
* “小灰条”,包括拍一拍 (Poke)、撤回提示等
|
|
||||||
*/
|
|
||||||
GreyTip = 8,
|
|
||||||
|
|
||||||
ARK = 10,
|
ARK = 10,
|
||||||
|
|
||||||
MFACE = 11,
|
MFACE = 11,
|
||||||
|
|
||||||
LIVEGIFT = 12,
|
LIVEGIFT = 12,
|
||||||
|
|
||||||
STRUCTLONGMSG = 13,
|
STRUCTLONGMSG = 13,
|
||||||
|
|
||||||
MARKDOWN = 14,
|
MARKDOWN = 14,
|
||||||
|
|
||||||
GIPHY = 15,
|
GIPHY = 15,
|
||||||
|
|
||||||
MULTIFORWARD = 16,
|
MULTIFORWARD = 16,
|
||||||
|
|
||||||
INLINEKEYBOARD = 17,
|
INLINEKEYBOARD = 17,
|
||||||
|
|
||||||
INTEXTGIFT = 18,
|
INTEXTGIFT = 18,
|
||||||
|
|
||||||
CALENDAR = 19,
|
CALENDAR = 19,
|
||||||
|
|
||||||
YOLOGAMERESULT = 20,
|
YOLOGAMERESULT = 20,
|
||||||
|
|
||||||
AVRECORD = 21,
|
AVRECORD = 21,
|
||||||
|
|
||||||
FEED = 22,
|
FEED = 22,
|
||||||
|
|
||||||
TOFURECORD = 23,
|
TOFURECORD = 23,
|
||||||
|
|
||||||
ACEBUBBLE = 24,
|
ACEBUBBLE = 24,
|
||||||
|
|
||||||
ACTIVITY = 25,
|
ACTIVITY = 25,
|
||||||
|
|
||||||
TOFU = 26,
|
TOFU = 26,
|
||||||
|
|
||||||
FACEBUBBLE = 27,
|
FACEBUBBLE = 27,
|
||||||
|
|
||||||
SHARELOCATION = 28,
|
SHARELOCATION = 28,
|
||||||
|
|
||||||
TASKTOPMSG = 29,
|
TASKTOPMSG = 29,
|
||||||
|
|
||||||
RECOMMENDEDMSG = 43,
|
RECOMMENDEDMSG = 43,
|
||||||
|
|
||||||
ACTIONBAR = 44
|
ACTIONBAR = 44
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
|
||||||
|
|
||||||
|
type ElementBase<
|
||||||
|
K extends keyof ElementFullBase,
|
||||||
|
S extends Partial<{ [P in K]: keyof NonNullable<ElementFullBase[P]> | Array<keyof NonNullable<ElementFullBase[P]>> }> = object
|
||||||
|
> = {
|
||||||
|
[P in K]:
|
||||||
|
S[P] extends Array<infer U>
|
||||||
|
? Pick<NonNullable<ElementFullBase[P]>, U & keyof NonNullable<ElementFullBase[P]>>
|
||||||
|
: S[P] extends keyof NonNullable<ElementFullBase[P]>
|
||||||
|
? Pick<NonNullable<ElementFullBase[P]>, S[P]>
|
||||||
|
: NonNullable<ElementFullBase[P]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SendElementBase<ET extends ElementType> {
|
||||||
|
elementType: ET;
|
||||||
|
elementId: string;
|
||||||
|
extBufForUI?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ActionBarElement {
|
export interface ActionBarElement {
|
||||||
rows: InlineKeyboardRow[];
|
rows: InlineKeyboardRow[];
|
||||||
botAppid: string;
|
botAppid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendActionBarElement {
|
|
||||||
elementType: ElementType.ACTIONBAR;
|
|
||||||
elementId: string;
|
|
||||||
actionBarElement: ActionBarElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecommendedMsgElement {
|
export interface RecommendedMsgElement {
|
||||||
rows: InlineKeyboardRow[];
|
rows: InlineKeyboardRow[];
|
||||||
botAppid: string;
|
botAppid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendRecommendedMsgElement {
|
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
|
||||||
elementType: ElementType.RECOMMENDEDMSG;
|
|
||||||
elementId: string;
|
|
||||||
recommendedMsgElement: RecommendedMsgElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InlineKeyboardButton {
|
export interface InlineKeyboardButton {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -171,11 +147,7 @@ export enum NTMsgType {
|
|||||||
KMSGTYPEWALLET = 10
|
KMSGTYPEWALLET = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendTaskTopMsgElement {
|
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
|
||||||
elementType: ElementType.TASKTOPMSG;
|
|
||||||
elementId: string;
|
|
||||||
taskTopMsgElement: TaskTopMsgElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TofuRecordElement {
|
export interface TofuRecordElement {
|
||||||
type: number;
|
type: number;
|
||||||
@@ -194,11 +166,7 @@ export interface TofuRecordElement {
|
|||||||
onscreennotify: boolean;
|
onscreennotify: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendTofuRecordElement {
|
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
|
||||||
elementType: ElementType.TOFURECORD;
|
|
||||||
elementId: string;
|
|
||||||
tofuRecordElement: TofuRecordElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FaceBubbleElement {
|
export interface FaceBubbleElement {
|
||||||
faceCount: number;
|
faceCount: number;
|
||||||
@@ -216,12 +184,7 @@ export interface FaceBubbleElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendFaceBubbleElement {
|
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
|
||||||
elementType: ElementType.FACEBUBBLE;
|
|
||||||
elementId: string;
|
|
||||||
faceBubbleElement: FaceBubbleElement;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AvRecordElement {
|
export interface AvRecordElement {
|
||||||
type: number;
|
type: number;
|
||||||
@@ -232,11 +195,7 @@ export interface AvRecordElement {
|
|||||||
extraType: number;
|
extraType: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendavRecordElement {
|
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
|
||||||
elementType: ElementType.AVRECORD;
|
|
||||||
elementId: string;
|
|
||||||
avRecordElement: AvRecordElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YoloUserInfo {
|
export interface YoloUserInfo {
|
||||||
uid: string;
|
uid: string;
|
||||||
@@ -245,24 +204,13 @@ export interface YoloUserInfo {
|
|||||||
bizId: string;
|
bizId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendInlineKeyboardElement {
|
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
|
||||||
elementType: ElementType.INLINEKEYBOARD;
|
|
||||||
elementId: string;
|
|
||||||
inlineKeyboardElement: {
|
|
||||||
rows: number;
|
|
||||||
botAppid: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YoloGameResultElement {
|
export interface YoloGameResultElement {
|
||||||
UserInfo: YoloUserInfo[];
|
UserInfo: YoloUserInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendYoloGameResultElement {
|
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
|
||||||
elementType: ElementType.YOLOGAMERESULT;
|
|
||||||
yoloGameResultElement: YoloGameResultElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GiphyElement {
|
export interface GiphyElement {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -271,17 +219,9 @@ export interface GiphyElement {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendGiphyElement {
|
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
|
||||||
elementType: ElementType.GIPHY;
|
|
||||||
elementId: string;
|
|
||||||
giphyElement: GiphyElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendWalletElement {
|
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
|
||||||
elementType: ElementType.UNKNOWN;//不做 设置位置
|
|
||||||
elementId: string;
|
|
||||||
walletElement: Record<string, never>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CalendarElement {
|
export interface CalendarElement {
|
||||||
summary: string;
|
summary: string;
|
||||||
@@ -291,49 +231,16 @@ export interface CalendarElement {
|
|||||||
schema: string;
|
schema: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendCalendarElement {
|
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
|
||||||
elementType: ElementType.CALENDAR;
|
|
||||||
elementId: string;
|
|
||||||
calendarElement: CalendarElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendliveGiftElement {
|
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
|
||||||
elementType: ElementType.LIVEGIFT;
|
|
||||||
elementId: string;
|
|
||||||
liveGiftElement: Record<string, never>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendTextElement {
|
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
|
||||||
elementType: ElementType.TEXT;
|
|
||||||
elementId: string;
|
|
||||||
textElement: {
|
|
||||||
content: string;
|
|
||||||
atType: number;
|
|
||||||
atUid: string;
|
|
||||||
atTinyId: string;
|
|
||||||
atNtUid: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendPttElement {
|
export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
|
||||||
elementType: ElementType.PTT;
|
pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
|
||||||
elementId: string;
|
'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
|
||||||
pttElement: {
|
}>;
|
||||||
fileName: string;
|
|
||||||
filePath: string;
|
|
||||||
md5HexStr: string;
|
|
||||||
fileSize: number;
|
|
||||||
duration: number; // 单位是秒
|
|
||||||
formatType: number;
|
|
||||||
voiceType: number;
|
|
||||||
voiceChangeType: number;
|
|
||||||
canConvert2Text: boolean;
|
|
||||||
waveAmplitudes: number[];
|
|
||||||
fileSubId: string;
|
|
||||||
playState: number;
|
|
||||||
autoConvertText: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PicType {
|
export enum PicType {
|
||||||
gif = 2000,
|
gif = 2000,
|
||||||
@@ -359,11 +266,7 @@ export enum NTMsgAtType {
|
|||||||
ATTYPEUNKNOWN = 0
|
ATTYPEUNKNOWN = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendPicElement {
|
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
|
||||||
elementType: ElementType.PIC;
|
|
||||||
elementId: string;
|
|
||||||
picElement: PicElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReplyElement {
|
export interface ReplyElement {
|
||||||
sourceMsgIdInRecords?: string;
|
sourceMsgIdInRecords?: string;
|
||||||
@@ -375,53 +278,27 @@ export interface ReplyElement {
|
|||||||
replyMsgClientSeq?: string;
|
replyMsgClientSeq?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendReplyElement {
|
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
|
||||||
elementType: ElementType.REPLY;
|
|
||||||
elementId: string;
|
|
||||||
replyElement: ReplyElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendFaceElement {
|
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
|
||||||
elementType: ElementType.FACE;
|
|
||||||
elementId: string;
|
|
||||||
faceElement: FaceElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendMarketFaceElement {
|
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
|
||||||
elementType: ElementType.MFACE;
|
|
||||||
marketFaceElement: MarketFaceElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendStructLongMsgElement {
|
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
|
||||||
elementType: ElementType.STRUCTLONGMSG;
|
|
||||||
elementId: string;
|
|
||||||
structLongMsgElement: StructLongMsgElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StructLongMsgElement {
|
export interface StructLongMsgElement {
|
||||||
xmlContent: string;
|
xmlContent: string;
|
||||||
resId: string;
|
resId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendactionBarElement {
|
export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
|
||||||
elementType: ElementType.ACTIONBAR;
|
|
||||||
elementId: string;
|
|
||||||
actionBarElement: {
|
|
||||||
rows: number;
|
|
||||||
botAppid: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShareLocationElement {
|
export interface ShareLocationElement {
|
||||||
text: string;
|
text: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendShareLocationElement {
|
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
|
||||||
elementType: ElementType.SHARELOCATION;
|
|
||||||
elementId: string;
|
|
||||||
shareLocationElement?: ShareLocationElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileElement {
|
export interface FileElement {
|
||||||
fileMd5?: string;
|
fileMd5?: string;
|
||||||
@@ -441,29 +318,13 @@ export interface FileElement {
|
|||||||
fileBizId?: number;
|
fileBizId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SendFileElement {
|
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
|
||||||
elementType: ElementType.FILE;
|
|
||||||
elementId: string;
|
|
||||||
fileElement: FileElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendVideoElement {
|
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
|
||||||
elementType: ElementType.VIDEO;
|
|
||||||
elementId: string;
|
|
||||||
videoElement: VideoElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendArkElement {
|
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
|
||||||
elementType: ElementType.ARK;
|
|
||||||
elementId: string;
|
|
||||||
arkElement: ArkElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendMarkdownElement {
|
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
|
||||||
elementType: ElementType.MARKDOWN;
|
|
||||||
elementId: string;
|
|
||||||
markdownElement: MarkdownElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendMessageElement = SendTextElement | SendPttElement |
|
export type SendMessageElement = SendTextElement | SendPttElement |
|
||||||
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
|
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
|
||||||
@@ -480,7 +341,7 @@ export interface TextElement {
|
|||||||
export interface MessageElement {
|
export interface MessageElement {
|
||||||
elementType: ElementType,
|
elementType: ElementType,
|
||||||
elementId: string,
|
elementId: string,
|
||||||
extBufForUI: string,//"0x",
|
extBufForUI?: string, //"0x",
|
||||||
textElement?: TextElement;
|
textElement?: TextElement;
|
||||||
faceElement?: FaceElement,
|
faceElement?: FaceElement,
|
||||||
marketFaceElement?: MarketFaceElement,
|
marketFaceElement?: MarketFaceElement,
|
||||||
@@ -509,7 +370,6 @@ export interface MessageElement {
|
|||||||
taskTopMsgElement?: TaskTopMsgElement,
|
taskTopMsgElement?: TaskTopMsgElement,
|
||||||
recommendedMsgElement?: RecommendedMsgElement,
|
recommendedMsgElement?: RecommendedMsgElement,
|
||||||
actionBarElement?: ActionBarElement
|
actionBarElement?: ActionBarElement
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AtType {
|
export enum AtType {
|
||||||
@@ -517,6 +377,12 @@ export enum AtType {
|
|||||||
atAll = 1,
|
atAll = 1,
|
||||||
atUser = 2
|
atUser = 2
|
||||||
}
|
}
|
||||||
|
export enum MsgSourceType {
|
||||||
|
K_DOWN_SOURCETYPE_AIOINNER = 1,
|
||||||
|
K_DOWN_SOURCETYPE_BIGSCREEN = 2,
|
||||||
|
K_DOWN_SOURCETYPE_HISTORY = 3,
|
||||||
|
K_DOWN_SOURCETYPE_UNKNOWN = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 来自Android分析
|
// 来自Android分析
|
||||||
export enum ChatType {
|
export enum ChatType {
|
||||||
@@ -572,7 +438,7 @@ export interface PttElement {
|
|||||||
fileSize: string; // "4261"
|
fileSize: string; // "4261"
|
||||||
fileSubId: string; // "0"
|
fileSubId: string; // "0"
|
||||||
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
|
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
|
||||||
formatType: string; // 1
|
formatType: number; // 1
|
||||||
invalidState: number; // 0
|
invalidState: number; // 0
|
||||||
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
|
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
|
||||||
playState: number; // 0
|
playState: number; // 0
|
||||||
@@ -583,6 +449,7 @@ export interface PttElement {
|
|||||||
voiceChangeType: number; // 0
|
voiceChangeType: number; // 0
|
||||||
voiceType: number; // 0
|
voiceType: number; // 0
|
||||||
waveAmplitudes: number[];
|
waveAmplitudes: number[];
|
||||||
|
autoConvertText: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArkElement {
|
export interface ArkElement {
|
||||||
@@ -788,7 +655,8 @@ export interface InlineKeyboardElementRowButton {
|
|||||||
export interface InlineKeyboardElement {
|
export interface InlineKeyboardElement {
|
||||||
rows: [{
|
rows: [{
|
||||||
buttons: InlineKeyboardElementRowButton[]
|
buttons: InlineKeyboardElementRowButton[]
|
||||||
}];
|
}],
|
||||||
|
botAppid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
|
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
|
||||||
@@ -874,6 +742,8 @@ export interface RawMessage {
|
|||||||
/**
|
/**
|
||||||
* 扩展字段,与 Ob11 msg ID 有关
|
* 扩展字段,与 Ob11 msg ID 有关
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
guildId: string;
|
guildId: string;
|
||||||
@@ -950,6 +820,10 @@ export interface RawMessage {
|
|||||||
records: RawMessage[];
|
records: RawMessage[];
|
||||||
|
|
||||||
elements: MessageElement[];
|
elements: MessageElement[];
|
||||||
|
|
||||||
|
sourceType: MsgSourceType;
|
||||||
|
|
||||||
|
isOnlineMsg: boolean;
|
||||||
}
|
}
|
||||||
export interface QueryMsgsParams {
|
export interface QueryMsgsParams {
|
||||||
chatInfo: Peer;
|
chatInfo: Peer;
|
||||||
|
@@ -43,6 +43,50 @@ export enum GroupInviteType {
|
|||||||
BYGROUPMEMBER,
|
BYGROUPMEMBER,
|
||||||
BYDISCUSSMEMBER
|
BYDISCUSSMEMBER
|
||||||
}
|
}
|
||||||
|
export interface ShutUpGroupHonor {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShutUpGroupMember {
|
||||||
|
uid: string;
|
||||||
|
qid: string;
|
||||||
|
uin: string;
|
||||||
|
nick: string;
|
||||||
|
remark: string;
|
||||||
|
cardType: number;
|
||||||
|
cardName: string;
|
||||||
|
role: number;
|
||||||
|
avatarPath: string;
|
||||||
|
shutUpTime: number;
|
||||||
|
isDelete: boolean;
|
||||||
|
isSpecialConcerned: boolean;
|
||||||
|
isSpecialShield: boolean;
|
||||||
|
isRobot: boolean;
|
||||||
|
groupHonor: ShutUpGroupHonor;
|
||||||
|
memberRealLevel: number;
|
||||||
|
memberLevel: number;
|
||||||
|
globalGroupLevel: number;
|
||||||
|
globalGroupPoint: number;
|
||||||
|
memberTitleId: number;
|
||||||
|
memberSpecialTitle: string;
|
||||||
|
specialTitleExpireTime: string;
|
||||||
|
userShowFlag: number;
|
||||||
|
userShowFlagNew: number;
|
||||||
|
richFlag: number;
|
||||||
|
mssVipType: number;
|
||||||
|
bigClubLevel: number;
|
||||||
|
bigClubFlag: number;
|
||||||
|
autoRemark: string;
|
||||||
|
creditLevel: number;
|
||||||
|
joinTime: number;
|
||||||
|
lastSpeakTime: number;
|
||||||
|
memberFlag: number;
|
||||||
|
memberFlagExt: number;
|
||||||
|
memberMobileFlag: number;
|
||||||
|
memberFlagExt2: number;
|
||||||
|
isSpecialShielded: boolean;
|
||||||
|
cardNameId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GroupNotify {
|
export interface GroupNotify {
|
||||||
seq: string; // 通知序列号
|
seq: string; // 通知序列号
|
||||||
|
@@ -175,8 +175,8 @@ export interface SimpleInfo {
|
|||||||
status: UserStatus | null;
|
status: UserStatus | null;
|
||||||
vasInfo: VasInfo | null;
|
vasInfo: VasInfo | null;
|
||||||
relationFlags: RelationFlags | null;
|
relationFlags: RelationFlags | null;
|
||||||
otherFlags: any | null;
|
otherFlags: any;
|
||||||
intimate: any | null;
|
intimate: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FriendV2 = SimpleInfo;
|
export type FriendV2 = SimpleInfo;
|
||||||
|
38
src/core/external/appid.json
vendored
38
src/core/external/appid.json
vendored
@@ -50,5 +50,41 @@
|
|||||||
"9.9.16-28788": {
|
"9.9.16-28788": {
|
||||||
"appid": 537249739,
|
"appid": 537249739,
|
||||||
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.16-28971": {
|
||||||
|
"appid": 537249775,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.13-28971": {
|
||||||
|
"appid": 537249848,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
|
||||||
|
},
|
||||||
|
"6.9.58-28971": {
|
||||||
|
"appid": 537249826,
|
||||||
|
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.16-29271": {
|
||||||
|
"appid": 537249813,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.13-29271": {
|
||||||
|
"appid": 537249913,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
|
||||||
|
},
|
||||||
|
"6.9.59-29271": {
|
||||||
|
"appid": 537249863,
|
||||||
|
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
|
||||||
|
},
|
||||||
|
"9.9.16-29456": {
|
||||||
|
"appid": 537249875,
|
||||||
|
"qua": "V1_WIN_NQ_9.9.16_29456_GW_B"
|
||||||
|
},
|
||||||
|
"3.2.13-29456": {
|
||||||
|
"appid": 537249996,
|
||||||
|
"qua": "V1_LNX_NQ_3.2.13_29456_GW_B"
|
||||||
|
},
|
||||||
|
"6.9.59-29456": {
|
||||||
|
"appid": 537249961,
|
||||||
|
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
src/core/external/napcat.json
vendored
5
src/core/external/napcat.json
vendored
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"fileLog": true,
|
"fileLog": false,
|
||||||
"consoleLog": true,
|
"consoleLog": true,
|
||||||
"fileLogLevel": "debug",
|
"fileLogLevel": "debug",
|
||||||
"consoleLogLevel": "info",
|
"consoleLogLevel": "info",
|
||||||
|
"packetBackend": "auto",
|
||||||
"packetServer": ""
|
"packetServer": ""
|
||||||
}
|
}
|
||||||
|
66
src/core/external/offset.json
vendored
66
src/core/external/offset.json
vendored
@@ -7,6 +7,14 @@
|
|||||||
"recv": "37A9004",
|
"recv": "37A9004",
|
||||||
"send": "37A4BD0"
|
"send": "37A4BD0"
|
||||||
},
|
},
|
||||||
|
"6.9.56-28418-x64": {
|
||||||
|
"send": "4471360",
|
||||||
|
"recv": "4473BCC"
|
||||||
|
},
|
||||||
|
"6.9.56-28418-arm64": {
|
||||||
|
"send": "3FBDBF8",
|
||||||
|
"recv": "3FC0410"
|
||||||
|
},
|
||||||
"9.9.15-28498-x64": {
|
"9.9.15-28498-x64": {
|
||||||
"recv": "37A9004",
|
"recv": "37A9004",
|
||||||
"send": "37A4BD0"
|
"send": "37A4BD0"
|
||||||
@@ -18,5 +26,61 @@
|
|||||||
"3.2.13-28788-x64": {
|
"3.2.13-28788-x64": {
|
||||||
"send": "A0CEC20",
|
"send": "A0CEC20",
|
||||||
"recv": "A0D2520"
|
"recv": "A0D2520"
|
||||||
|
},
|
||||||
|
"3.2.13-28788-arm64": {
|
||||||
|
"send": "6E91018",
|
||||||
|
"recv": "6E94850"
|
||||||
|
},
|
||||||
|
"9.9.16-28971-x64": {
|
||||||
|
"send": "38079F0",
|
||||||
|
"recv": "380BE24"
|
||||||
|
},
|
||||||
|
"3.2.13-28971-x64": {
|
||||||
|
"send": "A0CEF60",
|
||||||
|
"recv": "A0D2860"
|
||||||
|
},
|
||||||
|
"3.2.12-28971-arm64": {
|
||||||
|
"send": "6E91318",
|
||||||
|
"recv": "6E94B50"
|
||||||
|
},
|
||||||
|
"6.9.58-28971-x64": {
|
||||||
|
"send": "449ACA0",
|
||||||
|
"recv": "449D50C"
|
||||||
|
},
|
||||||
|
"6.9.58-28971-arm64": {
|
||||||
|
"send": "3FE0DB0",
|
||||||
|
"recv": "3FE35C8"
|
||||||
|
},
|
||||||
|
"9.9.16-29271-x64": {
|
||||||
|
"send": "3833510",
|
||||||
|
"recv": "3837944"
|
||||||
|
},
|
||||||
|
"3.2.13-29271-x64": {
|
||||||
|
"send": "A11E680",
|
||||||
|
"recv": "A121F80"
|
||||||
|
},
|
||||||
|
"3.2.13-29271-arm64": {
|
||||||
|
"send": "6ECA098",
|
||||||
|
"recv": "6ECD8D0"
|
||||||
|
},
|
||||||
|
"9.9.16-29456-x64": {
|
||||||
|
"send": "3835CD0",
|
||||||
|
"recv": "383A104"
|
||||||
|
},
|
||||||
|
"3.2.13-29456-x64": {
|
||||||
|
"send": "A11E820",
|
||||||
|
"recv": "A122120"
|
||||||
|
},
|
||||||
|
"3.2.13-29456-arm64": {
|
||||||
|
"send": "6ECA130",
|
||||||
|
"recv": "6ECD968"
|
||||||
|
},
|
||||||
|
"6.9.59-29456-x64": {
|
||||||
|
"send": "44C57A0",
|
||||||
|
"recv": "44C800C"
|
||||||
|
},
|
||||||
|
"6.9.59-29456-arm64": {
|
||||||
|
"send": "4005FE8",
|
||||||
|
"recv": "4008800"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
src/core/external/proto/EmojiLikeToOthers.proto
vendored
31
src/core/external/proto/EmojiLikeToOthers.proto
vendored
@@ -1,31 +0,0 @@
|
|||||||
syntax = 'proto3';
|
|
||||||
package SysMessage;
|
|
||||||
|
|
||||||
message EmojiLikeToOthersWrapper1 {
|
|
||||||
EmojiLikeToOthersWrapper2 wrapper = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message EmojiLikeToOthersWrapper2 {
|
|
||||||
EmojiLikeToOthersWrapper3 body = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message EmojiLikeToOthersWrapper3 {
|
|
||||||
EmojiLikeToOthersMsgSpec msgSpec = 2;
|
|
||||||
EmojiLikeToOthersAttributes attributes = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message EmojiLikeToOthersMsgSpec {
|
|
||||||
uint32 msgSeq = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message EmojiLikeToOthersAttributes {
|
|
||||||
enum Operation {
|
|
||||||
FALLBACK = 0;
|
|
||||||
LIKE = 1;
|
|
||||||
UNLIKE = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
string emojiId = 1;
|
|
||||||
string senderUid = 4;
|
|
||||||
Operation operation = 5;
|
|
||||||
}
|
|
9
src/core/external/proto/GreyTipWrapper.proto
vendored
9
src/core/external/proto/GreyTipWrapper.proto
vendored
@@ -1,9 +0,0 @@
|
|||||||
syntax = 'proto3';
|
|
||||||
package SysMessage;
|
|
||||||
|
|
||||||
message GreyTipWrapper {
|
|
||||||
uint32 subTypeId = 1;
|
|
||||||
uint32 groupCode = 4;
|
|
||||||
uint32 subTypeIdMinusOne = 13;
|
|
||||||
bytes rest = 44;
|
|
||||||
}
|
|
18
src/core/external/proto/ProfileLikeTip.proto
vendored
18
src/core/external/proto/ProfileLikeTip.proto
vendored
@@ -1,18 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
package SysMessage;
|
|
||||||
|
|
||||||
message likeDetail {
|
|
||||||
string txt = 1;
|
|
||||||
int64 uin = 3;
|
|
||||||
string nickname = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message likeMsg {
|
|
||||||
int32 times = 1;
|
|
||||||
int32 time = 2;
|
|
||||||
likeDetail detail = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message profileLikeTip {
|
|
||||||
likeMsg msg = 14;
|
|
||||||
}
|
|
36
src/core/external/proto/SysMessage.proto
vendored
36
src/core/external/proto/SysMessage.proto
vendored
@@ -1,36 +0,0 @@
|
|||||||
syntax = 'proto3';
|
|
||||||
package SysMessage;
|
|
||||||
|
|
||||||
message SysMessage {
|
|
||||||
repeated SysMessageHeader header = 1;
|
|
||||||
repeated SysMessageMsgSpec msgSpec = 2;
|
|
||||||
SysMessageBodyWrapper bodyWrapper = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SysMessageHeader {
|
|
||||||
uint32 PeerNumber = 1;
|
|
||||||
string PeerString = 2;
|
|
||||||
uint32 Uin = 5;
|
|
||||||
optional string Uid = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SysMessageMsgSpec {
|
|
||||||
uint32 msgType = 1;
|
|
||||||
uint32 subType = 2;
|
|
||||||
uint32 subSubType = 3;
|
|
||||||
uint32 msgSeq = 5;
|
|
||||||
uint32 time = 6;
|
|
||||||
uint64 msgId = 12;
|
|
||||||
uint32 other = 13;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SysMessageBodyWrapper {
|
|
||||||
bytes wrappedBody = 2;
|
|
||||||
// Find the first [08], or ignore the first 7 bytes?
|
|
||||||
// And it becomes another ProtoBuf message.
|
|
||||||
}
|
|
||||||
|
|
||||||
message KeyValuePair {
|
|
||||||
string key = 1;
|
|
||||||
string value = 2;
|
|
||||||
}
|
|
61
src/core/helper/adaptDecoder.ts
Normal file
61
src/core/helper/adaptDecoder.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// TODO: further refactor in NapCat.Packet v2
|
||||||
|
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||||
|
|
||||||
|
const LikeDetail = {
|
||||||
|
txt: ProtoField(1, ScalarType.STRING),
|
||||||
|
uin: ProtoField(3, ScalarType.INT64),
|
||||||
|
nickname: ProtoField(5, ScalarType.STRING)
|
||||||
|
};
|
||||||
|
|
||||||
|
const LikeMsg = {
|
||||||
|
times: ProtoField(1, ScalarType.INT32),
|
||||||
|
time: ProtoField(2, ScalarType.INT32),
|
||||||
|
detail: ProtoField(3, () => LikeDetail)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileLikeSubTip = {
|
||||||
|
msg: ProtoField(14, () => LikeMsg)
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProfileLikeTip = {
|
||||||
|
msgType: ProtoField(1, ScalarType.INT32),
|
||||||
|
subType: ProtoField(2, ScalarType.INT32),
|
||||||
|
content: ProtoField(203, () => ProfileLikeSubTip)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SysMessageHeader = {
|
||||||
|
PeerNumber: ProtoField(1, ScalarType.UINT32),
|
||||||
|
PeerString: ProtoField(2, ScalarType.STRING),
|
||||||
|
Uin: ProtoField(5, ScalarType.UINT32),
|
||||||
|
Uid: ProtoField(6, ScalarType.STRING, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SysMessageMsgSpec = {
|
||||||
|
msgType: ProtoField(1, ScalarType.UINT32),
|
||||||
|
subType: ProtoField(2, ScalarType.UINT32),
|
||||||
|
subSubType: ProtoField(3, ScalarType.UINT32),
|
||||||
|
msgSeq: ProtoField(5, ScalarType.UINT32),
|
||||||
|
time: ProtoField(6, ScalarType.UINT32),
|
||||||
|
msgId: ProtoField(12, ScalarType.UINT64),
|
||||||
|
other: ProtoField(13, ScalarType.UINT32)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SysMessageBodyWrapper = {
|
||||||
|
wrappedBody: ProtoField(2, ScalarType.BYTES)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SysMessage = {
|
||||||
|
header: ProtoField(1, () => SysMessageHeader, false, true),
|
||||||
|
msgSpec: ProtoField(2, () => SysMessageMsgSpec, false, true),
|
||||||
|
bodyWrapper: ProtoField(3, () => SysMessageBodyWrapper)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function decodeProfileLikeTip(buffer: Uint8Array) {
|
||||||
|
const msg = new NapProtoMsg(ProfileLikeTip);
|
||||||
|
return msg.decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeSysMessage(buffer: Uint8Array) {
|
||||||
|
const msg = new NapProtoMsg(SysMessage);
|
||||||
|
return msg.decode(buffer);
|
||||||
|
}
|
49
src/core/helper/adaptSysMessageDecoder.ts
Normal file
49
src/core/helper/adaptSysMessageDecoder.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// TODO: further refactor in NapCat.Packet v2
|
||||||
|
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||||
|
|
||||||
|
const BodyInner = {
|
||||||
|
msgType: ProtoField(1, ScalarType.UINT32, true),
|
||||||
|
subType: ProtoField(2, ScalarType.UINT32, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoifyData = {
|
||||||
|
skip: ProtoField(1, ScalarType.BYTES, true),
|
||||||
|
innerData: ProtoField(2, ScalarType.BYTES, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const MsgHead = {
|
||||||
|
bodyInner: ProtoField(2, () => BodyInner, true),
|
||||||
|
noifyData: ProtoField(3, () => NoifyData, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
const Message = {
|
||||||
|
msgHead: ProtoField(1, () => MsgHead)
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubDetail = {
|
||||||
|
msgSeq: ProtoField(1, ScalarType.UINT32),
|
||||||
|
msgTime: ProtoField(2, ScalarType.UINT32),
|
||||||
|
senderUid: ProtoField(6, ScalarType.STRING)
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecallDetails = {
|
||||||
|
operatorUid: ProtoField(1, ScalarType.STRING),
|
||||||
|
subDetail: ProtoField(3, () => SubDetail)
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecallGroup = {
|
||||||
|
type: ProtoField(1, ScalarType.INT32),
|
||||||
|
peerUid: ProtoField(4, ScalarType.UINT32),
|
||||||
|
recallDetails: ProtoField(11, () => RecallDetails),
|
||||||
|
grayTipsSeq: ProtoField(37, ScalarType.UINT32)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function decodeMessage(buffer: Uint8Array) {
|
||||||
|
const msg = new NapProtoMsg(Message);
|
||||||
|
return msg.decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodeRecallGroup(buffer: Uint8Array){
|
||||||
|
const msg = new NapProtoMsg(RecallGroup);
|
||||||
|
return msg.decode(buffer);
|
||||||
|
}
|
@@ -62,14 +62,32 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
|
|||||||
process.dlopen(nativemodule, wrapperNodePath);
|
process.dlopen(nativemodule, wrapperNodePath);
|
||||||
return nativemodule.exports;
|
return nativemodule.exports;
|
||||||
}
|
}
|
||||||
|
export function getMajorPath(QQVersion: string): string {
|
||||||
|
// major.node
|
||||||
|
let appPath;
|
||||||
|
if (os.platform() === 'darwin') {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
|
||||||
|
} else if (os.platform() === 'linux') {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
|
||||||
|
} else {
|
||||||
|
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
|
||||||
|
}
|
||||||
|
let majorPath = path.resolve(appPath, 'major.node');
|
||||||
|
if (!fs.existsSync(majorPath)) {
|
||||||
|
majorPath = path.join(appPath, `./resources/app/major.node`);
|
||||||
|
}
|
||||||
|
//老版本兼容 未来去掉
|
||||||
|
if (!fs.existsSync(majorPath)) {
|
||||||
|
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
|
||||||
|
}
|
||||||
|
return majorPath;
|
||||||
|
}
|
||||||
export class NapCatCore {
|
export class NapCatCore {
|
||||||
readonly context: InstanceContext;
|
readonly context: InstanceContext;
|
||||||
readonly apis: StableNTApiWrapper;
|
|
||||||
readonly eventWrapper: NTEventWrapper;
|
readonly eventWrapper: NTEventWrapper;
|
||||||
// readonly eventChannel: NTEventChannel;
|
NapCatDataPath: string = '';
|
||||||
NapCatDataPath: string;
|
NapCatTempPath: string = '';
|
||||||
NapCatTempPath: string;
|
apis: StableNTApiWrapper;
|
||||||
// runtime info, not readonly
|
// runtime info, not readonly
|
||||||
selfInfo: SelfInfo;
|
selfInfo: SelfInfo;
|
||||||
util: NodeQQNTWrapperUtil;
|
util: NodeQQNTWrapperUtil;
|
||||||
@@ -93,6 +111,8 @@ export class NapCatCore {
|
|||||||
UserApi: new NTQQUserApi(this.context, this),
|
UserApi: new NTQQUserApi(this.context, this),
|
||||||
GroupApi: new NTQQGroupApi(this.context, this),
|
GroupApi: new NTQQGroupApi(this.context, this),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
async initCore() {
|
||||||
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
|
||||||
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
|
||||||
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
|
||||||
@@ -100,7 +120,13 @@ export class NapCatCore {
|
|||||||
if (!fs.existsSync(this.NapCatTempPath)) {
|
if (!fs.existsSync(this.NapCatTempPath)) {
|
||||||
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
//遍历this.apis[i].initApi 如果存在该函数进行async 调用
|
||||||
|
for (const apiKey in this.apis) {
|
||||||
|
const api = this.apis[apiKey as keyof StableNTApiWrapper];
|
||||||
|
if ('initApi' in api && typeof api.initApi === 'function') {
|
||||||
|
await api.initApi();
|
||||||
|
}
|
||||||
|
}
|
||||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||||
|
|
||||||
this.context.logger.setFileLogEnabled(
|
this.context.logger.setFileLogEnabled(
|
||||||
@@ -114,7 +140,6 @@ export class NapCatCore {
|
|||||||
this.configLoader.configData.consoleLogLevel as LogLevel,
|
this.configLoader.configData.consoleLogLevel as LogLevel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataPath(): string {
|
get dataPath(): string {
|
||||||
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -140,7 +165,7 @@ export class NapCatCore {
|
|||||||
};
|
};
|
||||||
//await sleep(2500);
|
//await sleep(2500);
|
||||||
this.context.session.getMsgService().addKernelMsgListener(
|
this.context.session.getMsgService().addKernelMsgListener(
|
||||||
proxiedListenerOf(msgListener, this.context.logger) as any,
|
proxiedListenerOf(msgListener, this.context.logger),
|
||||||
);
|
);
|
||||||
|
|
||||||
const profileListener = new NodeIKernelProfileListener();
|
const profileListener = new NodeIKernelProfileListener();
|
||||||
@@ -185,7 +210,7 @@ export class NapCatCore {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
groupListener.onMemberListChange = (arg) => {
|
groupListener.onMemberListChange = (arg) => {
|
||||||
// todo: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
// TODO: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
||||||
const groupCode = arg.sceneId.split('_')[0];
|
const groupCode = arg.sceneId.split('_')[0];
|
||||||
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
|
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
|
||||||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
|
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
|
||||||
@@ -195,7 +220,7 @@ export class NapCatCore {
|
|||||||
if (existMember) {
|
if (existMember) {
|
||||||
Object.assign(existMember, member);
|
Object.assign(existMember, member);
|
||||||
} else {
|
} else {
|
||||||
existMembers!.set(uid, member);
|
existMembers.set(uid, member);
|
||||||
}
|
}
|
||||||
//移除成员
|
//移除成员
|
||||||
if (member.isDelete) {
|
if (member.isDelete) {
|
||||||
@@ -236,7 +261,7 @@ export class NapCatCore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.context.session.getGroupService().addKernelGroupListener(
|
this.context.session.getGroupService().addKernelGroupListener(
|
||||||
proxiedListenerOf(groupListener, this.context.logger) as any,
|
proxiedListenerOf(groupListener, this.context.logger),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +301,7 @@ export async function genSessionConfig(
|
|||||||
d2: '',
|
d2: '',
|
||||||
d2Key: '',
|
d2Key: '',
|
||||||
machineId: '',
|
machineId: '',
|
||||||
platform: systemPlatform, // 3是Windows?
|
platform: systemPlatform, // 3是Windows?
|
||||||
platVer: systemVersion, // 系统版本号, 应该可以固定
|
platVer: systemVersion, // 系统版本号, 应该可以固定
|
||||||
appid: QQVersionAppid,
|
appid: QQVersionAppid,
|
||||||
rdeliveryConfig: {
|
rdeliveryConfig: {
|
||||||
|
@@ -3,57 +3,57 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/entities';
|
|||||||
export type OnBuddyChangeParams = BuddyCategoryType[];
|
export type OnBuddyChangeParams = BuddyCategoryType[];
|
||||||
|
|
||||||
export class NodeIKernelBuddyListener {
|
export class NodeIKernelBuddyListener {
|
||||||
onBuddyListChangedV2(arg: unknown): void {
|
onBuddyListChangedV2(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddBuddyNeedVerify(arg: unknown) {
|
onAddBuddyNeedVerify(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddMeSettingChanged(arg: unknown) {
|
onAddMeSettingChanged(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onAvatarUrlUpdated(arg: unknown) {
|
onAvatarUrlUpdated(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlockChanged(arg: unknown) {
|
onBlockChanged(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyDetailInfoChange(arg: unknown) {
|
onBuddyDetailInfoChange(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyInfoChange(arg: unknown) {
|
onBuddyInfoChange(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyListChange(arg: OnBuddyChangeParams): void {
|
onBuddyListChange(arg: OnBuddyChangeParams): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyRemarkUpdated(arg: unknown): void {
|
onBuddyRemarkUpdated(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyReqChange(arg: FriendRequestNotify): void {
|
onBuddyReqChange(arg: FriendRequestNotify): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBuddyReqUnreadCntChange(arg: unknown): void {
|
onBuddyReqUnreadCntChange(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCheckBuddySettingResult(arg: unknown): void {
|
onCheckBuddySettingResult(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDelBatchBuddyInfos(arg: unknown): void {
|
onDelBatchBuddyInfos(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubtBuddyReqChange(arg: unknown): void {
|
onDoubtBuddyReqChange(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubtBuddyReqUnreadNumChange(arg: unknown): void {
|
onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onNickUpdated(arg: unknown): void {
|
onNickUpdated(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSmartInfos(arg: unknown): void {
|
onSmartInfos(arg: unknown): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpacePermissionInfos(arg: unknown): void {
|
onSpacePermissionInfos(arg: unknown): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,19 +7,19 @@ export class NodeIKernelFileAssistantListener {
|
|||||||
fileSpeed: number,
|
fileSpeed: number,
|
||||||
thumbPath: string | null,
|
thumbPath: string | null,
|
||||||
filePath: string | null,
|
filePath: string | null,
|
||||||
}) {
|
}): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSessionListChanged(...args: unknown[]) {
|
onSessionListChanged(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSessionChanged(...args: unknown[]) {
|
onSessionChanged(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileListChanged(...args: unknown[]) {
|
onFileListChanged(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileSearch(searchResult: SearchResultWrapper) {
|
onFileSearch(searchResult: SearchResultWrapper): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,84 +1,85 @@
|
|||||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
|
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
|
||||||
|
|
||||||
export class NodeIKernelGroupListener {
|
export class NodeIKernelGroupListener {
|
||||||
onGroupListInited(listEmpty: boolean): void { }
|
onGroupListInited(listEmpty: boolean): any { }
|
||||||
// 发现于Win 9.9.9 23159
|
// 发现于Win 9.9.9 23159
|
||||||
onGroupMemberLevelInfoChange(...args: unknown[]): void {
|
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGetGroupBulletinListResult(...args: unknown[]) {
|
onGetGroupBulletinListResult(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupAllInfoChange(...args: unknown[]) {
|
onGroupAllInfoChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinChange(...args: unknown[]) {
|
onGroupBulletinChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinRemindNotify(...args: unknown[]) {
|
onGroupBulletinRemindNotify(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupArkInviteStateResult(...args: unknown[]) {
|
onGroupArkInviteStateResult(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
|
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupConfMemberChange(...args: unknown[]) {
|
onGroupConfMemberChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupDetailInfoChange(...args: unknown[]) {
|
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupExtListUpdate(...args: unknown[]) {
|
onGroupExtListUpdate(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupFirstBulletinNotify(...args: unknown[]) {
|
onGroupFirstBulletinNotify(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
|
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
|
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
|
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
|
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
|
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupsMsgMaskResult(...args: unknown[]) {
|
onGroupsMsgMaskResult(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupStatisticInfoChange(...args: unknown[]) {
|
onGroupStatisticInfoChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onJoinGroupNotify(...args: unknown[]) {
|
onJoinGroupNotify(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onJoinGroupNoVerifyFlag(...args: unknown[]) {
|
onJoinGroupNoVerifyFlag(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>) {
|
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberListChange(arg: {
|
onMemberListChange(arg: {
|
||||||
sceneId: string,
|
sceneId: string,
|
||||||
ids: string[],
|
ids: string[],
|
||||||
infos: Map<string, GroupMember>, // uid -> GroupMember
|
infos: Map<string, GroupMember>, // uid -> GroupMember
|
||||||
finish: boolean,
|
hasPrev: boolean,
|
||||||
|
hasNext: boolean,
|
||||||
hasRobot: boolean
|
hasRobot: boolean
|
||||||
}) {
|
}): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchMemberChange(...args: unknown[]) {
|
onSearchMemberChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShutUpMemberListChanged(...args: unknown[]) {
|
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,57 +1,57 @@
|
|||||||
export class NodeIKernelLoginListener {
|
export class NodeIKernelLoginListener {
|
||||||
onLoginConnected(...args: any[]): void {
|
onLoginConnected(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginDisConnected(...args: any[]): void {
|
onLoginDisConnected(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginConnecting(...args: any[]): void {
|
onLoginConnecting(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): void {
|
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): any {
|
||||||
// let base64Data: string = arg.pngBase64QrcodeData
|
// let base64Data: string = arg.pngBase64QrcodeData
|
||||||
// base64Data = base64Data.split("data:image/png;base64,")[1]
|
// base64Data = base64Data.split("data:image/png;base64,")[1]
|
||||||
// let buffer = Buffer.from(base64Data, 'base64')
|
// let buffer = Buffer.from(base64Data, 'base64')
|
||||||
// console.log("onQRCodeGetPicture", arg);
|
// console.log("onQRCodeGetPicture", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeLoginPollingStarted(...args: any[]): void {
|
onQRCodeLoginPollingStarted(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeSessionUserScaned(...args: any[]): void {
|
onQRCodeSessionUserScaned(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): void {
|
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeSessionFailed(...args: any[]): void {
|
onQRCodeSessionFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginFailed(...args: any[]): void {
|
onLoginFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogoutSucceed(...args: any[]): void {
|
onLogoutSucceed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogoutFailed(...args: any[]): void {
|
onLogoutFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserLoggedIn(...args: any[]): void {
|
onUserLoggedIn(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQRCodeSessionQuickLoginFailed(...args: any[]): void {
|
onQRCodeSessionQuickLoginFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordLoginFailed(...args: any[]): void {
|
onPasswordLoginFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
OnConfirmUnusualDeviceFailed(...args: any[]): void {
|
OnConfirmUnusualDeviceFailed(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onQQLoginNumLimited(...args: any[]): void {
|
onQQLoginNumLimited(...args: any[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginState(...args: any[]): void {
|
onLoginState(...args: any[]): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ChatType, RawMessage } from '@/core/entities';
|
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities';
|
||||||
import { CommonFileInfo } from '@/core';
|
import { CommonFileInfo } from '@/core';
|
||||||
|
|
||||||
export interface OnRichMediaDownloadCompleteParams {
|
export interface OnRichMediaDownloadCompleteParams {
|
||||||
@@ -20,8 +20,8 @@ export interface OnRichMediaDownloadCompleteParams {
|
|||||||
fileSrvErrCode: string,
|
fileSrvErrCode: string,
|
||||||
clientMsg: string,
|
clientMsg: string,
|
||||||
businessId: number,
|
businessId: number,
|
||||||
userTotalSpacePerDay: unknown | null,
|
userTotalSpacePerDay: unknown,
|
||||||
userUsedSpacePerDay: unknown | null
|
userUsedSpacePerDay: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupFileInfoUpdateParamType {
|
export interface GroupFileInfoUpdateParamType {
|
||||||
@@ -29,50 +29,10 @@ export interface GroupFileInfoUpdateParamType {
|
|||||||
retMsg: string;
|
retMsg: string;
|
||||||
clientWording: string;
|
clientWording: string;
|
||||||
isEnd: boolean;
|
isEnd: boolean;
|
||||||
item: Array<{
|
item: Array<GroupFileInfoUpdateItem>;
|
||||||
peerId: string;
|
allFileCount: number;
|
||||||
type: number;
|
nextIndex: number;
|
||||||
folderInfo?: {
|
reqId: number;
|
||||||
folderId: string;
|
|
||||||
parentFolderId: string;
|
|
||||||
folderName: string;
|
|
||||||
createTime: number;
|
|
||||||
modifyTime: number;
|
|
||||||
createUin: string;
|
|
||||||
creatorName: string;
|
|
||||||
totalFileCount: number;
|
|
||||||
modifyUin: string;
|
|
||||||
modifyName: string;
|
|
||||||
usedSpace: string;
|
|
||||||
},
|
|
||||||
fileInfo?: {
|
|
||||||
fileModelId: string;
|
|
||||||
fileId: string;
|
|
||||||
fileName: string;
|
|
||||||
fileSize: string;
|
|
||||||
busId: number;
|
|
||||||
uploadedSize: string;
|
|
||||||
uploadTime: number;
|
|
||||||
deadTime: number;
|
|
||||||
modifyTime: number;
|
|
||||||
downloadTimes: number;
|
|
||||||
sha: string;
|
|
||||||
sha3: string;
|
|
||||||
md5: string;
|
|
||||||
uploaderLocalPath: string;
|
|
||||||
uploaderName: string;
|
|
||||||
uploaderUin: string;
|
|
||||||
parentFolderId: string;
|
|
||||||
localPath: string;
|
|
||||||
transStatus: number;
|
|
||||||
transType: number;
|
|
||||||
elementId: string;
|
|
||||||
isFolder: boolean;
|
|
||||||
},
|
|
||||||
}>;
|
|
||||||
allFileCount: string;
|
|
||||||
nextIndex: string;
|
|
||||||
reqId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
@@ -83,6 +43,49 @@ export interface GroupFileInfoUpdateParamType {
|
|||||||
// fromNick: '拾xxxx,
|
// fromNick: '拾xxxx,
|
||||||
// sig: '0x'
|
// sig: '0x'
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
export interface GroupFileInfoUpdateItem {
|
||||||
|
peerId: string;
|
||||||
|
type: number;
|
||||||
|
folderInfo?: {
|
||||||
|
folderId: string;
|
||||||
|
parentFolderId: string;
|
||||||
|
folderName: string;
|
||||||
|
createTime: number;
|
||||||
|
modifyTime: number;
|
||||||
|
createUin: string;
|
||||||
|
creatorName: string;
|
||||||
|
totalFileCount: number;
|
||||||
|
modifyUin: string;
|
||||||
|
modifyName: string;
|
||||||
|
usedSpace: string;
|
||||||
|
},
|
||||||
|
fileInfo?: {
|
||||||
|
fileModelId: string;
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: string;
|
||||||
|
busId: number;
|
||||||
|
uploadedSize: string;
|
||||||
|
uploadTime: number;
|
||||||
|
deadTime: number;
|
||||||
|
modifyTime: number;
|
||||||
|
downloadTimes: number;
|
||||||
|
sha: string;
|
||||||
|
sha3: string;
|
||||||
|
md5: string;
|
||||||
|
uploaderLocalPath: string;
|
||||||
|
uploaderName: string;
|
||||||
|
uploaderUin: string;
|
||||||
|
parentFolderId: string;
|
||||||
|
localPath: string;
|
||||||
|
transStatus: number;
|
||||||
|
transType: number;
|
||||||
|
elementId: string;
|
||||||
|
isFolder: boolean;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export interface TempOnRecvParams {
|
export interface TempOnRecvParams {
|
||||||
sessionType: number,//1
|
sessionType: number,//1
|
||||||
chatType: ChatType,//100
|
chatType: ChatType,//100
|
||||||
@@ -94,108 +97,108 @@ export interface TempOnRecvParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NodeIKernelMsgListener {
|
export class NodeIKernelMsgListener {
|
||||||
onAddSendMsg(msgRecord: RawMessage) {
|
onAddSendMsg(msgRecord: RawMessage): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
|
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
|
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
|
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onContactUnreadCntUpdate(hashMap: unknown) {
|
onContactUnreadCntUpdate(hashMap: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
|
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
|
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
|
onEmojiDownloadComplete(emojiNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
|
onEmojiResourceUpdate(emojiResourceInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileMsgCome(arrayList: unknown) {
|
onFileMsgCome(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFirstViewGroupGuildMapping(arrayList: unknown) {
|
onFirstViewGroupGuildMapping(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
|
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupFileInfoAdd(groupItem: unknown) {
|
onGroupFileInfoAdd(groupItem: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType) {
|
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
|
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onGroupTransferInfoAdd(groupItem: unknown) {
|
onGroupTransferInfoAdd(groupItem: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
|
onGroupTransferInfoUpdate(groupFileListResult: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
|
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
|
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
|
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
|
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
|
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
|
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
|
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,176 +211,176 @@ export class NodeIKernelMsgListener {
|
|||||||
statusText: string;
|
statusText: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
toUin: string;
|
toUin: string;
|
||||||
}) {
|
}): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onKickedOffLine(kickedInfo: unknown) {
|
onKickedOffLine(kickedInfo: KickedOffLineInfo): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLineDev(arrayList: unknown) {
|
onLineDev(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogLevelChanged(j2: unknown) {
|
onLogLevelChanged(j2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgAbstractUpdate(arrayList: unknown) {
|
onMsgAbstractUpdate(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgBoxChanged(arrayList: unknown) {
|
onMsgBoxChanged(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgDelete(contact: unknown, arrayList: unknown) {
|
onMsgDelete(contact: unknown, arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgEventListUpdate(hashMap: unknown) {
|
onMsgEventListUpdate(hashMap: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgInfoListAdd(arrayList: unknown) {
|
onMsgInfoListAdd(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgInfoListUpdate(msgList: RawMessage[]) {
|
onMsgInfoListUpdate(msgList: RawMessage[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgQRCodeStatusChanged(i2: unknown) {
|
onMsgQRCodeStatusChanged(i2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
|
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgSecurityNotify(msgRecord: unknown) {
|
onMsgSecurityNotify(msgRecord: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgSettingUpdate(msgSetting: unknown) {
|
onMsgSettingUpdate(msgSetting: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNtFirstViewMsgSyncEnd() {
|
onNtFirstViewMsgSyncEnd(): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNtMsgSyncEnd() {
|
onNtMsgSyncEnd(): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onNtMsgSyncStart() {
|
onNtMsgSyncStart(): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
|
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvGroupGuildFlag(i2: unknown) {
|
onRecvGroupGuildFlag(i2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvMsg(arrayList: RawMessage[]) {
|
onRecvMsg(arrayList: RawMessage[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
|
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvOnlineFileMsg(arrayList: unknown) {
|
onRecvOnlineFileMsg(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvS2CMsg(arrayList: unknown) {
|
onRecvS2CMsg(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvSysMsg(arrayList: Array<number>) {
|
onRecvSysMsg(arrayList: Array<number>): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecvUDCFlag(i2: unknown) {
|
onRecvUDCFlag(i2: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
|
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
|
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
|
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
|
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
|
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
|
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
|
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnreadCntAfterFirstView(hashMap: unknown) {
|
onUnreadCntAfterFirstView(hashMap: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnreadCntUpdate(hashMap: unknown) {
|
onUnreadCntUpdate(hashMap: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserChannelTabStatusChanged(z: unknown) {
|
onUserChannelTabStatusChanged(z: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserOnlineStatusChanged(z: unknown) {
|
onUserOnlineStatusChanged(z: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserTabStatusChanged(arrayList: unknown) {
|
onUserTabStatusChanged(arrayList: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
|
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
|
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一次发现于Linux
|
// 第一次发现于Linux
|
||||||
onUserSecQualityChanged(...args: unknown[]) {
|
onUserSecQualityChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
|
onMsgWithRichLinkInfoUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRedTouchChanged(...args: unknown[]) {
|
onRedTouchChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第一次发现于Win 9.9.9-23159
|
// 第一次发现于Win 9.9.9-23159
|
||||||
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
|
onBroadcastHelperProgerssUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,67 +5,67 @@ export class NodeIKernelProfileListener {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileSimpleChanged(...args: unknown[]) {
|
onProfileSimpleChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onProfileDetailInfoChanged(profile: User) {
|
onProfileDetailInfoChanged(profile: User): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStatusUpdate(...args: unknown[]) {
|
onStatusUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelfStatusChanged(...args: unknown[]) {
|
onSelfStatusChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStrangerRemarkChanged(...args: unknown[]) {
|
onStrangerRemarkChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberListChange(...args: unknown[]) {
|
onMemberListChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMemberInfoChange(...args: unknown[]) {
|
onMemberInfoChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupListUpdate(...args: unknown[]) {
|
onGroupListUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupAllInfoChange(...args: unknown[]) {
|
onGroupAllInfoChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupDetailInfoChange(...args: unknown[]) {
|
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupConfMemberChange(...args: unknown[]) {
|
onGroupConfMemberChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupExtListUpdate(...args: unknown[]) {
|
onGroupExtListUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUpdated(...args: unknown[]) {
|
onGroupNotifiesUpdated(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
|
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupMemberLevelInfoChange(...args: unknown[]) {
|
onGroupMemberLevelInfoChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupBulletinChange(...args: unknown[]) {
|
onGroupBulletinChange(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
export class NodeIKernelRecentContactListener {
|
export class NodeIKernelRecentContactListener {
|
||||||
onDeletedContactsNotify(...args: unknown[]) {
|
onDeletedContactsNotify(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number) {
|
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgUnreadCountUpdate(...args: unknown[]) {
|
onMsgUnreadCountUpdate(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGuildDisplayRecentContactListChanged(...args: unknown[]) {
|
onGuildDisplayRecentContactListChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecentContactListChanged(...args: unknown[]) {
|
onRecentContactListChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRecentContactListChangedVer2(...args: unknown[]) {
|
onRecentContactListChangedVer2(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
export class NodeIKernelRobotListener {
|
export class NodeIKernelRobotListener {
|
||||||
onRobotFriendListChanged(...args: unknown[]) {
|
onRobotFriendListChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRobotListChanged(...args: unknown[]) {
|
onRobotListChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRobotProfileChanged(...args: unknown[]) {
|
onRobotProfileChanged(...args: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@ export interface GroupSearchResult {
|
|||||||
}
|
}
|
||||||
export interface NodeIKernelSearchListener {
|
export interface NodeIKernelSearchListener {
|
||||||
|
|
||||||
onSearchGroupResult(params: GroupSearchResult): void;
|
onSearchGroupResult(params: GroupSearchResult): any;
|
||||||
|
|
||||||
onSearchFileKeywordsResult(params: {
|
onSearchFileKeywordsResult(params: {
|
||||||
searchId: string,
|
searchId: string,
|
||||||
@@ -93,5 +93,5 @@ export interface NodeIKernelSearchListener {
|
|||||||
end: number
|
end: number
|
||||||
}[]
|
}[]
|
||||||
}[]
|
}[]
|
||||||
}): void;
|
}): any;
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
export class NodeIKernelSessionListener {
|
export class NodeIKernelSessionListener {
|
||||||
onNTSessionCreate(args: unknown) {
|
onNTSessionCreate(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGProSessionCreate(args: unknown) {
|
onGProSessionCreate(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSessionInitComplete(args: unknown) {
|
onSessionInitComplete(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpentelemetryInit(args: unknown) {
|
onOpentelemetryInit(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserOnlineResult(args: unknown) {
|
onUserOnlineResult(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGetSelfTinyId(args: unknown) {
|
onGetSelfTinyId(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
export class NodeIKernelStorageCleanListener {
|
export class NodeIKernelStorageCleanListener {
|
||||||
onCleanCacheProgressChanged(args: unknown) {
|
onCleanCacheProgressChanged(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScanCacheProgressChanged(args: unknown) {
|
onScanCacheProgressChanged(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanCacheStorageChanged(args: unknown) {
|
onCleanCacheStorageChanged(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onFinishScan(args: unknown) {
|
onFinishScan(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onChatCleanDone(args: unknown) {
|
onChatCleanDone(args: unknown): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +1,5 @@
|
|||||||
export class NodeIKernelTicketListener {
|
export class NodeIKernelTicketListener {
|
||||||
|
listener(): any {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
export class NodeIO3MiscListener {
|
export class NodeIO3MiscListener {
|
||||||
getOnAmgomDataPiece(...arg: unknown[]) {
|
getOnAmgomDataPiece(...arg: unknown[]): any {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,179 +0,0 @@
|
|||||||
import { LogWrapper } from "@/common/log";
|
|
||||||
import { LRUCache } from "@/common/lru-cache";
|
|
||||||
import WebSocket, { Data } from "ws";
|
|
||||||
import crypto, { createHash } from "crypto";
|
|
||||||
import { NapCatCore } from "@/core";
|
|
||||||
import { PacketHexStr } from "@/core/packet/packer";
|
|
||||||
import { sleep } from "@/common/helper";
|
|
||||||
|
|
||||||
export interface RecvPacket {
|
|
||||||
type: string, // 仅recv
|
|
||||||
trace_id_md5?: string,
|
|
||||||
data: RecvPacketData
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecvPacketData {
|
|
||||||
seq: number
|
|
||||||
cmd: string
|
|
||||||
hex_data: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PacketClient {
|
|
||||||
private websocket: WebSocket | undefined;
|
|
||||||
private isConnected: boolean = false;
|
|
||||||
private reconnectAttempts: number = 0;
|
|
||||||
private readonly maxReconnectAttempts: number = 5;//现在暂时不可配置
|
|
||||||
private readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
|
||||||
private readonly clientUrl: string = '';
|
|
||||||
readonly napCatCore: NapCatCore;
|
|
||||||
private readonly logger: LogWrapper;
|
|
||||||
|
|
||||||
constructor(url: string, core: NapCatCore) {
|
|
||||||
this.clientUrl = url;
|
|
||||||
this.napCatCore = core;
|
|
||||||
this.logger = core.context.logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
get available(): boolean {
|
|
||||||
return this.isConnected && this.websocket !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private randText(len: number) {
|
|
||||||
let text = '';
|
|
||||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
|
||||||
this.websocket = new WebSocket(this.clientUrl);
|
|
||||||
this.websocket.on('error', (err) => {}/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
|
||||||
this.isConnected = true;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.logger.log.bind(this.logger)(`[Core] [Packet Server] Connected to ${this.clientUrl}`);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onerror = (error) => {
|
|
||||||
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
|
||||||
reject(new Error(`${error.message}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onmessage = (event) => {
|
|
||||||
// const message = JSON.parse(event.data.toString());
|
|
||||||
// console.log("Received message:", message);
|
|
||||||
this.handleMessage(event.data).then().catch();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onclose = () => {
|
|
||||||
this.isConnected = false;
|
|
||||||
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
|
||||||
this.attemptReconnect();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private attemptReconnect(): void {
|
|
||||||
try {
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.connect().catch((error) => {
|
|
||||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Reconnecting attempt failed,${error.message}`);
|
|
||||||
});
|
|
||||||
}, 5000 * this.reconnectAttempts);
|
|
||||||
} else {
|
|
||||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] Max reconnect attempts reached. ${this.clientUrl}`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.logError.bind(this.logger)(`Error attempting to reconnect: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
|
||||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
|
||||||
if (!this.isConnected || !this.websocket) {
|
|
||||||
throw new Error("WebSocket is not connected");
|
|
||||||
}
|
|
||||||
const initMessage = {
|
|
||||||
action: 'init',
|
|
||||||
pid: pid,
|
|
||||||
recv: recv,
|
|
||||||
send: send
|
|
||||||
};
|
|
||||||
this.websocket.send(JSON.stringify(initMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
|
||||||
}): Promise<RecvPacketData> {
|
|
||||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
|
||||||
if (!this.isConnected || !this.websocket) {
|
|
||||||
throw new Error("WebSocket is not connected");
|
|
||||||
}
|
|
||||||
const commandMessage = {
|
|
||||||
action: 'send',
|
|
||||||
cmd: cmd,
|
|
||||||
data: data,
|
|
||||||
trace_id: trace_id
|
|
||||||
};
|
|
||||||
this.websocket.send(JSON.stringify(commandMessage));
|
|
||||||
if (rsp) {
|
|
||||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
resolve(json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
|
||||||
sendcb(json);
|
|
||||||
if (!rsp) {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
resolve(json);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const timeoutHandle = setTimeout(() => {
|
|
||||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
|
||||||
}, timeout);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleMessage(message: Data): Promise<void> {
|
|
||||||
try {
|
|
||||||
const json: RecvPacket = JSON.parse(message.toString());
|
|
||||||
const trace_id_md5 = json.trace_id_md5;
|
|
||||||
const action = json?.type ?? 'init';
|
|
||||||
const event = this.cb.get(trace_id_md5 + action);
|
|
||||||
if (event) {
|
|
||||||
await event(json.data);
|
|
||||||
}
|
|
||||||
//console.log("Received message:", json);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
|
||||||
// wtfk tx
|
|
||||||
// 校验失败和异常 可能返回undefined
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.available) {
|
|
||||||
this.logger.logError('NapCat.Packet is not init');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
|
||||||
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
|
|
||||||
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
|
||||||
// await sleep(10);
|
|
||||||
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
|
|
||||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user