mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
440 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
901828f5a6 | ||
![]() |
2a4b0cbc09 | ||
![]() |
c5434efd56 | ||
![]() |
b73f283095 | ||
![]() |
24ef54f01c | ||
![]() |
bff3b85337 | ||
![]() |
811d9a7237 | ||
![]() |
a764cb8dc2 | ||
![]() |
9204b9b286 | ||
![]() |
da94faa9bb | ||
![]() |
4b53e9a895 | ||
![]() |
f5db96187b | ||
![]() |
857b191b03 | ||
![]() |
09014d1ab5 | ||
![]() |
7557b71869 | ||
![]() |
9c4751794f | ||
![]() |
d07187bd5d | ||
![]() |
2c6a6ba440 | ||
![]() |
4592bf7817 | ||
![]() |
afd6d450a0 | ||
![]() |
b134849dcf | ||
![]() |
e7d0f6d6da | ||
![]() |
16a29b0127 | ||
![]() |
1f5596ef16 | ||
![]() |
bef05432d0 | ||
![]() |
67533d7743 | ||
![]() |
0cc86c6348 | ||
![]() |
607dd68620 | ||
![]() |
7c8cbc0799 | ||
![]() |
ec0c2e8c33 | ||
![]() |
7f3dbe0552 | ||
![]() |
0e9044e0c8 | ||
![]() |
3171640193 | ||
![]() |
a56cee3485 | ||
![]() |
c8ee371982 | ||
![]() |
5778daeb60 | ||
![]() |
f51f3b9861 | ||
![]() |
44dd1a0b02 | ||
![]() |
61a00ffcbf | ||
![]() |
4b0a0f0a32 | ||
![]() |
a3088fb8bc | ||
![]() |
88fd1f9eb1 | ||
![]() |
15156bac1e | ||
![]() |
a898d2e7be | ||
![]() |
95b003802c | ||
![]() |
95c9eae4ed | ||
![]() |
e3814403e4 | ||
![]() |
3d16d52dd8 | ||
![]() |
1ae47fffb4 | ||
![]() |
4e7096b9e2 | ||
![]() |
8cc9b7f6a7 | ||
![]() |
fb45c1020e | ||
![]() |
e9db4ae8f4 | ||
![]() |
c46ec32bd6 | ||
![]() |
c58a26ed99 | ||
![]() |
a66f5e4971 | ||
![]() |
574c8c6089 | ||
![]() |
67afd95910 | ||
![]() |
f7d0cb0be7 | ||
![]() |
be9b68a0b1 | ||
![]() |
4637414af2 | ||
![]() |
4bd92a72bd | ||
![]() |
a3be26f3e4 | ||
![]() |
675c906cbf | ||
![]() |
6be6023236 | ||
![]() |
42cee0d018 | ||
![]() |
041f725748 | ||
![]() |
0594d61631 | ||
![]() |
15cae6b765 | ||
![]() |
b984116c35 | ||
![]() |
13bda6e3f4 | ||
![]() |
c0d18549d1 | ||
![]() |
3caff72fce | ||
![]() |
1313e9c3f4 | ||
![]() |
0848d5a39e | ||
![]() |
7660646059 | ||
![]() |
bcd90fc744 | ||
![]() |
638fc22d62 | ||
![]() |
c87d365b88 | ||
![]() |
aee9602f25 | ||
![]() |
976fbd0220 | ||
![]() |
afd955d06f | ||
![]() |
4d548da66b | ||
![]() |
41b70f53d1 | ||
![]() |
a47a618bcd | ||
![]() |
62170a30af | ||
![]() |
780c5ac23c | ||
![]() |
9fba519a5a | ||
![]() |
3cd0e7d26b | ||
![]() |
a8fd6af994 | ||
![]() |
4000b89644 | ||
![]() |
9c00bbc0b7 | ||
![]() |
a2989d3b38 | ||
![]() |
fc731b60d5 | ||
![]() |
193980dd4a | ||
![]() |
35427b0768 | ||
![]() |
73ea130e40 | ||
![]() |
5667e6aaee | ||
![]() |
fbd626131d | ||
![]() |
7b82444338 | ||
![]() |
8108b9f565 | ||
![]() |
c6ddd00cd9 | ||
![]() |
20c0c00fa0 | ||
![]() |
1f90364ba6 | ||
![]() |
49ea4d31a5 | ||
![]() |
dc35f1456a | ||
![]() |
0ebeb90804 | ||
![]() |
3ef5436c98 | ||
![]() |
de7996d789 | ||
![]() |
ac52d9bae2 | ||
![]() |
cb02df3b76 | ||
![]() |
5fc5a6f1a6 | ||
![]() |
726a0d0394 | ||
![]() |
6edf5345a3 | ||
![]() |
242bbfdb14 | ||
![]() |
89e7712676 | ||
![]() |
9525786929 | ||
![]() |
72088e41a8 | ||
![]() |
a3ed9ff2ef | ||
![]() |
ff16dc73ec | ||
![]() |
2da4ef5f0f | ||
![]() |
eaf481799d | ||
![]() |
1f72863aba | ||
![]() |
6b353fd8d8 | ||
![]() |
56cde4ad79 | ||
![]() |
3b86d3c632 | ||
![]() |
4ac7a25afb | ||
![]() |
8248011a12 | ||
![]() |
5f454456d2 | ||
![]() |
e99a619c23 | ||
![]() |
1fc791bb68 | ||
![]() |
f1d83f7c16 | ||
![]() |
527bb72bcf | ||
![]() |
d78409fd07 | ||
![]() |
d5e7e8944f | ||
![]() |
fb405a5c1c | ||
![]() |
a9e471deca | ||
![]() |
9cd15ae337 | ||
![]() |
8ed4cc4b0a | ||
![]() |
a62de441cf | ||
![]() |
02a8999410 | ||
![]() |
59c7979d69 | ||
![]() |
bb7b28cd8f | ||
![]() |
056497b98a | ||
![]() |
ac2fb032c4 | ||
![]() |
c933bdd5d9 | ||
![]() |
89c71a58fa | ||
![]() |
27ba85b4ff | ||
![]() |
79a75fed8e | ||
![]() |
ee7a76b29f | ||
![]() |
c53bdc3ce0 | ||
![]() |
f36e328751 | ||
![]() |
871b5688c2 | ||
![]() |
b96076b297 | ||
![]() |
d4488e40cf | ||
![]() |
7e61497243 | ||
![]() |
e71ccdd12a | ||
![]() |
202129d491 | ||
![]() |
a1700dd503 | ||
![]() |
2954776539 | ||
![]() |
fb1f122ef7 | ||
![]() |
96c63e4689 | ||
![]() |
c94936d3dc | ||
![]() |
8c22f11087 | ||
![]() |
8a089c84a9 | ||
![]() |
b631e6f8a2 | ||
![]() |
b3b48b032c | ||
![]() |
f3e8230eca | ||
![]() |
cc9adf9d40 | ||
![]() |
15a640d1dc | ||
![]() |
c25b9f86db | ||
![]() |
ecfd033afb | ||
![]() |
f3ed8c7dff | ||
![]() |
6089046721 | ||
![]() |
44ff92ad4b | ||
![]() |
892262eb85 | ||
![]() |
2d9cc4d198 | ||
![]() |
a0c479485d | ||
![]() |
5650f18e50 | ||
![]() |
553885d025 | ||
![]() |
35de00c4af | ||
![]() |
09583e5de5 | ||
![]() |
38b0b7cd00 | ||
![]() |
8b9c7b0c27 | ||
![]() |
1005619bf3 | ||
![]() |
3e09cff9cb | ||
![]() |
c24384e454 | ||
![]() |
f87a543406 | ||
![]() |
f752136283 | ||
![]() |
7e71622a44 | ||
![]() |
da92afb379 | ||
![]() |
d3062de5f9 | ||
![]() |
f1440b03a8 | ||
![]() |
9a8b266cef | ||
![]() |
2a9bc57120 | ||
![]() |
2ed83a0e30 | ||
![]() |
116e8fd30a | ||
![]() |
891f11173b | ||
![]() |
dfc7996c17 | ||
![]() |
dc0561d34f | ||
![]() |
4fb0845d79 | ||
![]() |
0e0d4837b8 | ||
![]() |
a6adde7966 | ||
![]() |
7b693132f9 | ||
![]() |
3c3114b6ab | ||
![]() |
5cdbf58f59 | ||
![]() |
6f0a4131a2 | ||
![]() |
aa520e2f5d | ||
![]() |
2c3b7e9ee8 | ||
![]() |
b86a28092a | ||
![]() |
d59e5f2133 | ||
![]() |
3fdd187102 | ||
![]() |
3f085fd8ae | ||
![]() |
a4fc131aec | ||
![]() |
d7d446c3fc | ||
![]() |
212666e603 | ||
![]() |
b545c28340 | ||
![]() |
72bc345515 | ||
![]() |
cc5082a9e3 | ||
![]() |
45782a6c6c | ||
![]() |
e86d646cce | ||
![]() |
92cfc6b8c8 | ||
![]() |
82289d9f1f | ||
![]() |
4cdbdaaf4e | ||
![]() |
ecde2427da | ||
![]() |
fed1ec5d83 | ||
![]() |
4fbd764ced | ||
![]() |
5361079010 | ||
![]() |
002d135ef5 | ||
![]() |
a39b0a4a78 | ||
![]() |
eb5d68422f | ||
![]() |
3dc13e5c2e | ||
![]() |
16881f057a | ||
![]() |
1cd7d0577f | ||
![]() |
3c872df97a | ||
![]() |
218b7bd2a0 | ||
![]() |
4552d6970d | ||
![]() |
4b319d15a7 | ||
![]() |
0ae3a4172c | ||
![]() |
bf0c12f1c4 | ||
![]() |
cb5eeecb86 | ||
![]() |
8d857cf2be | ||
![]() |
6f232c465f | ||
![]() |
032d444246 | ||
![]() |
49488dd3fb | ||
![]() |
9aec3865ff | ||
![]() |
b6b7f2051b | ||
![]() |
46254a699a | ||
![]() |
7b3c287137 | ||
![]() |
1a533742a5 | ||
![]() |
2027266852 | ||
![]() |
946d8b1a7b | ||
![]() |
6d2fb5de6f | ||
![]() |
91c4a002dd | ||
![]() |
4d8112aae5 | ||
![]() |
bb53f245cf | ||
![]() |
9f31cdbf5b | ||
![]() |
9a33039d73 | ||
![]() |
7cf3be8333 | ||
![]() |
82afb88e53 | ||
![]() |
4aa24b5d67 | ||
![]() |
57112c21a2 | ||
![]() |
0e8ceeb6c9 | ||
![]() |
f52b8d1f04 | ||
![]() |
f374cc77ae | ||
![]() |
7c694e7fae | ||
![]() |
932ffc2673 | ||
![]() |
3de5438139 | ||
![]() |
c4b5f34271 | ||
![]() |
22d3ac33a2 | ||
![]() |
2e5dd6535a | ||
![]() |
eac58a2a50 | ||
![]() |
e939ec0e52 | ||
![]() |
5b17a14a2a | ||
![]() |
8fb8c888f5 | ||
![]() |
4a2884509e | ||
![]() |
e295235a89 | ||
![]() |
ef515a38d0 | ||
![]() |
02cff040e3 | ||
![]() |
bb0f65a52d | ||
![]() |
d51d6a5cc1 | ||
![]() |
eb99379a79 | ||
![]() |
388eb57d0d | ||
![]() |
0b8131392a | ||
![]() |
229efbd006 | ||
![]() |
a482fa3a8d | ||
![]() |
6cf047af39 | ||
![]() |
41748c0b3f | ||
![]() |
1ce8be3c7e | ||
![]() |
32778acf57 | ||
![]() |
a3c71473ae | ||
![]() |
aceece7e90 | ||
![]() |
52efb4f9ef | ||
![]() |
6b0d96fe8d | ||
![]() |
ad052821b0 | ||
![]() |
da7636e60c | ||
![]() |
ef01dd0d77 | ||
![]() |
03f7d4673f | ||
![]() |
94e9c87978 | ||
![]() |
501bbbe4df | ||
![]() |
c9122a3fee | ||
![]() |
8a289d014e | ||
![]() |
ddadd38151 | ||
![]() |
0b8d0e3cac | ||
![]() |
eeb27d38bc | ||
![]() |
491a79ec96 | ||
![]() |
f429db61af | ||
![]() |
2881099602 | ||
![]() |
672ae8decf | ||
![]() |
2abc7e541d | ||
![]() |
45b1f369ac | ||
![]() |
3b5d2c8f6f | ||
![]() |
5376e16c9f | ||
![]() |
af052242fa | ||
![]() |
85e0b71545 | ||
![]() |
1206d1fcf6 | ||
![]() |
f7534dc438 | ||
![]() |
97f317254e | ||
![]() |
9eaf51e15f | ||
![]() |
7221f4ac02 | ||
![]() |
1bb6dce239 | ||
![]() |
d13db5e8eb | ||
![]() |
040b5535f3 | ||
![]() |
b44e1618fb | ||
![]() |
1e13483bc3 | ||
![]() |
f9519d3923 | ||
![]() |
86cdfbb79b | ||
![]() |
a70585e854 | ||
![]() |
040d0a8635 | ||
![]() |
efa512ab21 | ||
![]() |
9b04aed8b3 | ||
![]() |
7087eafe37 | ||
![]() |
c81c4af653 | ||
![]() |
c05cc9dd02 | ||
![]() |
1a0da00f2d | ||
![]() |
31b0c1d3d7 | ||
![]() |
53c1d40bcf | ||
![]() |
97cacb4383 | ||
![]() |
e03905abaf | ||
![]() |
06eba28b4c | ||
![]() |
bbfeac46dd | ||
![]() |
2fe4da094a | ||
![]() |
b454d8c0f9 | ||
![]() |
1f9b5453cc | ||
![]() |
3261791e99 | ||
![]() |
3bb12e3f45 | ||
![]() |
1dc2f7e5a2 | ||
![]() |
2531b08538 | ||
![]() |
9fcfb5493c | ||
![]() |
4576354c51 | ||
![]() |
1dcf2ef0c6 | ||
![]() |
3642c65e8c | ||
![]() |
40e105994a | ||
![]() |
f2ee973882 | ||
![]() |
3aa30792bf | ||
![]() |
6e336fa78e | ||
![]() |
900027a6b7 | ||
![]() |
38bdca2409 | ||
![]() |
7196e476bf | ||
![]() |
e0fd3785d9 | ||
![]() |
b53ebb6c2a | ||
![]() |
1ea80f4447 | ||
![]() |
627d3c0a7a | ||
![]() |
182cccfc71 | ||
![]() |
6a3713e86c | ||
![]() |
788da4e4f1 | ||
![]() |
fd26d34e19 | ||
![]() |
e9fcdc7d2e | ||
![]() |
0fe4911d01 | ||
![]() |
d4fb09fa80 | ||
![]() |
e6d5a37236 | ||
![]() |
79fd10ac10 | ||
![]() |
a2e6095e44 | ||
![]() |
64530471a0 | ||
![]() |
e31e831309 | ||
![]() |
cf6871df9b | ||
![]() |
482e7f1c75 | ||
![]() |
aab501e31e | ||
![]() |
ceec9e5e1b | ||
![]() |
aadebb3cc5 | ||
![]() |
657ddd3341 | ||
![]() |
62127b6d48 | ||
![]() |
f5f405796f | ||
![]() |
39873947a3 | ||
![]() |
a1079dd948 | ||
![]() |
4eeabcc9e0 | ||
![]() |
c3568d07e8 | ||
![]() |
1adb4a4ba8 | ||
![]() |
6d0020533c | ||
![]() |
4e6af0a655 | ||
![]() |
00f726b515 | ||
![]() |
035aa32305 | ||
![]() |
62ea4b98e1 | ||
![]() |
4be821137d | ||
![]() |
7fba9960bf | ||
![]() |
876bfbd3cb | ||
![]() |
edde2c210b | ||
![]() |
f956d96d94 | ||
![]() |
c2296fd900 | ||
![]() |
0feed5b640 | ||
![]() |
93904dcb1b | ||
![]() |
86cbdf793a | ||
![]() |
56b1b9b598 | ||
![]() |
f7ec3ae131 | ||
![]() |
01d11d6213 | ||
![]() |
74a316e758 | ||
![]() |
d20c5185a4 | ||
![]() |
da965e7b39 | ||
![]() |
3fbed815a5 | ||
![]() |
152be29739 | ||
![]() |
e521740a44 | ||
![]() |
ee047e8bc1 | ||
![]() |
5eaa9ca347 | ||
![]() |
40f79ee816 | ||
![]() |
f0dcef7981 | ||
![]() |
3c09ff13d0 | ||
![]() |
7158f25f37 | ||
![]() |
54f805b6e4 | ||
![]() |
70c4651fbf | ||
![]() |
962d3c064f | ||
![]() |
c6a459a111 | ||
![]() |
b0242ccb62 | ||
![]() |
53f5277b08 | ||
![]() |
90b54435b5 | ||
![]() |
12a1681b42 | ||
![]() |
4277cb3f3c | ||
![]() |
8353d53589 | ||
![]() |
9e94d98cfb | ||
![]() |
b6ec1aaa9b | ||
![]() |
e7e8763f1c | ||
![]() |
515c1af676 | ||
![]() |
6fa7a973ba | ||
![]() |
3e63f509bc | ||
![]() |
b3b02e781a | ||
![]() |
6d83921e20 | ||
![]() |
30bd372d45 | ||
![]() |
63254b7e55 | ||
![]() |
f4c08d93f4 | ||
![]() |
6ca1ac21e4 | ||
![]() |
381ee1c30e |
2
.env.universal
Normal file
2
.env.universal
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_BUILD_TYPE = Production
|
||||
VITE_BUILD_PLATFORM = Universal
|
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -10,13 +10,12 @@ body:
|
||||
在提交新的 Bug 反馈前,请确保您:
|
||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||
* 不与现有的某一 issue 重复
|
||||
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
|
||||
- type: input
|
||||
id: system-version
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 运行 QQNT 的系统版本
|
||||
placeholder: Windows 10 Pro Workstation 22H2
|
||||
placeholder: Windows 11 24H2
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -24,7 +23,7 @@ body:
|
||||
attributes:
|
||||
label: QQNT 版本
|
||||
description: 可在 QQNT 的「关于」的设置页中找到
|
||||
placeholder: 9.9.7-21804
|
||||
placeholder: 9.9.16-29927
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -40,21 +39,21 @@ body:
|
||||
attributes:
|
||||
label: OneBot 客户端
|
||||
description: 连接至 NapCat 的客户端版本信息
|
||||
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
|
||||
placeholder: Karin 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: 发生了什么?
|
||||
description: 填写你认为的 NapCat 的不正常行为
|
||||
description: 填写你认为的 NapCat 的异常行为
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-reproduce
|
||||
attributes:
|
||||
label: 如何复现
|
||||
description: 填写应当如何操作才能触发这个不正常行为
|
||||
description: 填写应当如何操作才能触发这个异常行为
|
||||
placeholder: |
|
||||
1. xxx
|
||||
2. xxx
|
||||
|
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -1,11 +1,6 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,14 +1,12 @@
|
||||
# Develop
|
||||
node_modules/
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
out/
|
||||
dist/
|
||||
/src/core.lib/common/
|
||||
/localdebug/
|
||||
|
||||
# Editor
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/*
|
||||
|
||||
|
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.expand": false,
|
||||
"explorer.fileNesting.patterns": {
|
||||
".env.universal": ".env.*",
|
||||
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
|
||||
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
|
||||
}
|
||||
}
|
208
LICENSE
208
LICENSE
@@ -1,201 +1,19 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Limited Redistribution License for NapCat
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright © 2024 Mlikiowa
|
||||
|
||||
1. Definitions.
|
||||
1. Usage and Reproduction:
|
||||
- Unauthorized use, reproduction, modification, or distribution of this code is prohibited without explicit permission from the main author of the NapCat repository.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
2. Redistribution:
|
||||
- Redistribution of this code is permitted, provided that the full text of this license is included, and the source and copyright information is clearly stated.
|
||||
- Minor modifications and extensions are allowed for redistribution purposes, but the modified code must not be publicly released.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
3. Non-Commercial Use:
|
||||
- This code is not to be used for any commercial purposes.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
4. Additional Permissions:
|
||||
- Any rights not explicitly addressed in this license must be requested from and granted by the main author of the NapCat repository.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5. Disclaimer:
|
||||
- This code is provided "as is," without any express or implied warranties, including but not limited to the implied warranties of merchantability and fitness for a particular purpose. In no event shall the author be liable for any damages or other liability arising from, out of, or in connection with the use or distribution of this code.
|
||||
|
28
README.md
28
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,25 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||
|
||||
[Server.Other](https://napcat.cyou/)
|
||||
[Server.Other](https://docs.napcat.cyou/)
|
||||
|
||||
[NapCat.Wiki](https://www.napcat.wiki)
|
||||
|
||||
## 回家旅途
|
||||
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
|
||||
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
|
||||
|
||||
[QQ Group#2](https://qm.qq.com/q/HaRcfrHpUk)
|
||||
|
||||
[Telegram](https://t.me/MelodicMoonlight)
|
||||
|
||||
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
|
||||
|
||||
## 性能设计/协议标准
|
||||
NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
|
||||
|
||||
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,消息Id无法持久,无法上报撤回消息原始内容。
|
||||
|
||||
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
|
||||
|
||||
## 感谢他们
|
||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||
@@ -45,12 +59,8 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
||||
|
||||
---
|
||||
|
||||
## 延缓Native模块与NapCat对新版QQ适配
|
||||
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
|
||||
|
||||
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
|
||||
|
||||
因此此时推荐使用release指定版本
|
||||
## 特殊感谢
|
||||
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
|
||||
|
||||
## 开源附加
|
||||
|
||||
|
BIN
external/LiteLoaderWrapper.zip
vendored
BIN
external/LiteLoaderWrapper.zip
vendored
Binary file not shown.
BIN
external/logo.png
vendored
Normal file
BIN
external/logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "qq-chat",
|
||||
"version": "9.9.16-29456",
|
||||
"verHash": "dd395162",
|
||||
"linuxVersion": "3.2.13-29456",
|
||||
"linuxVerHash": "e379390a",
|
||||
"version": "9.9.17-30899",
|
||||
"verHash": "ececf273",
|
||||
"linuxVersion": "3.2.15-30899",
|
||||
"linuxVerHash": "63c751e8",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"description": "QQ",
|
||||
@@ -18,7 +18,7 @@
|
||||
"qd": "externals/devtools/cli/index.js"
|
||||
},
|
||||
"main": "./loadNapCat.js",
|
||||
"buildVersion": "29456",
|
||||
"buildVersion": "30899",
|
||||
"isPureShell": true,
|
||||
"isByteCodeShell": true,
|
||||
"platform": "win32",
|
||||
|
BIN
logo.png
BIN
logo.png
Binary file not shown.
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 684 KiB |
@@ -4,16 +4,12 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.1.12",
|
||||
"version": "4.4.1",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
"name": "MliKiowa",
|
||||
"link": "https://github.com/MliKiowa"
|
||||
},
|
||||
{
|
||||
"name": "Young",
|
||||
"link": "https://github.com/Wesley-Young"
|
||||
"name": "NapNeko",
|
||||
"link": "https://github.com/NapNeko"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="./logo_webui.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NapCat WebUI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -5,21 +5,24 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||
"webui:dev": "vite",
|
||||
"webui:dev": "vite --host",
|
||||
"webui:build": "vite build",
|
||||
"webui:preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"mitt": "^3.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"tdesign-icons-vue-next": "^0.3.3",
|
||||
"tdesign-vue-next": "^1.10.3",
|
||||
"vue": "^3.5.12",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@vitejs/plugin-legacy": "^5.4.3",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
|
BIN
napcat.webui/public/logo_webui.png
Normal file
BIN
napcat.webui/public/logo_webui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
@@ -1,7 +1,112 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="app" theme-mode="dark">
|
||||
<router-view />
|
||||
</div>
|
||||
<div v-if="show">
|
||||
<t-sticky-tool shape="round" placement="right-bottom" :offset="[-50, 10]" @click="changeTheme">
|
||||
<t-sticky-item label="浅色" popup="切换浅色模式">
|
||||
<template #icon><sunny-icon /></template>
|
||||
</t-sticky-item>
|
||||
<t-sticky-item label="深色" popup="切换深色模式">
|
||||
<template #icon><mode-dark-icon /></template>
|
||||
</t-sticky-item>
|
||||
<t-sticky-item label="自动" popup="跟随系统">
|
||||
<template #icon><control-platform-icon /></template>
|
||||
</t-sticky-item>
|
||||
</t-sticky-tool>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { ControlPlatformIcon, ModeDarkIcon, SunnyIcon } from 'tdesign-icons-vue-next';
|
||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
||||
interface Item {
|
||||
label: string;
|
||||
popup: string;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
item: Item;
|
||||
}
|
||||
enum ThemeMode {
|
||||
Dark = 'dark',
|
||||
Light = 'light',
|
||||
Auto = 'auto',
|
||||
}
|
||||
const themeLabelMap: Record<string, ThemeMode> = {
|
||||
"浅色": ThemeMode.Light,
|
||||
"深色": ThemeMode.Dark,
|
||||
"自动": ThemeMode.Auto,
|
||||
};
|
||||
const show = ref<boolean>(true);
|
||||
const createSetThemeAttributeFunction = () => {
|
||||
let mediaQueryForAutoTheme: MediaQueryList | null = null;
|
||||
return (mode: ThemeMode | null) => {
|
||||
const element = document.documentElement;
|
||||
if (mode === ThemeMode.Dark) {
|
||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
||||
} else if (mode === ThemeMode.Light) {
|
||||
element.removeAttribute('theme-mode');
|
||||
} else if (mode === ThemeMode.Auto) {
|
||||
mediaQueryForAutoTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
element.setAttribute('theme-mode', ThemeMode.Dark);
|
||||
} else {
|
||||
element.removeAttribute('theme-mode');
|
||||
}
|
||||
};
|
||||
mediaQueryForAutoTheme.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQueryForAutoTheme.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQueryForAutoTheme.dispatchEvent(event);
|
||||
onBeforeUnmount(() => {
|
||||
if (mediaQueryForAutoTheme) {
|
||||
mediaQueryForAutoTheme.removeEventListener('change', handleMediaChange);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const setThemeAttribute = createSetThemeAttributeFunction();
|
||||
|
||||
const getStoredTheme = (): ThemeMode | null => {
|
||||
return localStorage.getItem('theme') as ThemeMode | null;
|
||||
};
|
||||
|
||||
const initTheme = () => {
|
||||
const storedTheme = getStoredTheme();
|
||||
if (storedTheme === null) {
|
||||
setThemeAttribute(ThemeMode.Auto);
|
||||
} else {
|
||||
setThemeAttribute(storedTheme);
|
||||
}
|
||||
};
|
||||
|
||||
const changeTheme = (context: Context) => {
|
||||
const themeLabel = themeLabelMap[context.item.label] as ThemeMode;
|
||||
console.log(themeLabel);
|
||||
setThemeAttribute(themeLabel);
|
||||
localStorage.setItem('theme', themeLabel);
|
||||
};
|
||||
const haddingFbars = () => {
|
||||
show.value = !smallScreen.matches;
|
||||
if (smallScreen.matches) {
|
||||
localStorage.setItem('theme', 'auto');
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
haddingFbars();
|
||||
window.addEventListener('resize', haddingFbars);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', haddingFbars);
|
||||
});
|
||||
</script>
|
||||
<style></style>
|
||||
|
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
BIN
napcat.webui/src/assets/0xProtoNerdFont-Italic.ttf
Normal file
Binary file not shown.
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
BIN
napcat.webui/src/assets/logo_webui.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
66
napcat.webui/src/backend/githubApi.ts
Normal file
66
napcat.webui/src/backend/githubApi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export class githubApiManager {
|
||||
public async GetBaseData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting github data :', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetReleasesData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/releases', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting releases data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetPullsData(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/pulls', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Pulls data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async GetContributors(): Promise<Response | null> {
|
||||
try {
|
||||
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/contributors', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
return await ConfigResponse.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Pulls data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
72
napcat.webui/src/backend/log.ts
Normal file
72
napcat.webui/src/backend/log.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
type LogListItem = string;
|
||||
type LogListData = LogListItem[];
|
||||
let eventSourcePoly: EventSourcePolyfill | null = null;
|
||||
export class LogManager {
|
||||
private readonly retCredential: string;
|
||||
private readonly apiPrefix: string;
|
||||
|
||||
//调试时http://127.0.0.1:6099/api 打包时 ../api
|
||||
constructor(retCredential: string, apiPrefix: string = '../api') {
|
||||
this.retCredential = retCredential;
|
||||
this.apiPrefix = apiPrefix;
|
||||
}
|
||||
public async GetLogList(): Promise<LogListData> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLogList`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data as LogListData;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting LogList:', error);
|
||||
}
|
||||
return [] as LogListData;
|
||||
}
|
||||
public async GetLog(FileName: string): Promise<string> {
|
||||
try {
|
||||
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLog?id=${FileName}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting LogData:', error);
|
||||
}
|
||||
return 'null';
|
||||
}
|
||||
public async getRealTimeLogs(): Promise<EventSourcePolyfill | null> {
|
||||
this.creatEventSource();
|
||||
return eventSourcePoly;
|
||||
}
|
||||
private creatEventSource() {
|
||||
try {
|
||||
eventSourcePoly = new EventSourcePolyfill(`${this.apiPrefix}/Log/GetLogRealTime`, {
|
||||
heartbeatTimeout: 3 * 60 * 1000,
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
Accept: 'text/event-stream',
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建SSE连接出错:', error);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
|
||||
import { ResponseCode } from '../../../src/webui/src/const/status';
|
||||
export class QQLoginManager {
|
||||
private retCredential: string;
|
||||
private readonly apiPrefix: string;
|
||||
@@ -22,8 +22,8 @@ export class QQLoginManager {
|
||||
});
|
||||
if (ConfigResponse.status == 200) {
|
||||
const ConfigResponseJson = await ConfigResponse.json();
|
||||
if (ConfigResponseJson.code == 0) {
|
||||
return ConfigResponseJson?.data as OneBotConfig;
|
||||
if (ConfigResponseJson.code == ResponseCode.Success) {
|
||||
return ConfigResponseJson.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -74,6 +74,26 @@ export class QQLoginManager {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
|
||||
try {
|
||||
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.retCredential,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (QQLoginResponse.status == 200) {
|
||||
const QQLoginResponseJson = await QQLoginResponse.json();
|
||||
if (QQLoginResponseJson.code == 0) {
|
||||
return QQLoginResponseJson.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking QQ login status:', error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async checkWebUiLogined(): Promise<boolean> {
|
||||
try {
|
||||
|
@@ -1,16 +1,28 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
||||
<div class="content">
|
||||
<router-view />
|
||||
<t-layout class="dashboard-container">
|
||||
<div v-if="!mediaQuery.matches">
|
||||
<SidebarMenu
|
||||
:menu-items="menuItems"
|
||||
class="sidebar-menu"
|
||||
:menu-width="sidebarWidth"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<t-layout>
|
||||
<router-view />
|
||||
</t-layout>
|
||||
<div v-if="mediaQuery.matches" class="bottom-menu">
|
||||
<BottomMenu :menu-items="menuItems" />
|
||||
</div>
|
||||
</t-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import SidebarMenu from './webui/Nav.vue';
|
||||
|
||||
import BottomMenu from './webui/NavBottom.vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
const sidebarWidth = ['232px', '64px'];
|
||||
interface MenuItem {
|
||||
value: string;
|
||||
icon: string;
|
||||
@@ -25,6 +37,19 @@ const menuItems = ref<MenuItem[]>([
|
||||
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
||||
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
|
||||
]);
|
||||
emitter.on('sendMenu', (event) => {
|
||||
const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
|
||||
emitter.emit('sendWidth', menuWidth);
|
||||
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (mediaQuery.matches){
|
||||
localStorage.setItem('menuWidth', '0');
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -32,19 +57,18 @@ const menuItems = ref<MenuItem[]>([
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
/* padding: 20px; */
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.bottom-menu {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -53,3 +77,19 @@ const menuItems = ref<MenuItem[]>([
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.t-head-menu__inner .t-menu:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.t-head-menu__inner{
|
||||
width: 100%;
|
||||
}
|
||||
.t-head-menu .t-menu{
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.t-menu__content{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,40 +1,33 @@
|
||||
<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
|
||||
>
|
||||
<t-card class="layout" :bordered="false">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">QQ Login</h2>
|
||||
<div class="login-methods">
|
||||
<t-tooltip content="快速登录">
|
||||
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
|
||||
@click="loginMethod = 'quick'">Quick Login</t-button>
|
||||
</t-tooltip>
|
||||
<t-tooltip content="二维码登录">
|
||||
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
|
||||
@click="loginMethod = 'qrcode'">QR Code</t-button>
|
||||
</t-tooltip>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
|
||||
@change="selectAccount">
|
||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
||||
</t-select>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
||||
<canvas ref="qrcodeCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||
<t-select
|
||||
id="quick-login-select"
|
||||
v-model="selectedAccount"
|
||||
placeholder="Select Account"
|
||||
@change="selectAccount"
|
||||
>
|
||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
||||
</t-select>
|
||||
</div>
|
||||
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
||||
<canvas ref="qrcodeCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
@@ -47,10 +40,14 @@ const selectedAccount = ref<string>('');
|
||||
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
||||
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
||||
let heartBeatTimer: number | null = null;
|
||||
let qrcodeUrl: string = '';
|
||||
|
||||
const selectAccount = async (accountName: string): Promise<void> => {
|
||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||
if (result) {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
await MessagePlugin.success('登录成功即将跳转');
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else {
|
||||
@@ -73,36 +70,61 @@ const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void =>
|
||||
};
|
||||
|
||||
const HeartBeat = async (): Promise<void> => {
|
||||
const isLogined = await qqLoginManager.checkQQLoginStatus();
|
||||
if (isLogined) {
|
||||
const isLogined = await qqLoginManager.checkQQLoginStatusWithQrcode();
|
||||
if (isLogined?.isLogin) {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
await MessagePlugin.success('登录成功即将跳转');
|
||||
await router.push({ path: '/dashboard/basic-info' });
|
||||
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
|
||||
qrcodeUrl = isLogined.qrcodeurl;
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
}
|
||||
};
|
||||
|
||||
const InitPages = async (): Promise<void> => {
|
||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
|
||||
generateQrCode(qrcodeData, qrcodeCanvas.value);
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
|
||||
await nextTick();
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
InitPages();
|
||||
InitPages().then().catch((err) => {
|
||||
console.error('InitPages Error:', err);
|
||||
});
|
||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (heartBeatTimer) {
|
||||
clearInterval(heartBeatTimer);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
watch(loginMethod, async (newMethod) => {
|
||||
if (newMethod === 'qrcode') {
|
||||
await nextTick();
|
||||
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@@ -161,7 +183,5 @@ onMounted(() => {
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
@@ -1,20 +1,22 @@
|
||||
<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>
|
||||
<t-card class="layout" :bordered="false">
|
||||
<div class="login-container">
|
||||
<h2 class="sotheby-font">WebUi Login</h2>
|
||||
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
||||
<t-form-item name="password">
|
||||
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
||||
<template #prefix-icon>
|
||||
<lock-on-icon />
|
||||
</template>
|
||||
</t-input>
|
||||
</t-form-item>
|
||||
<t-form-item>
|
||||
<t-button theme="primary" type="submit" block>登录</t-button>
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -94,14 +96,16 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
height: 100vh;
|
||||
}
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@@ -145,7 +149,5 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,16 +1,31 @@
|
||||
<template>
|
||||
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
||||
<template #logo> </template>
|
||||
<t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
|
||||
<template #logo>
|
||||
<div class="logo">
|
||||
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
|
||||
<div class="logo-textBox">
|
||||
<div class="logo-text">{{ collapsed ? '' : 'NapCat' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</t-menu-item>
|
||||
<t-tooltip :disabled="!collapsed" :content="item.label" placement="right">
|
||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
{{ item.label }}
|
||||
</t-menu-item>
|
||||
</t-tooltip>
|
||||
</router-link>
|
||||
<template #operations>
|
||||
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
|
||||
<t-button
|
||||
:disabled="disBtn"
|
||||
class="t-demo-collapse-btn"
|
||||
variant="text"
|
||||
shape="square"
|
||||
@click="changeCollapsed"
|
||||
>
|
||||
<template #icon><t-icon :name="iconName" /></template>
|
||||
</t-button>
|
||||
</template>
|
||||
@@ -18,7 +33,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps } from 'vue';
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
|
||||
type MenuItem = {
|
||||
value: string;
|
||||
@@ -27,19 +43,42 @@ type MenuItem = {
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
menuItems: MenuItem[];
|
||||
menuWidth: string | number | Array<string | number>;
|
||||
}>();
|
||||
|
||||
const mediaQuery = window.matchMedia('(max-width: 800px)');
|
||||
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
||||
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
||||
const disBtn = ref<boolean>(false);
|
||||
|
||||
const changeCollapsed = (): void => {
|
||||
collapsed.value = !collapsed.value;
|
||||
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
||||
};
|
||||
watch(collapsed, (newValue, oldValue) => {
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
});
|
||||
onMounted(() => {
|
||||
emitter.emit('sendMenu', collapsed.value);
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
disBtn.value = e.matches;
|
||||
if (e.matches) {
|
||||
collapsed.value = e.matches;
|
||||
}
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQuery.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQuery.dispatchEvent(event);
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -57,12 +96,28 @@ const changeCollapsed = (): void => {
|
||||
width: 100px; /* 移动端侧边栏宽度 */
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.logo-img {
|
||||
object-fit: contain;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.logo-textBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.logo-text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 22px;
|
||||
font-family: Sotheby, Helvetica, monospace;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
35
napcat.webui/src/components/webui/NavBottom.vue
Normal file
35
napcat.webui/src/components/webui/NavBottom.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<t-head-menu theme="light" class="bottom-menu">
|
||||
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
|
||||
<t-tooltip :content="item.label" placement="top">
|
||||
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||
<template #icon>
|
||||
<t-icon :name="item.icon" />
|
||||
</template>
|
||||
|
||||
<!-- {{item.label}}-->
|
||||
</t-menu-item>
|
||||
</t-tooltip>
|
||||
</router-link>
|
||||
</t-head-menu>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
type MenuItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
route: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
menuItems: MenuItem[];
|
||||
}>();
|
||||
</script>
|
||||
<style scoped>
|
||||
.bottom-menu {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 0.8px solid #ddd;
|
||||
}
|
||||
</style>
|
@@ -4,3 +4,10 @@
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'ProtoNerdFontItalic';
|
||||
src: url('../assets/0xProtoNerdFont-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
@@ -19,6 +19,10 @@ import {
|
||||
List as TList,
|
||||
Alert as TAlert,
|
||||
Tag as TTag,
|
||||
Descriptions as TDescriptionsProps,
|
||||
DescriptionsItem as TDescriptionsItem,
|
||||
Collapse as TCollapse,
|
||||
CollapsePanel as TCollapsePanel,
|
||||
ListItem as TListItem,
|
||||
Tabs as TTabs,
|
||||
TabPanel as TTabPanel,
|
||||
@@ -27,10 +31,23 @@ import {
|
||||
Popup as TPopup,
|
||||
Dialog as TDialog,
|
||||
Switch as TSwitch,
|
||||
Tooltip as Tooltip,
|
||||
StickyTool as TStickyTool,
|
||||
StickyItem as TStickyItem,
|
||||
Layout as TLayout,
|
||||
Content as TContent,
|
||||
Footer as TFooter,
|
||||
Aside as TAside,
|
||||
Popconfirm as Tpopconfirm,
|
||||
Empty as TEmpty,
|
||||
Dropdown as TDropdown,
|
||||
Typography as TTypographyText,
|
||||
TreeSelect as TTreeSelect,
|
||||
Loading as TLoading,
|
||||
HeadMenu as THeadMenu
|
||||
} from 'tdesign-vue-next';
|
||||
import { router } from './router';
|
||||
import router from './router';
|
||||
import 'tdesign-vue-next/es/style/index.css';
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(TButton);
|
||||
@@ -51,6 +68,10 @@ app.use(TLink);
|
||||
app.use(TList);
|
||||
app.use(TAlert);
|
||||
app.use(TTag);
|
||||
app.use(TDescriptionsProps);
|
||||
app.use(TDescriptionsItem);
|
||||
app.use(TCollapse);
|
||||
app.use(TCollapsePanel);
|
||||
app.use(TListItem);
|
||||
app.use(TTabs);
|
||||
app.use(TTabPanel);
|
||||
@@ -59,4 +80,18 @@ app.use(TCheckbox);
|
||||
app.use(TPopup);
|
||||
app.use(TDialog);
|
||||
app.use(TSwitch);
|
||||
app.use(Tooltip);
|
||||
app.use(TStickyTool);
|
||||
app.use(TStickyItem);
|
||||
app.use(TLayout);
|
||||
app.use(TContent);
|
||||
app.use(TFooter);
|
||||
app.use(TAside);
|
||||
app.use(Tpopconfirm);
|
||||
app.use(TEmpty);
|
||||
app.use(TDropdown);
|
||||
app.use(TTypographyText);
|
||||
app.use(TTreeSelect);
|
||||
app.use(TLoading);
|
||||
app.use(THeadMenu);
|
||||
app.mount('#app');
|
||||
|
@@ -1,23 +1,101 @@
|
||||
<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>
|
||||
<t-divider content="面板关于信息" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<info-circle-icon></info-circle-icon>
|
||||
<div style="margin-left: 5px">面板关于信息</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
<t-alert theme="success" class="header" message="NapCat.WebUi is running" />
|
||||
<t-list>
|
||||
<t-list-item>
|
||||
<div class="label-box">
|
||||
<star-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Star:</span>
|
||||
</div>
|
||||
<span class="item-content">
|
||||
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/stargazers">{{
|
||||
githubBastData?.stargazers_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item class="list-item">
|
||||
<span class="item-label">版本信息:</span>
|
||||
<t-list-item>
|
||||
<tips-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">issues:</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-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
|
||||
githubBastData?.open_issues_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<git-pull-request-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Pull Requests:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/pulls">{{githubPullData?.length
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item >
|
||||
<bookmark-add-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Releases:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/releases">{{
|
||||
githubReleasesData&&githubReleasesData[0]?timeDifference(githubReleasesData[0].published_at) + '前更新':''
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<usergroup-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Contributors:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/graphs/contributors">{{githubContributorsData?.length}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<browse-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Watchers:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/watchers">{{
|
||||
githubBastData?.watchers
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<fork-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Fork:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/fork">{{
|
||||
githubBastData?.forks_count
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<statue-of-jesus-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">License:</span>
|
||||
<span class="item-content">
|
||||
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ#License-1-ov-file">{{
|
||||
githubBastData?.license.key
|
||||
}}</t-link>
|
||||
</span>
|
||||
</t-list-item>
|
||||
<t-list-item>
|
||||
<component-layout-filled-icon class="item-icon" size="large" />
|
||||
<span class="item-label">Version:</span>
|
||||
<span class="item-content">
|
||||
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
|
||||
<t-tag class="tag-item nc-color">
|
||||
NapCat:
|
||||
{{ napCatVersion }}
|
||||
</t-tag>
|
||||
<t-tag v-if="githubReleasesData&&githubReleasesData[0] ?.tag_name" class="tag-item nc-color">
|
||||
New NapCat:
|
||||
{{ githubReleasesData[0].tag_name }}
|
||||
</t-tag>
|
||||
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
|
||||
</span>
|
||||
</t-list-item>
|
||||
</t-list>
|
||||
@@ -28,6 +106,51 @@
|
||||
<script setup lang="ts">
|
||||
import pkg from '../../package.json';
|
||||
import { napCatVersion } from '../../../src/common/version';
|
||||
import {
|
||||
InfoCircleIcon,
|
||||
TipsFilledIcon,
|
||||
StarFilledIcon,
|
||||
GitPullRequestFilledIcon,
|
||||
ForkFilledIcon,
|
||||
StatueOfJesusFilledIcon,
|
||||
BookmarkAddFilledIcon,
|
||||
UsergroupFilledIcon,
|
||||
BrowseFilledIcon,
|
||||
ComponentLayoutFilledIcon,
|
||||
} from 'tdesign-icons-vue-next';
|
||||
import { githubApiManager } from '@/backend/githubApi';
|
||||
import { onMounted, ref } from 'vue';
|
||||
const githubApi = new githubApiManager();
|
||||
const githubBastData = ref<any>(null);
|
||||
const githubReleasesData = ref<any>(null);
|
||||
const githubContributorsData = ref<any>(null);
|
||||
const githubPullData = ref<any>(null);
|
||||
const getBaseData = async () => {
|
||||
githubBastData.value = await githubApi.GetBaseData();
|
||||
githubReleasesData.value = await githubApi.GetReleasesData();
|
||||
githubContributorsData.value = await githubApi.GetContributors();
|
||||
githubPullData.value = await githubApi.GetPullsData();
|
||||
};
|
||||
const timeDifference = (timestamp: string): string => {
|
||||
const givenTime = new Date(timestamp);
|
||||
const currentTime = new Date();
|
||||
const diffInMilliseconds = currentTime.getTime() - givenTime.getTime();
|
||||
|
||||
const seconds = Math.floor(diffInMilliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}小时`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分钟`;
|
||||
} else {
|
||||
return `${seconds}秒`;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getBaseData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -35,23 +158,26 @@ import { napCatVersion } from '../../../src/common/version';
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.list {
|
||||
.label-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
padding: 5px;
|
||||
color: #ffffff;
|
||||
border-radius: 3px;
|
||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
||||
}
|
||||
.item-label {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
@@ -64,3 +190,37 @@ import { napCatVersion } from '../../../src/common/version';
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.t-list-item {
|
||||
padding: 5px var(--td-comp-paddingLR-l);
|
||||
}
|
||||
.item-label {
|
||||
flex: 2;
|
||||
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.pgk-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(-225deg, #9be15d 0%, #00e3ae 100%);
|
||||
}
|
||||
.nc-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
|
||||
}
|
||||
.td-color {
|
||||
color: white;
|
||||
background-image: linear-gradient(225deg, #0acffe 0%, #495aff 100%);
|
||||
}
|
||||
.header {
|
||||
background-image: linear-gradient(225deg, #dfffcd 0%, #90f9c4 48%, #39f3bb 100%) !important;
|
||||
}
|
||||
.link-text{
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #B6CEE8 0%, #F578DC 100%);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,6 +1,600 @@
|
||||
<template>
|
||||
<div class="log-view">
|
||||
<h1>面板日志信息</h1>
|
||||
<p>这里显示面板的日志信息。</p>
|
||||
<div class="title">
|
||||
<t-divider content="日志查看" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<system-log-icon></system-log-icon>
|
||||
<div style="margin-left: 5px">日志查看</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
</div>
|
||||
<div class="tab-box">
|
||||
<t-tabs default-value="realtime" @change="selectType">
|
||||
<t-tab-panel value="realtime" label="实时日志"></t-tab-panel>
|
||||
<t-tab-panel value="history" label="历史日志"></t-tab-panel>
|
||||
</t-tabs>
|
||||
</div>
|
||||
<div class="card-box">
|
||||
<t-card class="card" :bordered="true">
|
||||
<template #actions>
|
||||
<t-row :align="'middle'" justify="center" :style="{ gap: smallScreen.matches ? '5px' : '24px' }">
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="清理日志">
|
||||
<t-button variant="text" shape="square" @click="clearLogs">
|
||||
<clear-icon></clear-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="下载日志">
|
||||
<t-button variant="text" shape="square" @click="downloadText">
|
||||
<download-icon></download-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col
|
||||
v-if="LogDataType === 'history'"
|
||||
flex="auto"
|
||||
style="display: inline-flex; justify-content: center">
|
||||
<t-tooltip content="历史日志">
|
||||
<t-button variant="text" shape="square" @click="historyLog">
|
||||
<history-icon></history-icon>
|
||||
</t-button>
|
||||
</t-tooltip>
|
||||
</t-col>
|
||||
<t-col flex="auto" style="display: inline-flex; justify-content: center">
|
||||
<div class="tag-box">
|
||||
<t-tag class="t-tag" :style="{ backgroundImage: typeKey[optValue.description] }">{{
|
||||
optValue.content }}</t-tag>
|
||||
</div>
|
||||
<t-dropdown :options="options" :min-column-width="112" @click="openTypeList">
|
||||
<t-button variant="text" shape="square">
|
||||
<more-icon />
|
||||
</t-button>
|
||||
</t-dropdown>
|
||||
</t-col>
|
||||
</t-row>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="content" ref="contentBox">
|
||||
<div v-for="item in LogDataType === 'realtime'
|
||||
? realtimeLogHtmlList.get(optValue.description)
|
||||
: historyLogHtmlList.get(optValue.description)">
|
||||
<span>{{ item.time }}</span><span :id="item.type">{{ item.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-card>
|
||||
</div>
|
||||
<t-dialog v-model:visible="visibleBody" header="历史日志" :destroy-on-close="true" :show-in-attached-element="true"
|
||||
:on-confirm="GetLogList" class=".t-dialog__ctx .t-dialog__position">
|
||||
<t-select v-model="value" :options="logFileData" placeholder="请选择日志" :multiple="true"
|
||||
style="text-align: left" />
|
||||
</t-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { MoreIcon, ClearIcon, DownloadIcon, HistoryIcon, SystemLogIcon } from 'tdesign-icons-vue-next';
|
||||
import { nextTick, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { LogManager } from '@/backend/log';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
const smallScreen = window.matchMedia('(max-width: 768px)');
|
||||
const LogDataType = ref<string>('realtime');
|
||||
const visibleBody = ref<boolean>(false);
|
||||
const contentBox = ref<HTMLElement | null>(null);
|
||||
let isMouseEntered = false;
|
||||
const logManager = new LogManager(localStorage.getItem('auth') || '');
|
||||
const eventSource = ref<EventSourcePolyfill | null>(null);
|
||||
const intervalId = ref<number | null>(null);
|
||||
const isPaused = ref(false);
|
||||
interface OptionItem {
|
||||
content: string;
|
||||
value: number;
|
||||
description: string;
|
||||
}
|
||||
const options = ref<OptionItem[]>([
|
||||
{
|
||||
content: '全部',
|
||||
value: 1,
|
||||
description: 'all',
|
||||
},
|
||||
{
|
||||
content: '调试',
|
||||
value: 2,
|
||||
description: 'debug',
|
||||
},
|
||||
{
|
||||
content: '提示',
|
||||
value: 3,
|
||||
description: 'info',
|
||||
},
|
||||
{
|
||||
content: '警告',
|
||||
value: 4,
|
||||
description: 'warn',
|
||||
},
|
||||
{
|
||||
content: '错误',
|
||||
value: 5,
|
||||
description: 'error',
|
||||
},
|
||||
{
|
||||
content: '致命',
|
||||
value: 5,
|
||||
description: 'fatal',
|
||||
},
|
||||
]);
|
||||
const typeKey = ref<Record<string, string>>({
|
||||
all: 'linear-gradient(60deg,#16a085 0%, #f4d03f 100%)',
|
||||
debug: 'linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%)',
|
||||
info: 'linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%)',
|
||||
warn: 'linear-gradient(to right, #e14fad 0%, #f9d423 48%, #e37318 100%)',
|
||||
error: 'linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%)',
|
||||
fatal: 'linear-gradient(-225deg, #fd0700, #ec567f)',
|
||||
});
|
||||
interface logHtml {
|
||||
type?: string;
|
||||
content: string;
|
||||
color?: string;
|
||||
time?: string;
|
||||
}
|
||||
type LogHtmlMap = Map<string, logHtml[]>;
|
||||
const realtimeLogHtmlList = ref<LogHtmlMap>(
|
||||
new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
])
|
||||
);
|
||||
const historyLogHtmlList = ref<LogHtmlMap>(
|
||||
new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
])
|
||||
);
|
||||
const logFileData = ref<{ label: string; value: string }[]>([]);
|
||||
const value = ref([]);
|
||||
const optValue = ref<OptionItem>({
|
||||
content: '全部',
|
||||
value: 1,
|
||||
description: 'all',
|
||||
});
|
||||
const openTypeList = (data: OptionItem) => {
|
||||
optValue.value = data;
|
||||
};
|
||||
const logType = ['debug', 'info', 'warn', 'error', 'fatal'];
|
||||
//清理log
|
||||
const clearLogs = () => {
|
||||
if (LogDataType.value === 'realtime') {
|
||||
clearAllLogs(realtimeLogHtmlList);
|
||||
} else {
|
||||
clearAllLogs(historyLogHtmlList);
|
||||
}
|
||||
};
|
||||
const clearAllLogs = (logList: Ref<Map<string, Array<logHtml>>>) => {
|
||||
if ((optValue.value && optValue.value.description === 'all') || !optValue.value) {
|
||||
logList.value = new Map([
|
||||
['all', []],
|
||||
['debug', []],
|
||||
['info', []],
|
||||
['warn', []],
|
||||
['error', []],
|
||||
['fatal', []],
|
||||
]);
|
||||
} else {
|
||||
logList.value.set(optValue.value.description, []);
|
||||
}
|
||||
};
|
||||
//定时清理log
|
||||
|
||||
const TimerClear = () => {
|
||||
clearAllLogs(realtimeLogHtmlList);
|
||||
};
|
||||
const startTimer = () => {
|
||||
if (!isPaused.value) {
|
||||
intervalId.value = window.setInterval(TimerClear, 0.5 * 60 * 1000);
|
||||
}
|
||||
};
|
||||
const pauseTimer = () => {
|
||||
if (intervalId.value) {
|
||||
window.clearInterval(intervalId.value);
|
||||
isPaused.value = true;
|
||||
}
|
||||
};
|
||||
const resumeTimer = () => {
|
||||
if (isPaused.value) {
|
||||
startTimer();
|
||||
isPaused.value = false;
|
||||
}
|
||||
};
|
||||
const stopTimer = () => {
|
||||
if (intervalId.value) {
|
||||
window.clearInterval(intervalId.value);
|
||||
intervalId.value = null;
|
||||
}
|
||||
};
|
||||
const extractContent = (text: string): string | null => {
|
||||
const regex = /\[([^\]]+)]/;
|
||||
const match = regex.exec(text);
|
||||
if (match && match[1]) {
|
||||
const extracted = match[1].toLowerCase();
|
||||
if (logType.includes(extracted)) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const loadData = (text: string, loadType: string) => {
|
||||
const lines = text.split(/\r\n/);
|
||||
lines.forEach((line) => {
|
||||
if (loadType === 'realtime') {
|
||||
let remoteJson = JSON.parse(line) as { message: string, level: string };
|
||||
const type = remoteJson.level;
|
||||
const actualType = type || 'other';
|
||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
||||
const data: logHtml = {
|
||||
type: actualType,
|
||||
content: remoteJson.message,
|
||||
color: color,
|
||||
time: '',
|
||||
};
|
||||
updateLogList(realtimeLogHtmlList, actualType, data);
|
||||
} else if (loadType === 'history') {
|
||||
const type = extractContent(line);
|
||||
const actualType = type || 'other';
|
||||
const timeRegex = /(\d{2}-\d{2} \d{2}:\d{2}:\d{2})|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
|
||||
const match = timeRegex.exec(line);
|
||||
let time = match ? match[0] : null;
|
||||
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
|
||||
const data: logHtml = {
|
||||
type: actualType,
|
||||
content: line.slice(match ? match[0].length : 0) || '',
|
||||
color: color,
|
||||
time: time ? time + ' ' : '',
|
||||
};
|
||||
updateLogList(historyLogHtmlList, actualType, data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateLogList = (logList: Ref<Map<string, Array<logHtml>>>, actualType: string, data: logHtml) => {
|
||||
const allLogs = logList.value.get('all');
|
||||
if (Array.isArray(allLogs)) {
|
||||
allLogs.push(data);
|
||||
}
|
||||
if (actualType !== 'other') {
|
||||
const typeLogs = logList.value.get(actualType);
|
||||
if (Array.isArray(typeLogs)) {
|
||||
typeLogs.push(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
const selectType = (key: string) => {
|
||||
LogDataType.value = key;
|
||||
};
|
||||
interface CustomURL extends URL {
|
||||
recycleObjectURL: (url: string) => void;
|
||||
}
|
||||
|
||||
const isCompatibleWithCustomURL = (obj: any): obj is CustomURL => {
|
||||
return typeof obj === 'object' && obj !== null && typeof (obj as any).recycleObjectURL === 'function';
|
||||
};
|
||||
|
||||
const recycleURL = (url: string) => {
|
||||
if (isCompatibleWithCustomURL(window.URL)) {
|
||||
const customURL = window.URL as CustomURL;
|
||||
customURL.recycleObjectURL(url);
|
||||
}
|
||||
};
|
||||
const generateTXT = (textContent: string, fileName: string) => {
|
||||
try {
|
||||
const blob = new Blob([textContent], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
recycleURL(url);
|
||||
} catch (error) {
|
||||
console.error('下载文本时出现错误:', error);
|
||||
}
|
||||
};
|
||||
const downloadText = () => {
|
||||
if (LogDataType.value === 'realtime') {
|
||||
const logs = realtimeLogHtmlList.value.get(optValue.value.description);
|
||||
if (logs && logs.length > 0) {
|
||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
||||
generateTXT(result, '实时日志');
|
||||
} else {
|
||||
MessagePlugin.error('暂无可下载日志');
|
||||
}
|
||||
} else {
|
||||
const logs = historyLogHtmlList.value.get(optValue.value.description);
|
||||
if (logs && logs.length > 0) {
|
||||
const result = logs.map((obj) => obj.content).join('\r\n');
|
||||
generateTXT(result, '历史日志');
|
||||
} else {
|
||||
MessagePlugin.error('暂无可下载日志');
|
||||
}
|
||||
}
|
||||
};
|
||||
const historyLog = async () => {
|
||||
value.value = [];
|
||||
visibleBody.value = true;
|
||||
const res = await logManager.GetLogList();
|
||||
clearAllLogs(historyLogHtmlList);
|
||||
if (res.length > 0) {
|
||||
logFileData.value = res.map((ele: string) => {
|
||||
return { label: ele, value: ele };
|
||||
});
|
||||
} else {
|
||||
logFileData.value = [];
|
||||
}
|
||||
};
|
||||
const GetLogList = async () => {
|
||||
if (value.value.length > 0) {
|
||||
for (const ele of value.value) {
|
||||
try {
|
||||
const data = await logManager.GetLog(ele);
|
||||
if (data && data !== 'null') {
|
||||
loadData(data, 'history');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取日志 ${ele} 时出现错误:`, error);
|
||||
}
|
||||
}
|
||||
visibleBody.value = false;
|
||||
} else {
|
||||
MessagePlugin.error('请选择日志');
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRealTimeLogs = async () => {
|
||||
eventSource.value = await logManager.getRealTimeLogs();
|
||||
if (eventSource.value) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-expect-error
|
||||
eventSource.value.onmessage = (event: MessageEvent) => {
|
||||
console.log(event.data)
|
||||
loadData(event.data, 'realtime');
|
||||
};
|
||||
}
|
||||
};
|
||||
const closeRealTimeLogs = async () => {
|
||||
if (eventSource.value) {
|
||||
eventSource.value.close();
|
||||
}
|
||||
};
|
||||
const scrollToBottom = () => {
|
||||
if (!isMouseEntered) {
|
||||
nextTick(() => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.scrollTop = contentBox.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const observeDOMChanges = () => {
|
||||
if (contentBox.value) {
|
||||
const observer = new MutationObserver(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
observer.observe(contentBox.value, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
const showScrollbar = () => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'auto';
|
||||
}
|
||||
};
|
||||
const hideScrollbar = () => {
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'hidden';
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
realtimeLogHtmlList,
|
||||
() => {
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
historyLogHtmlList,
|
||||
() => {
|
||||
if (!isMouseEntered) {
|
||||
scrollToBottom();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fetchRealTimeLogs();
|
||||
startTimer();
|
||||
contentBox.value = document.querySelector('.content');
|
||||
if (contentBox.value) {
|
||||
contentBox.value.style.overflow = 'hidden';
|
||||
contentBox.value.addEventListener('mouseenter', () => {
|
||||
isMouseEntered = true;
|
||||
showScrollbar();
|
||||
pauseTimer();
|
||||
});
|
||||
contentBox.value.addEventListener('mouseleave', () => {
|
||||
isMouseEntered = false;
|
||||
hideScrollbar();
|
||||
resumeTimer();
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 1000);
|
||||
});
|
||||
observeDOMChanges();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
closeRealTimeLogs();
|
||||
stopTimer();
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
|
||||
.tab-box {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 56vh;
|
||||
background-image: url('@/assets/logo.png');
|
||||
border: 1px solid #ddd6d6 !important;
|
||||
padding: 5px 10px;
|
||||
text-align: left;
|
||||
overflow-y: auto;
|
||||
margin-top: -10px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.content span {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@keyframes fadeInOnce {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutOnce {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content div {
|
||||
animation: fadeInOnce 0.5s forwards;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #888888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.t-tag {
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#debug {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%);
|
||||
}
|
||||
|
||||
#info {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%);
|
||||
}
|
||||
|
||||
#warn {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(225deg, #e14fad 0%, #f9d423 48%, #e37318 100%);
|
||||
}
|
||||
|
||||
#error {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%);
|
||||
}
|
||||
|
||||
#fatal {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to right, #fd0700, #ec567f);
|
||||
}
|
||||
|
||||
#other {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%);
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.content {
|
||||
height: 50vh;
|
||||
font-family: ProtoNerdFontItalic, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 14.3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.card {
|
||||
padding: 5px 10px 20px 10px !important;
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.card {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,122 +1,344 @@
|
||||
<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 ref="headerBox" class="title">
|
||||
<t-divider content="网络配置" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<wifi1-icon />
|
||||
<div style="margin-left: 5px">网络配置</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
<t-divider align="right">
|
||||
<t-button @click="addConfig()">
|
||||
<template #icon><add-icon /></template>
|
||||
添加配置</t-button>
|
||||
</t-divider>
|
||||
</div>
|
||||
<div v-if="loadPage" ref="setting" class="setting">
|
||||
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
|
||||
<t-tab-panel value="all" label="全部"></t-tab-panel>
|
||||
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="httpSseServers" label="HTTP SSE 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
|
||||
<t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
|
||||
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
|
||||
|
||||
</t-tabs>
|
||||
</div>
|
||||
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
|
||||
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative"></div>
|
||||
</t-loading>
|
||||
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
|
||||
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
|
||||
<div v-for="(item, index) in cardConfig" :key="index">
|
||||
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
|
||||
:header-bordered="true" class="setting-card">
|
||||
<template #actions>
|
||||
<t-space>
|
||||
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
|
||||
<t-popconfirm content="确认删除" @confirm="delConfig(item)">
|
||||
<delete-icon size="20px"></delete-icon>
|
||||
</t-popconfirm>
|
||||
</t-space>
|
||||
</template>
|
||||
<div class="setting-content">
|
||||
<t-card class="card-address" :style="{
|
||||
borderLeft:
|
||||
'7px solid ' + (item.enable ? 'var(--td-success-color)' : 'var(--td-error-color)'),
|
||||
}">
|
||||
<div class="local-box" v-if="item.host && item.port">
|
||||
<server-filled-icon class="local-icon" size="20px"
|
||||
@click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<strong class="local">{{ item.host }}:{{ item.port }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px"
|
||||
@click="copyText(item.host + ':' + item.port)"></copy-icon>
|
||||
</div>
|
||||
<div class="local-box" v-if="item.url">
|
||||
<server-filled-icon class="local-icon" size="20px"
|
||||
@click="toggleProperty(item, 'enable')"></server-filled-icon>
|
||||
<strong class="local">{{ item.url }}</strong>
|
||||
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
|
||||
</div>
|
||||
</t-card>
|
||||
<t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
|
||||
<t-collapse-panel header="基础信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions-item v-if="item.token" label="连接密钥">
|
||||
<div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
|
||||
<span>{{ showToken ? item.token : '******' }}</span>
|
||||
<browse-icon class="browse-icon" v-if="showToken" size="18px"
|
||||
@click="showToken = false"></browse-icon>
|
||||
<browse-off-icon class="browse-icon" v-else size="18px"
|
||||
@click="showToken = true"></browse-off-icon>
|
||||
</div>
|
||||
<div v-else>
|
||||
<t-popup :showArrow="true" trigger="click">
|
||||
<t-tag theme="primary">点击查看</t-tag>
|
||||
<template #content>
|
||||
<div @click="copyText(item.token)">{{ item.token }}</div>
|
||||
</template>
|
||||
</t-popup>
|
||||
</div>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item label="消息格式">{{
|
||||
item.messagePostFormat
|
||||
}}</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
<t-collapse-panel header="状态信息">
|
||||
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'"
|
||||
class="setting-base-info">
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
|
||||
<t-tag :class="item.debug ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'debug')">
|
||||
{{ item.debug ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
|
||||
label="Websocket 功能">
|
||||
<t-tag :class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableWebsocket')">
|
||||
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
|
||||
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableCors')">
|
||||
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="上报自身消息">
|
||||
<t-tag :class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'reportSelfMessage')">
|
||||
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
|
||||
label="强制推送事件">
|
||||
<t-tag class="tag-item"
|
||||
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
|
||||
@click="toggleProperty(item, 'enableForcePushEvent')">
|
||||
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag>
|
||||
</t-descriptions-item>
|
||||
</t-descriptions>
|
||||
</t-collapse-panel>
|
||||
</t-collapse>
|
||||
</div>
|
||||
</t-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-card>
|
||||
</div>
|
||||
<div style="height: 20vh"></div>
|
||||
</div>
|
||||
<t-card v-else>
|
||||
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
||||
</t-card>
|
||||
</div>
|
||||
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
|
||||
:show-in-attached-element="true" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog__position">
|
||||
<div slot="body" class="dialog-body">
|
||||
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
|
||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
|
||||
label="名称" name="name">
|
||||
<t-input v-model="newTab.name" />
|
||||
</t-form-item>
|
||||
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
|
||||
<t-select v-model="newTab.type">
|
||||
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
|
||||
label="类型" name="type">
|
||||
<t-select v-model="newTab.type" @change="onloadDefault">
|
||||
<t-option value="httpServers">HTTP 服务器</t-option>
|
||||
<t-option value="httpSseServers">HTTP SSE 服务器</t-option>
|
||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
||||
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
||||
</t-select>
|
||||
</t-form-item>
|
||||
<div>
|
||||
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||
:config="newTab.data" />
|
||||
</div>
|
||||
</t-form>
|
||||
</t-dialog>
|
||||
</t-space>
|
||||
</div>
|
||||
</t-dialog>
|
||||
</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,
|
||||
AddIcon,
|
||||
DeleteIcon,
|
||||
Edit2Icon,
|
||||
ServerFilledIcon,
|
||||
CopyIcon,
|
||||
BrowseOffIcon,
|
||||
BrowseIcon,
|
||||
Wifi1Icon,
|
||||
} from 'tdesign-icons-vue-next';
|
||||
import {
|
||||
loadConfig as loadConfigOnebot,
|
||||
NetworkAdapterConfig,
|
||||
NetworkConfigKey,
|
||||
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';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
import { onMounted, onUnmounted, ref, watch, resolveDynamicComponent } from 'vue';
|
||||
import emitter from '@/ts/event-bus';
|
||||
import HttpSseServerComponent from './network/HttpSseServerComponent.vue';
|
||||
|
||||
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
|
||||
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
|
||||
type ComponentUnion =
|
||||
const showToken = ref<boolean>(false);
|
||||
const infoOneCol = ref<boolean>(true);
|
||||
const tabsWidth = ref<number>(0);
|
||||
const menuWidth = ref<number>(0);
|
||||
const cardWidth = ref<number>(0);
|
||||
const cardHeight = ref<number>(0);
|
||||
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
|
||||
const largeScreen = window.matchMedia('(min-width: 1025px)');
|
||||
const headerBox = ref<HTMLDivElement | null>(null);
|
||||
const setting = ref<HTMLDivElement | null>(null);
|
||||
const loadPage = ref<boolean>(false);
|
||||
const visibleBody = ref<boolean>(false);
|
||||
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
|
||||
const dialogTitle = ref<string>('');
|
||||
|
||||
type ComponentKey = Exclude<NetworkConfigKey, 'plugins'>
|
||||
const componentMap: Record<
|
||||
ComponentKey,
|
||||
| typeof HttpServerComponent
|
||||
| typeof HttpClientComponent
|
||||
| typeof WebsocketServerComponent
|
||||
| typeof WebsocketClientComponent;
|
||||
|
||||
const componentMap: Record<ConfigKey, ComponentUnion> = {
|
||||
| typeof WebsocketClientComponent
|
||||
| typeof HttpSseServerComponent
|
||||
> = {
|
||||
httpServers: HttpServerComponent,
|
||||
httpSseServers: HttpSseServerComponent,
|
||||
httpClients: HttpClientComponent,
|
||||
websocketServers: WebsocketServerComponent,
|
||||
websocketClients: WebsocketClientComponent,
|
||||
};
|
||||
|
||||
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
|
||||
httpServers: httpServerDefaultConfigs,
|
||||
httpClients: httpClientDefaultConfigs,
|
||||
websocketServers: websocketServerDefaultConfigs,
|
||||
websocketClients: websocketClientDefaultConfigs,
|
||||
//操作类型
|
||||
const operateType = ref<string>('');
|
||||
//配置项索引
|
||||
const configIndex = ref<number>(0);
|
||||
//保存时所用数据
|
||||
const networkConfig: { [key: string]: any } = {
|
||||
websocketClients: [],
|
||||
websocketServers: [],
|
||||
httpSseServers: [],
|
||||
httpClients: [],
|
||||
httpServers: [],
|
||||
};
|
||||
|
||||
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) => {
|
||||
//挂载的数据
|
||||
const WebConfg = ref(
|
||||
new Map<string, Array<null>>([
|
||||
['all', []],
|
||||
['httpServers', []],
|
||||
['httpClients', []],
|
||||
['httpSseServers', []],
|
||||
['websocketServers', []],
|
||||
['websocketClients', []],
|
||||
])
|
||||
);
|
||||
const typeCh: Record<ComponentKey, string> = {
|
||||
httpServers: 'HTTP 服务器',
|
||||
httpClients: 'HTTP 客户端',
|
||||
httpSseServers: 'HTTP SSE 服务器',
|
||||
websocketServers: 'WebSocket 服务器',
|
||||
websocketClients: 'WebSocket 客户端',
|
||||
};
|
||||
const cardConfig = ref<any>([]);
|
||||
const getComponent = (type: ComponentKey) => {
|
||||
return componentMap[type];
|
||||
};
|
||||
const getKeyByValue = (obj: typeof typeCh, value: string): string | undefined => {
|
||||
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
|
||||
};
|
||||
|
||||
const addConfig = () => {
|
||||
dialogTitle.value = '添加配置';
|
||||
newTab.value = { name: '', data: {}, type: '' };
|
||||
operateType.value = 'add';
|
||||
visibleBody.value = true;
|
||||
};
|
||||
|
||||
const editConfig = (item: any) => {
|
||||
dialogTitle.value = '编辑配置';
|
||||
newTab.value = { name: item.name, data: { ...item }, type: getKeyByValue(typeCh, item.type) || '' };
|
||||
operateType.value = 'edit';
|
||||
visibleBody.value = true;
|
||||
};
|
||||
|
||||
const toggleProperty = async (item: any, tagData: string) => {
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
const newData = { ...item };
|
||||
newData[tagData] = !item[tagData];
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: newData, type: type };
|
||||
}
|
||||
operateType.value = 'edit';
|
||||
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||
await saveConfig();
|
||||
};
|
||||
|
||||
const delConfig = (item: any) => {
|
||||
const type = getKeyByValue(typeCh, item.type);
|
||||
if (type) {
|
||||
newTab.value = { name: item.name, data: item, type: type };
|
||||
}
|
||||
configIndex.value = configIndex.value = networkConfig[newTab.value.type].findIndex(
|
||||
(obj: any) => obj.name === item.name
|
||||
);
|
||||
operateType.value = 'delete';
|
||||
saveConfig();
|
||||
};
|
||||
|
||||
const selectType = (key: ComponentKey) => {
|
||||
cardConfig.value = WebConfg.value.get(key) || [];
|
||||
};
|
||||
|
||||
const onloadDefault = (key: ComponentKey) => {
|
||||
newTab.value.data = {};
|
||||
};
|
||||
//检测重名
|
||||
const checkName = (name: string) => {
|
||||
const allConfigs = WebConfg.value.get('all')?.findIndex((obj: any) => obj.name === name);
|
||||
if (newTab.value.name === '' || newTab.value.type === '') {
|
||||
MessagePlugin.error('请填写完整信息');
|
||||
return false;
|
||||
} else if (allConfigs === -1 || newTab.value.data.name === name) {
|
||||
return true;
|
||||
} else {
|
||||
MessagePlugin.error('名称已存在');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
//保存
|
||||
const saveConfig = async () => {
|
||||
if (operateType.value == 'add') {
|
||||
if (!checkName(newTab.value.name)) return;
|
||||
newTab.value.data.name = newTab.value.name;
|
||||
networkConfig[newTab.value.type].push(newTab.value.data);
|
||||
} else if (operateType.value == 'edit') {
|
||||
if (!checkName(newTab.value.name)) return;
|
||||
newTab.value.data.name = newTab.value.name;
|
||||
networkConfig[newTab.value.type][configIndex.value] = newTab.value.data;
|
||||
} else if (operateType.value == 'delete') {
|
||||
networkConfig[newTab.value.type].splice(configIndex.value, 1);
|
||||
}
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
userConfig.network = networkConfig as any;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
operateType.value = '';
|
||||
configIndex.value = 0;
|
||||
MessagePlugin.success('配置保存成功');
|
||||
await loadConfig();
|
||||
visibleBody.value = false;
|
||||
} else {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
@@ -137,113 +359,245 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
||||
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 getAllData = (data: { [key: string]: Array<NetworkAdapterConfig> }) => {
|
||||
cardConfig.value = [];
|
||||
WebConfg.value.set('all', []);
|
||||
for (const key in data) {
|
||||
const configs = data[key as keyof NetworkAdapterConfig];
|
||||
if (key in networkConfig) {
|
||||
networkConfig[key] = [...configs];
|
||||
const newConfigsArray = configs.map((config: any) => ({
|
||||
...config,
|
||||
type: typeCh[key as ComponentKey],
|
||||
}));
|
||||
WebConfg.value.set(key, newConfigsArray);
|
||||
const allConfigs = WebConfg.value.get('all');
|
||||
if (allConfigs) {
|
||||
const newAllConfigs = [...allConfigs, ...newConfigsArray];
|
||||
WebConfg.value.set('all', newAllConfigs);
|
||||
}
|
||||
cardConfig.value = WebConfg.value.get('all');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const userConfig = await getOB11Config();
|
||||
if (!userConfig) return;
|
||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||
addConfigDataToPanel(mergedConfig.network);
|
||||
const mergedConfig = loadConfigOnebot(userConfig);
|
||||
getAllData(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;
|
||||
const copyText = async (text: string) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
MessagePlugin.success('复制成功');
|
||||
} catch (err) {
|
||||
console.error('复制失败', err);
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
userConfig.network = config;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
await MessagePlugin.success('配置保存成功');
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
|
||||
if (mediumScreen.matches) {
|
||||
cardWidth.value = (tabsWidth.value - 20) / 2;
|
||||
} else if (largeScreen.matches) {
|
||||
cardWidth.value = (tabsWidth.value - 40) / 3;
|
||||
} else {
|
||||
await MessagePlugin.error('配置保存失败');
|
||||
cardWidth.value = tabsWidth.value;
|
||||
}
|
||||
loadPage.value = true;
|
||||
setTimeout(() => {
|
||||
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
|
||||
}, 300)
|
||||
};
|
||||
|
||||
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;
|
||||
emitter.on('sendWidth', (width) => {
|
||||
if (typeof width === 'string') {
|
||||
const strWidth = width as string;
|
||||
menuWidth.value = parseInt(strWidth);
|
||||
}
|
||||
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();
|
||||
};
|
||||
|
||||
});
|
||||
watch(menuWidth, (newValue, oldValue) => {
|
||||
loadPage.value = false;
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 300)
|
||||
});
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const cachedWidth = localStorage.getItem('menuWidth');
|
||||
if (cachedWidth) {
|
||||
menuWidth.value = parseInt(cachedWidth);
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 300)
|
||||
}
|
||||
window.addEventListener('resize', () => {
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 300)
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', () => {
|
||||
setTimeout(() => {
|
||||
handleResize();
|
||||
}, 300)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.full-space {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.full-tabs {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.setting {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.full-tab-panel {
|
||||
.setting-box {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.setting-card {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-address svg {
|
||||
fill: var(--td-brand-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.local-box {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.local-icon {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
.local {
|
||||
flex: 6;
|
||||
margin: 0 10px 0 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.token-view {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.token-view span {
|
||||
flex: 5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-item-on {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
|
||||
}
|
||||
|
||||
.tag-item-off {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
|
||||
}
|
||||
|
||||
.browse-icon {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
:global(.t-dialog__ctx .t-dialog__position) {
|
||||
padding: 48px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 786px) {
|
||||
.setting-box {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card-box {
|
||||
margin: 10px 20px 0 20px;
|
||||
}
|
||||
|
||||
.card-none {
|
||||
line-height: 400px !important;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.setting-card .t-card__title {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.setting-card .t-card__description {
|
||||
margin-bottom: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.setting-base-info .t-descriptions__header {
|
||||
font-size: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.setting-base-info .t-descriptions__label {
|
||||
padding: 0 var(--td-comp-paddingLR-l) !important;
|
||||
}
|
||||
|
||||
.setting-base-info tr>td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-coll .t-collapse-panel__wrapper .t-collapse-panel__content {
|
||||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l);
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,22 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-divider content="其余配置" align="left" />
|
||||
<div class="title">
|
||||
<t-divider content="其余配置" align="left">
|
||||
<template #content>
|
||||
<div style="display: flex; justify-content: center; align-items: center">
|
||||
<setting-icon />
|
||||
<div style="margin-left: 5px">其余配置</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-divider>
|
||||
</div>
|
||||
<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>
|
||||
<t-card class="card">
|
||||
<div class="other-config-container">
|
||||
<div class="other-config">
|
||||
<t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
|
||||
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
||||
<t-input v-model="otherConfig.musicSignUrl" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
||||
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
|
||||
<t-switch v-model="otherConfig.parseMultMsg" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
<div class="button-container">
|
||||
<t-button @click="saveConfig">保存</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -24,12 +36,15 @@ import { ref, onMounted } from 'vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
import { SettingIcon } from 'tdesign-icons-vue-next';
|
||||
|
||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||
musicSignUrl: '',
|
||||
enableLocalFile2Url: false,
|
||||
parseMultMsg: true,
|
||||
});
|
||||
|
||||
const labelAlign = ref<string>();
|
||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||
const storedCredential = localStorage.getItem('auth');
|
||||
if (!storedCredential) {
|
||||
@@ -56,6 +71,7 @@ const loadConfig = async () => {
|
||||
if (userConfig) {
|
||||
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
||||
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
||||
otherConfig.value.parseMultMsg = userConfig.parseMultMsg;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading config:', error);
|
||||
@@ -68,6 +84,7 @@ const saveConfig = async () => {
|
||||
if (userConfig) {
|
||||
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
||||
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
||||
userConfig.parseMultMsg = otherConfig.value.parseMultMsg ?? true;
|
||||
const success = await setOB11Config(userConfig);
|
||||
if (success) {
|
||||
MessagePlugin.success('配置保存成功');
|
||||
@@ -80,55 +97,60 @@ const saveConfig = async () => {
|
||||
MessagePlugin.error('配置保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig();
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
const handleMediaChange = (e: MediaQueryListEvent) => {
|
||||
if (e.matches) {
|
||||
labelAlign.value = 'top';
|
||||
} else {
|
||||
labelAlign.value = 'left';
|
||||
}
|
||||
};
|
||||
mediaQuery.addEventListener('change', handleMediaChange);
|
||||
const event = new Event('change');
|
||||
Object.defineProperty(event, 'matches', {
|
||||
value: mediaQuery.matches,
|
||||
writable: false,
|
||||
});
|
||||
mediaQuery.dispatchEvent(event);
|
||||
return () => {
|
||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
padding: 20px 20px 0 20px;
|
||||
}
|
||||
.card {
|
||||
margin: 0 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.other-config-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.other-config {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
max-width: 500px;
|
||||
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,22 +0,0 @@
|
||||
<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>
|
@@ -1,68 +1,61 @@
|
||||
<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>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpClientConfig = {
|
||||
name: 'http-client',
|
||||
enable: false,
|
||||
url: 'http://localhost:8080',
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
token: '',
|
||||
debug: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpClientConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.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>
|
||||
<style scoped></style>
|
@@ -1,74 +1,69 @@
|
||||
<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>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 CORS">
|
||||
<t-switch v-model="config.enableCors" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 WS">
|
||||
<t-switch v-model="config.enableWebsocket" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpServerConfig = {
|
||||
name: 'http-server',
|
||||
enable: false,
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
messagePostFormat: 'array',
|
||||
token: '',
|
||||
debug: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.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>
|
||||
<style scoped></style>
|
73
napcat.webui/src/pages/network/HttpSseServerComponent.vue
Normal file
73
napcat.webui/src/pages/network/HttpSseServerComponent.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 CORS">
|
||||
<t-switch v-model="config.enableCors" />
|
||||
</t-form-item>
|
||||
<t-form-item label="启用 WS">
|
||||
<t-switch v-model="config.enableWebsocket" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" type="text" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { HttpSseServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: HttpSseServerConfig = {
|
||||
name: 'http-sse-server',
|
||||
enable: false,
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
enableCors: true,
|
||||
enableWebsocket: true,
|
||||
messagePostFormat: 'array',
|
||||
token: '',
|
||||
debug: false,
|
||||
reportSelfMessage: false
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: HttpSseServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
config.value.messagePostFormat = 'array';
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@@ -1,71 +1,66 @@
|
||||
<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>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="URL">
|
||||
<t-input v-model="config.url" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="报告自身消息">
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: WebsocketClientConfig = {
|
||||
name: 'websocket-client',
|
||||
enable: false,
|
||||
url: 'ws://localhost:8082',
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
reconnectInterval: 5000,
|
||||
token: '',
|
||||
debug: false,
|
||||
heartInterval: 30000
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketClientConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.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>
|
||||
<style scoped></style>
|
@@ -1,77 +1,73 @@
|
||||
<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>
|
||||
<t-form labelAlign="left">
|
||||
<t-form-item label="启用">
|
||||
<t-switch v-model="config.enable" />
|
||||
</t-form-item>
|
||||
<t-form-item label="主机">
|
||||
<t-input v-model="config.host" />
|
||||
</t-form-item>
|
||||
<t-form-item label="端口">
|
||||
<t-input v-model.number="config.port" type="number" />
|
||||
</t-form-item>
|
||||
<t-form-item label="消息格式">
|
||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||
</t-form-item>
|
||||
<t-form-item label="上报自身消息">
|
||||
<t-switch v-model="config.reportSelfMessage" />
|
||||
</t-form-item>
|
||||
<t-form-item label="Token">
|
||||
<t-input v-model="config.token" />
|
||||
</t-form-item>
|
||||
<t-form-item label="强制推送事件">
|
||||
<t-switch v-model="config.enableForcePushEvent" />
|
||||
</t-form-item>
|
||||
<t-form-item label="调试模式">
|
||||
<t-switch v-model="config.debug" />
|
||||
</t-form-item>
|
||||
<t-form-item label="心跳间隔">
|
||||
<t-input v-model.number="config.heartInterval" type="number" />
|
||||
</t-form-item>
|
||||
</t-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||
|
||||
const defaultConfig: WebsocketServerConfig = {
|
||||
name: 'websocket-server',
|
||||
enable: false,
|
||||
host: '0.0.0.0',
|
||||
port: 3001,
|
||||
messagePostFormat: 'array',
|
||||
reportSelfMessage: false,
|
||||
token: '',
|
||||
enableForcePushEvent: true,
|
||||
debug: false,
|
||||
heartInterval: 30000
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
config: WebsocketServerConfig;
|
||||
}>();
|
||||
|
||||
const config = ref(Object.assign({}, defaultConfig, props.config));
|
||||
|
||||
const messageFormatOptions = ref([
|
||||
{ label: 'Array', value: 'array' },
|
||||
{ label: 'String', value: 'string' },
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.config.messagePostFormat,
|
||||
() => config.value.messagePostFormat,
|
||||
(newValue) => {
|
||||
if (newValue !== 'array' && newValue !== 'string') {
|
||||
props.config.messagePostFormat = 'array';
|
||||
config.value.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>
|
||||
<style scoped></style>
|
@@ -7,6 +7,8 @@ import NetWork from '../pages/NetWork.vue';
|
||||
import QQLogin from '../components/QQLogin.vue';
|
||||
import WebUiLogin from '../components/WebUiLogin.vue';
|
||||
import OtherConfig from '../pages/OtherConfig.vue';
|
||||
import { MessagePlugin } from 'tdesign-vue-next';
|
||||
import { QQLoginManager } from '@/backend/shell';
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: '/', redirect: '/webui' },
|
||||
@@ -26,7 +28,27 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const isPublicRoute = ['/webui', '/qqlogin'].includes(to.path);
|
||||
const token = localStorage.getItem('auth');
|
||||
|
||||
if (!isPublicRoute) {
|
||||
if (!token) {
|
||||
MessagePlugin.error('请先登录');
|
||||
return next('/webui');
|
||||
}
|
||||
const login = await new QQLoginManager(token).checkWebUiLogined();
|
||||
if (!login) {
|
||||
MessagePlugin.error('请先登录');
|
||||
return next('/webui');
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
3
napcat.webui/src/ts/event-bus.ts
Normal file
3
napcat.webui/src/ts/event-bus.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import mitt from 'mitt';
|
||||
const emitter = mitt();
|
||||
export default emitter;
|
@@ -3,22 +3,15 @@
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "vue",
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"baseUrl": ".",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"types": ["vite/client"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
@@ -30,5 +23,5 @@
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [{"path": "./tsconfig.node.json"}]
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
24
package.json
24
package.json
@@ -2,27 +2,32 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.1.12",
|
||||
"version": "4.4.1",
|
||||
"scripts": {
|
||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
||||
"build:webui": "cd napcat.webui && vite build",
|
||||
"dev:universal": "vite build --mode universal",
|
||||
"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",
|
||||
"dev:depend": "npm i && cd napcat.webui && npm i"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.24.0",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@eslint/compat": "^1.2.2",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@log4js-node/log4js-api": "^1.0.2",
|
||||
"@napneko/nap-proto-core": "^0.0.4",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@sinclair/typebox": "^0.34.9",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"@types/node": "^22.0.1",
|
||||
@@ -32,19 +37,18 @@
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"ajv": "^8.13.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"commander": "^12.1.0",
|
||||
"commander": "^13.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"fast-xml-parser": "^4.3.6",
|
||||
"file-type": "^19.0.0",
|
||||
"file-type": "^20.0.0",
|
||||
"globals": "^15.12.0",
|
||||
"image-size": "^1.1.1",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"vite": "^5.2.6",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-cp": "^4.0.8",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"winston": "^3.17.0"
|
||||
@@ -52,9 +56,9 @@
|
||||
"dependencies": {
|
||||
"express": "^5.0.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"piscina": "^4.7.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"silk-wasm": "^3.6.1",
|
||||
"ws": "^8.18.0",
|
||||
"piscina": "^4.7.0"
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
@@ -96,7 +96,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.logError.bind(logger)('convert silk failed', error.stack);
|
||||
logger.logError('convert silk failed', error.stack);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
107
src/common/cancel-task.ts
Normal file
107
src/common/cancel-task.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void;
|
||||
|
||||
export class CancelableTask<T> {
|
||||
private promise: Promise<T>;
|
||||
private cancelCallback: (() => void) | null = null;
|
||||
private isCanceled = false;
|
||||
private cancelListeners: Array<() => void> = [];
|
||||
|
||||
constructor(executor: TaskExecutor<T>) {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
const onCancel = (callback: () => void) => {
|
||||
this.cancelCallback = callback;
|
||||
};
|
||||
|
||||
executor(
|
||||
(value) => {
|
||||
if (!this.isCanceled) {
|
||||
resolve(value);
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
if (!this.isCanceled) {
|
||||
reject(reason);
|
||||
}
|
||||
},
|
||||
onCancel
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public cancel() {
|
||||
if (this.cancelCallback) {
|
||||
this.cancelCallback();
|
||||
}
|
||||
this.isCanceled = true;
|
||||
this.cancelListeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
public isTaskCanceled(): boolean {
|
||||
return this.isCanceled;
|
||||
}
|
||||
|
||||
public onCancel(listener: () => void) {
|
||||
this.cancelListeners.push(listener);
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
|
||||
): Promise<T | TResult> {
|
||||
return this.promise.catch(onrejected);
|
||||
}
|
||||
|
||||
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
|
||||
return this.promise.finally(onfinally);
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
next: () => this.promise.then(value => ({ value, done: true })),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function demoAwait() {
|
||||
const executor: TaskExecutor<number> = (resolve, reject, onCancel) => {
|
||||
let count = 0;
|
||||
const intervalId = setInterval(() => {
|
||||
count++;
|
||||
console.log(`Task is running... Count: ${count}`);
|
||||
if (count === 5) {
|
||||
clearInterval(intervalId);
|
||||
resolve(count);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
onCancel(() => {
|
||||
clearInterval(intervalId);
|
||||
console.log('Task has been canceled.');
|
||||
reject(new Error('Task was canceled'));
|
||||
});
|
||||
};
|
||||
|
||||
const task = new CancelableTask(executor);
|
||||
|
||||
task.onCancel(() => {
|
||||
console.log('Cancel listener triggered.');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
task.cancel(); // 取消任务
|
||||
}, 6000);
|
||||
|
||||
try {
|
||||
const result = await task;
|
||||
console.log(`Task completed with result: ${result}`);
|
||||
} catch (error) {
|
||||
console.error('Task failed:', error);
|
||||
}
|
||||
}
|
@@ -33,27 +33,27 @@ export abstract class ConfigBase<T> {
|
||||
}
|
||||
|
||||
read(copy_default: boolean = true): T {
|
||||
const logger = this.core.context.logger;
|
||||
|
||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||
if (!fs.existsSync(configPath) && copy_default) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||
this.core.context.logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||
} catch (e: any) {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
this.core.context.logger.logError(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
|
||||
}
|
||||
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||
fs.writeFileSync(configPath, '{}');
|
||||
}
|
||||
try {
|
||||
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
|
||||
this.core.context.logger.logDebug(`[Core] [Config] 配置文件${configPath}加载`, this.configData);
|
||||
return this.configData;
|
||||
} catch (e: any) {
|
||||
if (e instanceof SyntaxError) {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
this.core.context.logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||
} else {
|
||||
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
this.core.context.logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||
}
|
||||
return {} as T;
|
||||
}
|
||||
@@ -61,14 +61,13 @@ export abstract class ConfigBase<T> {
|
||||
|
||||
|
||||
save(newConfigData: T = this.configData) {
|
||||
const logger = this.core.context.logger;
|
||||
const selfInfo = this.core.selfInfo;
|
||||
this.configData = newConfigData;
|
||||
const configPath = this.getConfigPath(selfInfo.uin);
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||
} catch (e: any) {
|
||||
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
src/common/decorator.ts
Normal file
22
src/common/decorator.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// decoratorAsyncMethod(this,function,wrapper)
|
||||
async function decoratorMethod<T, R>(
|
||||
target: T,
|
||||
method: () => Promise<R>,
|
||||
wrapper: (result: R) => Promise<any>,
|
||||
executeImmediately: boolean = true
|
||||
): Promise<any> {
|
||||
const execute = async () => {
|
||||
try {
|
||||
const result = await method.call(target);
|
||||
return wrapper(result);
|
||||
} catch (error) {
|
||||
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
};
|
||||
|
||||
if (executeImmediately) {
|
||||
return execute();
|
||||
} else {
|
||||
return execute;
|
||||
}
|
||||
}
|
43
src/common/fall-back.ts
Normal file
43
src/common/fall-back.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
type Handler<T> = () => T | Promise<T>;
|
||||
type Checker<T> = (result: T) => T | Promise<T>;
|
||||
|
||||
export class Fallback<T> {
|
||||
private handlers: Handler<T>[] = [];
|
||||
private checker: Checker<T>;
|
||||
|
||||
constructor(checker?: Checker<T>) {
|
||||
this.checker = checker || (async (result: T) => result);
|
||||
}
|
||||
|
||||
add(handler: Handler<T>): this {
|
||||
this.handlers.push(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
// 执行处理程序链
|
||||
async run(): Promise<T> {
|
||||
const errors: Error[] = [];
|
||||
for (const handler of this.handlers) {
|
||||
try {
|
||||
const result = await handler();
|
||||
const data = await this.checker(result);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
errors.push(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
throw new AggregateError(errors, 'All handlers failed');
|
||||
}
|
||||
}
|
||||
export class FallbackUtil {
|
||||
static boolchecker<T>(value: T, condition: boolean): T {
|
||||
if (condition) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error('Condition is false, throwing error');
|
||||
}
|
||||
}
|
||||
}
|
121
src/common/file-uuid.ts
Normal file
121
src/common/file-uuid.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Peer } from '@/core';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
class TimeBasedCache<K, V> {
|
||||
private cache = new Map<K, { value: V, timestamp: number, frequency: number }>();
|
||||
private keyList = new Set<K>();
|
||||
private operationCount = 0;
|
||||
|
||||
constructor(private maxCapacity: number, private ttl: number = 30 * 1000 * 60, private cleanupCount: number = 10) {}
|
||||
|
||||
public put(key: K, value: V): void {
|
||||
const timestamp = Date.now();
|
||||
const cacheEntry = { value, timestamp, frequency: 1 };
|
||||
this.cache.set(key, cacheEntry);
|
||||
this.keyList.add(key);
|
||||
this.operationCount++;
|
||||
if (this.keyList.size > this.maxCapacity) this.evict();
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
}
|
||||
|
||||
public get(key: K): V | undefined {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && Date.now() - entry.timestamp < this.ttl) {
|
||||
entry.timestamp = Date.now();
|
||||
entry.frequency++;
|
||||
this.operationCount++;
|
||||
if (this.operationCount >= this.cleanupCount) this.cleanup(this.cleanupCount);
|
||||
return entry.value;
|
||||
} else {
|
||||
this.deleteKey(key);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private cleanup(count: number): void {
|
||||
const currentTime = Date.now();
|
||||
let cleaned = 0;
|
||||
for (const key of this.keyList) {
|
||||
if (cleaned >= count) break;
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && currentTime - entry.timestamp >= this.ttl) {
|
||||
this.deleteKey(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
this.operationCount = 0; // 重置操作计数器
|
||||
}
|
||||
|
||||
private deleteKey(key: K): void {
|
||||
this.cache.delete(key);
|
||||
this.keyList.delete(key);
|
||||
}
|
||||
|
||||
private evict(): void {
|
||||
while (this.keyList.size > this.maxCapacity) {
|
||||
let oldestKey: K | undefined;
|
||||
let minFrequency = Infinity;
|
||||
for (const key of this.keyList) {
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.frequency < minFrequency) {
|
||||
minFrequency = entry.frequency;
|
||||
oldestKey = key;
|
||||
}
|
||||
}
|
||||
if (oldestKey !== undefined) this.deleteKey(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface FileUUIDData {
|
||||
peer: Peer;
|
||||
modelId?: string;
|
||||
fileId?: string;
|
||||
msgId?: string;
|
||||
elementId?: string;
|
||||
fileUUID?: string;
|
||||
}
|
||||
|
||||
class FileUUIDManager {
|
||||
private cache: TimeBasedCache<string, FileUUIDData>;
|
||||
|
||||
constructor(ttl: number) {
|
||||
this.cache = new TimeBasedCache<string, FileUUIDData>(5000, ttl);
|
||||
}
|
||||
|
||||
public encode(data: FileUUIDData, endString: string = "", customUUID?: string): string {
|
||||
const uuid = customUUID ? customUUID : randomUUID().replace(/-/g, '') + endString;
|
||||
this.cache.put(uuid, data);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public decode(uuid: string): FileUUIDData | undefined {
|
||||
return this.cache.get(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUIDWrap {
|
||||
private manager: FileUUIDManager;
|
||||
|
||||
constructor(ttl: number = 86400000) {
|
||||
this.manager = new FileUUIDManager(ttl);
|
||||
}
|
||||
|
||||
public encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = "", customUUID?: string): string {
|
||||
return this.manager.encode({ peer, modelId, fileId, fileUUID }, endString, customUUID);
|
||||
}
|
||||
|
||||
public decodeModelId(uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
|
||||
public encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", customUUID?: string): string {
|
||||
return this.manager.encode({ peer, msgId, elementId, fileUUID }, "", customUUID);
|
||||
}
|
||||
|
||||
public decode(uuid: string): FileUUIDData | undefined {
|
||||
return this.manager.decode(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
export const FileNapCatOneBotUUID = new FileNapCatOneBotUUIDWrap();
|
@@ -1,9 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import { stat } from 'fs/promises';
|
||||
import crypto, { randomUUID } from 'crypto';
|
||||
import util from 'util';
|
||||
import path from 'node:path';
|
||||
import * as fileType from 'file-type';
|
||||
import { solveProblem } from '@/common/helper';
|
||||
|
||||
export interface HttpDownloadOptions {
|
||||
@@ -15,18 +13,9 @@ type Uri2LocalRes = {
|
||||
success: boolean,
|
||||
errMsg: string,
|
||||
fileName: string,
|
||||
ext: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
export function isGIF(path: string) {
|
||||
const buffer = Buffer.alloc(4);
|
||||
const fd = fs.openSync(path, 'r');
|
||||
fs.readSync(fd, buffer, 0, 4, 0);
|
||||
fs.closeSync(fd);
|
||||
return buffer.toString() === 'GIF8';
|
||||
}
|
||||
|
||||
// 定义一个异步函数来检查文件是否存在
|
||||
export function checkFileExist(path: string, timeout: number = 3000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -81,27 +70,6 @@ async function checkFile(path: string): Promise<void> {
|
||||
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
||||
}
|
||||
|
||||
export async function file2base64(path: string) {
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const result = {
|
||||
err: '',
|
||||
data: '',
|
||||
};
|
||||
try {
|
||||
try {
|
||||
await checkFileExist(path, 5000);
|
||||
} catch (e: any) {
|
||||
result.err = e.toString();
|
||||
return result;
|
||||
}
|
||||
const data = await readFile(path);
|
||||
result.data = data.toString('base64');
|
||||
} catch (err: any) {
|
||||
result.err = err.toString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function calculateFileMD5(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建一个流式读取器
|
||||
@@ -168,20 +136,6 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
|
||||
return Buffer.from(buffer);
|
||||
}
|
||||
|
||||
export async function checkFileV2(filePath: string) {
|
||||
try {
|
||||
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
if (ext) {
|
||||
fs.renameSync(filePath, filePath + `.${ext}`);
|
||||
filePath += `.${ext}`;
|
||||
return { success: true, ext: ext, path: filePath };
|
||||
}
|
||||
} catch (e) {
|
||||
// log("获取文件类型失败", filePath,e.stack)
|
||||
}
|
||||
return { success: false, ext: '', path: filePath };
|
||||
}
|
||||
|
||||
export enum FileUriType {
|
||||
Unknown = 0,
|
||||
Local = 1,
|
||||
@@ -190,7 +144,6 @@ export enum FileUriType {
|
||||
}
|
||||
|
||||
export async function checkUriType(Uri: string) {
|
||||
|
||||
const LocalFileRet = await solveProblem((uri: string) => {
|
||||
if (fs.existsSync(uri)) {
|
||||
return { Uri: uri, Type: FileUriType.Local };
|
||||
@@ -199,23 +152,17 @@ export async function checkUriType(Uri: string) {
|
||||
}, Uri);
|
||||
if (LocalFileRet) return LocalFileRet;
|
||||
const OtherFileRet = await solveProblem((uri: string) => {
|
||||
//再判断是否是Http
|
||||
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
||||
// 再判断是否是Http
|
||||
if (uri.startsWith('http:') || uri.startsWith('https:')) {
|
||||
return { Uri: uri, Type: FileUriType.Remote };
|
||||
}
|
||||
//再判断是否是Base64
|
||||
if (uri.startsWith('base64://')) {
|
||||
// 再判断是否是Base64
|
||||
if (uri.startsWith('base64:')) {
|
||||
return { Uri: uri, Type: FileUriType.Base64 };
|
||||
}
|
||||
if (uri.startsWith('file://')) {
|
||||
let filePath: string;
|
||||
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
|
||||
if (process.platform === 'win32') {
|
||||
filePath = pathname.slice(1);
|
||||
} else {
|
||||
filePath = pathname;
|
||||
}
|
||||
|
||||
// 默认file://
|
||||
if (uri.startsWith('file:')) {
|
||||
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
|
||||
return { Uri: filePath, Type: FileUriType.Local };
|
||||
}
|
||||
if (uri.startsWith('data:')) {
|
||||
@@ -228,60 +175,34 @@ export async function checkUriType(Uri: string) {
|
||||
return { Uri: Uri, Type: FileUriType.Unknown };
|
||||
}
|
||||
|
||||
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> {
|
||||
export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
|
||||
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||
//解析失败
|
||||
const tempName = randomUUID();
|
||||
if (!filename) filename = randomUUID();
|
||||
//解析Http和Https协议
|
||||
|
||||
if (UriType == FileUriType.Unknown) {
|
||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||
}
|
||||
//解析File协议和本地文件
|
||||
if (UriType == FileUriType.Local) {
|
||||
const filePath = path.join(dir, filename);
|
||||
|
||||
switch (UriType) {
|
||||
case FileUriType.Local: {
|
||||
const fileExt = path.extname(HandledUri);
|
||||
let filename = path.basename(HandledUri, fileExt);
|
||||
filename += fileExt;
|
||||
//复制文件到临时文件并保持后缀
|
||||
const filenameTemp = tempName + fileExt;
|
||||
const filePath = path.join(dir, filenameTemp);
|
||||
fs.copyFileSync(HandledUri, filePath);
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
|
||||
const tempFilePath = path.join(dir, filename + fileExt);
|
||||
fs.copyFileSync(HandledUri, tempFilePath);
|
||||
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
|
||||
}
|
||||
//接下来都要有文件名
|
||||
|
||||
if (UriType == FileUriType.Remote) {
|
||||
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
|
||||
if (pathInfo.name) {
|
||||
const pathlen = 200 - dir.length - pathInfo.name.length;
|
||||
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
|
||||
if (pathInfo.ext) {
|
||||
filename += pathInfo.ext;
|
||||
}
|
||||
}
|
||||
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
|
||||
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
|
||||
const filePath = path.join(dir, tempName + fileExt);
|
||||
const buffer = await httpDownload(HandledUri);
|
||||
//没有文件就创建
|
||||
case FileUriType.Remote: {
|
||||
const buffer = await httpDownload({ url: HandledUri, headers: headers });
|
||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
//解析Base64
|
||||
if (UriType == FileUriType.Base64) {
|
||||
|
||||
case FileUriType.Base64: {
|
||||
const base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||
const buffer = Buffer.from(base64, 'base64');
|
||||
let filePath = path.join(dir, filename);
|
||||
let fileExt = '';
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
const { success, ext, path: fileTypePath } = await checkFileV2(filePath);
|
||||
if (success) {
|
||||
filePath = fileTypePath;
|
||||
fileExt = ext;
|
||||
filename = filename + '.' + ext;
|
||||
}
|
||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
||||
const base64Buffer = Buffer.from(base64, 'base64');
|
||||
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
|
||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||
}
|
||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'fs';
|
||||
import os from 'node:os';
|
||||
import { Peer, QQLevel } from '@/core';
|
||||
import { QQLevel } from '@/core';
|
||||
|
||||
export async function solveProblem<T extends (...arg: any[]) => any>(func: T, ...args: Parameters<T>): Promise<ReturnType<T> | undefined> {
|
||||
return new Promise<ReturnType<T> | undefined>((resolve) => {
|
||||
@@ -24,81 +24,6 @@ export async function solveAsyncProblem<T extends (...args: any[]) => Promise<an
|
||||
});
|
||||
}
|
||||
|
||||
export class FileNapCatOneBotUUID {
|
||||
static encodeModelId(peer: Peer, modelId: string, fileId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|ModelIdFile|${peer.chatType}|${peer.peerUid}|${modelId}|${fileId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);//储存data的hex长度
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decodeModelId(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
modelId: string,
|
||||
fileId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|ModelIdFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
modelId,
|
||||
fileId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
|
||||
static encode(peer: Peer, msgId: string, elementId: string, fileUUID: string = "", endString: string = ""): string {
|
||||
const data = `NapCatOneBot|MsgFile|${peer.chatType}|${peer.peerUid}|${msgId}|${elementId}|${fileUUID}`;
|
||||
//前四个字节塞data长度
|
||||
//一个字节8位 一个ascii字符1字节 一个hex字符4位 表示一个ascii字符需要两个hex字符
|
||||
const length = Buffer.alloc(4 + data.length);
|
||||
length.writeUInt32BE(data.length * 2, 0);
|
||||
length.write(data, 4);
|
||||
return length.toString('hex') + endString;
|
||||
}
|
||||
|
||||
static decode(uuid: string): undefined | {
|
||||
peer: Peer,
|
||||
msgId: string,
|
||||
elementId: string,
|
||||
fileUUID?: string
|
||||
} {
|
||||
//前四个字节是data长度
|
||||
const length = Buffer.from(uuid.slice(0, 8), 'hex').readUInt32BE(0);
|
||||
//根据length计算需要读取的长度
|
||||
const dataId = uuid.slice(8, 8 + length);
|
||||
//hex还原为string
|
||||
const realData = Buffer.from(dataId, 'hex').toString();
|
||||
if (!realData.startsWith('NapCatOneBot|MsgFile|')) return undefined;
|
||||
const data = realData.split('|');
|
||||
if (data.length < 6) return undefined; // compatibility requirement
|
||||
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
|
||||
return {
|
||||
peer: {
|
||||
chatType: +chatType,
|
||||
peerUid: peerUid,
|
||||
},
|
||||
msgId,
|
||||
elementId,
|
||||
fileUUID
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import winston, { format, transports } from 'winston';
|
||||
import { truncateString } from '@/common/helper';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
||||
|
||||
import EventEmitter from 'node:events';
|
||||
export enum LogLevel {
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
@@ -24,6 +24,36 @@ function getFormattedTimestamp() {
|
||||
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
|
||||
}
|
||||
|
||||
const logEmitter = new EventEmitter();
|
||||
export type LogListener = (msg: string) => void;
|
||||
class Subscription {
|
||||
public static MAX_HISTORY = 100;
|
||||
public static history: string[] = [];
|
||||
|
||||
subscribe(listener: LogListener) {
|
||||
for (const history of Subscription.history) {
|
||||
try {
|
||||
listener(history);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
logEmitter.on('log', listener);
|
||||
}
|
||||
unsubscribe(listener: LogListener) {
|
||||
logEmitter.off('log', listener);
|
||||
}
|
||||
notify(msg: string) {
|
||||
logEmitter.emit('log', msg);
|
||||
if (Subscription.history.length >= Subscription.MAX_HISTORY) {
|
||||
Subscription.history.shift();
|
||||
}
|
||||
Subscription.history.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export const logSubscription = new Subscription();
|
||||
|
||||
export class LogWrapper {
|
||||
fileLogEnabled = true;
|
||||
consoleLogEnabled = true;
|
||||
@@ -47,7 +77,7 @@ export class LogWrapper {
|
||||
filename: logPath,
|
||||
level: 'debug',
|
||||
maxsize: 5 * 1024 * 1024, // 5MB
|
||||
maxFiles: 5
|
||||
maxFiles: 5,
|
||||
}),
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
@@ -56,9 +86,9 @@ export class LogWrapper {
|
||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
]
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.setLogSelfInfo({ nick: '', uid: '' });
|
||||
@@ -67,26 +97,20 @@ export class LogWrapper {
|
||||
|
||||
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 => {
|
||||
fs.readdir(logDir).then((files) => {
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(logDir, file);
|
||||
this.deleteOldLogFile(filePath, oneWeekAgo);
|
||||
});
|
||||
}).catch((err) => {
|
||||
this.logger.error('Failed to read log directory', err);
|
||||
});
|
||||
}
|
||||
|
||||
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err) {
|
||||
this.logger.error('Failed to get file stats', err);
|
||||
return;
|
||||
}
|
||||
fs.stat(filePath).then((stats) => {
|
||||
if (stats.mtime.getTime() < oneWeekAgo) {
|
||||
fs.unlink(filePath, err => {
|
||||
fs.unlink(filePath).catch((err) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
this.logger.warn(`File already deleted: ${filePath}`);
|
||||
@@ -98,6 +122,8 @@ export class LogWrapper {
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
this.logger.error('Failed to get file stats', err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,7 +137,7 @@ export class LogWrapper {
|
||||
});
|
||||
}
|
||||
|
||||
setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
|
||||
setLogSelfInfo(selfInfo: { nick: string; uid: string }) {
|
||||
const userInfo = `${selfInfo.nick}`;
|
||||
this.logger.defaultMeta = { userInfo };
|
||||
}
|
||||
@@ -135,14 +161,16 @@ export class LogWrapper {
|
||||
}
|
||||
|
||||
formatMsg(msg: any[]) {
|
||||
return msg.map(msgItem => {
|
||||
if (msgItem instanceof Error) {
|
||||
return msgItem.stack;
|
||||
} else if (typeof msgItem === 'object') {
|
||||
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
||||
}
|
||||
return msgItem;
|
||||
}).join(' ');
|
||||
return msg
|
||||
.map((msgItem) => {
|
||||
if (msgItem instanceof Error) {
|
||||
return msgItem.stack;
|
||||
} else if (typeof msgItem === 'object') {
|
||||
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
||||
}
|
||||
return msgItem;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
_log(level: LogLevel, ...args: any[]) {
|
||||
@@ -155,6 +183,7 @@ export class LogWrapper {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
|
||||
}
|
||||
logSubscription.notify(JSON.stringify({ level, message }));
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
@@ -282,13 +311,9 @@ function textElementToText(textElement: any): string {
|
||||
}
|
||||
|
||||
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})`
|
||||
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})`
|
||||
}]`;
|
||||
}
|
@@ -78,7 +78,7 @@ class MessageUniqueWrapper {
|
||||
private readonly msgDataMap: LimitedHashTable<string, number>;
|
||||
private readonly msgIdMap: LimitedHashTable<string, number>;
|
||||
|
||||
constructor(maxMap: number = 1000) {
|
||||
constructor(maxMap: number = 5000) {
|
||||
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);
|
||||
this.msgDataMap = new LimitedHashTable<string, number>(maxMap);
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import https from 'node:https';
|
||||
import http from 'node:http';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export class RequestUtil {
|
||||
// 适用于获取服务器下发cookies时获取,仅GET
|
||||
@@ -69,7 +68,7 @@ export class RequestUtil {
|
||||
// 'Content-Length': Buffer.byteLength(postData),
|
||||
// },
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = protocol.request(options, (res: any) => {
|
||||
const req = protocol.request(options, (res: http.IncomingMessage) => {
|
||||
let responseBody = '';
|
||||
res.on('data', (chunk: string | Buffer) => {
|
||||
responseBody += chunk.toString();
|
||||
@@ -112,24 +111,4 @@ export class RequestUtil {
|
||||
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
||||
}
|
||||
|
||||
static async createFormData(boundary: string, filePath: string): Promise<Buffer> {
|
||||
let type = 'image/png';
|
||||
if (filePath.endsWith('.jpg')) {
|
||||
type = 'image/jpeg';
|
||||
}
|
||||
const formDataParts = [
|
||||
`------${boundary}\r\n`,
|
||||
`Content-Disposition: form-data; name="share_image"; filename="${filePath}"\r\n`,
|
||||
'Content-Type: ' + type + '\r\n\r\n',
|
||||
];
|
||||
|
||||
const fileContent = readFileSync(filePath);
|
||||
const footer = `\r\n------${boundary}--`;
|
||||
return Buffer.concat([
|
||||
Buffer.from(formDataParts.join(''), 'utf8'),
|
||||
fileContent,
|
||||
Buffer.from(footer, 'utf8'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.1.12';
|
||||
export const napCatVersion = '4.4.1';
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { MsfChangeReasonType, MsfStatusType } from "../types/adapter";
|
||||
import { MsfChangeReasonType, MsfStatusType } from "@/core/types/adapter";
|
||||
|
||||
export class NodeIDependsAdapter {
|
||||
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
Peer,
|
||||
PicElement,
|
||||
PicSubType,
|
||||
PicType,
|
||||
RawMessage,
|
||||
SendFileElement,
|
||||
SendPicElement,
|
||||
@@ -17,16 +16,17 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import fsPromises from 'fs/promises';
|
||||
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
||||
import * as fileType from 'file-type';
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import imageSize from 'image-size';
|
||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
||||
import { RkeyManager } from '../helper/rkey';
|
||||
import { calculateFileMD5, isGIF } from '@/common/file';
|
||||
import { RkeyManager } from '@/core/helper/rkey';
|
||||
import { calculateFileMD5 } from '@/common/file';
|
||||
import pathLib from 'node:path';
|
||||
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { encodeSilk } from '@/common/audio';
|
||||
import { MessageContext } from '@/onebot/api';
|
||||
import { SendMessageContext } from '@/onebot/api';
|
||||
import { getFileTypeForSendType } from '../helper/msg';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
@@ -40,7 +40,7 @@ export class NTQQFileApi {
|
||||
this.rkeyManager = new RkeyManager([
|
||||
'https://rkey.napneko.icu/rkeys'
|
||||
],
|
||||
this.context.logger
|
||||
this.context.logger
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class NTQQFileApi {
|
||||
|
||||
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||
const fileMd5 = await calculateFileMD5(filePath);
|
||||
const extOrEmpty = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
|
||||
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||
let fileName = `${path.basename(filePath)}`;
|
||||
if (fileName.indexOf('.') === -1) {
|
||||
@@ -90,7 +90,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendFileElement(context: MessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
|
||||
async createValidSendFileElement(context: SendMessageContext, filePath: string, fileName: string = '', folderId: string = '',): Promise<SendFileElement> {
|
||||
const {
|
||||
fileName: _fileName,
|
||||
path,
|
||||
@@ -112,7 +112,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
|
||||
async createValidSendPicElement(context: SendMessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
|
||||
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||
if (fileSize === 0) {
|
||||
throw new Error('文件异常,大小为0');
|
||||
@@ -130,7 +130,7 @@ export class NTQQFileApi {
|
||||
fileName: fileName,
|
||||
sourcePath: path,
|
||||
original: true,
|
||||
picType: isGIF(picPath) ? PicType.NEWPIC_GIF : PicType.NEWPIC_JPEG,
|
||||
picType: await getFileTypeForSendType(picPath),
|
||||
picSubType: subType,
|
||||
fileUuid: '',
|
||||
fileSubId: '',
|
||||
@@ -140,8 +140,7 @@ export class NTQQFileApi {
|
||||
};
|
||||
}
|
||||
|
||||
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
const logger = this.core.context.logger;
|
||||
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||
let videoInfo = {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
@@ -151,17 +150,17 @@ export class NTQQFileApi {
|
||||
filePath,
|
||||
};
|
||||
try {
|
||||
videoInfo = await getVideoInfo(filePath, logger);
|
||||
videoInfo = await getVideoInfo(filePath, this.context.logger);
|
||||
} catch (e) {
|
||||
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
|
||||
this.context.logger.logError('获取视频信息失败,将使用默认值', e);
|
||||
}
|
||||
|
||||
let fileExt = 'mp4';
|
||||
try {
|
||||
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
|
||||
const tempExt = (await fileTypeFromFile(filePath))?.ext;
|
||||
if (tempExt) fileExt = tempExt;
|
||||
} catch (e) {
|
||||
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
|
||||
this.context.logger.logError('获取文件类型失败', e);
|
||||
}
|
||||
const newFilePath = filePath + '.' + fileExt;
|
||||
fs.copyFileSync(filePath, newFilePath);
|
||||
@@ -182,7 +181,7 @@ export class NTQQFileApi {
|
||||
ffmpeg(filePath)
|
||||
.on('error', (err) => {
|
||||
try {
|
||||
logger.logDebug('获取视频封面失败,使用默认封面', err);
|
||||
this.context.logger.logDebug('获取视频封面失败,使用默认封面', err);
|
||||
if (diyThumbPath) {
|
||||
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
||||
resolve(thumbPath);
|
||||
@@ -192,7 +191,7 @@ export class NTQQFileApi {
|
||||
resolve(thumbPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
|
||||
this.context.logger.logError('获取视频封面失败,使用默认封面失败', error);
|
||||
}
|
||||
})
|
||||
.screenshots({
|
||||
@@ -229,6 +228,7 @@ export class NTQQFileApi {
|
||||
}
|
||||
|
||||
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
|
||||
|
||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
||||
if (!silkPath) {
|
||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||
@@ -238,8 +238,7 @@ export class NTQQFileApi {
|
||||
throw new Error('文件异常,大小为0');
|
||||
}
|
||||
if (converted) {
|
||||
fsPromises.unlink(silkPath).then().catch(
|
||||
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
|
||||
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e)
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -306,18 +305,18 @@ export class NTQQFileApi {
|
||||
element.elementType === ElementType.FILE
|
||||
) {
|
||||
switch (element.elementType) {
|
||||
case ElementType.PIC:
|
||||
case ElementType.PIC:
|
||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
break;
|
||||
case ElementType.VIDEO:
|
||||
element.videoElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
break;
|
||||
case ElementType.PTT:
|
||||
element.pttElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
break;
|
||||
case ElementType.FILE:
|
||||
element.fileElement!.filePath = elementResults[elementIndex];
|
||||
break;
|
||||
break;
|
||||
}
|
||||
elementIndex++;
|
||||
}
|
||||
@@ -453,7 +452,7 @@ export class NTQQFileApi {
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
|
||||
this.context.logger.logError('获取rkey失败', error.message);
|
||||
}
|
||||
|
||||
if (!rkeyData.online_rkey) {
|
||||
@@ -463,7 +462,7 @@ export class NTQQFileApi {
|
||||
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);
|
||||
this.context.logger.logDebug('获取rkey失败 Fallback Old Mode', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { FriendV2 } from '@/core/types';
|
||||
import { FriendRequest, FriendV2 } from '@/core/types';
|
||||
import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
|
||||
@@ -79,16 +79,10 @@ export class NTQQFriendApi {
|
||||
return ret;
|
||||
}
|
||||
|
||||
async handleFriendRequest(flag: string, accept: boolean) {
|
||||
const data = flag.split('|');
|
||||
if (data.length < 2) {
|
||||
return;
|
||||
}
|
||||
const friendUid = data[0];
|
||||
const reqTime = data[1];
|
||||
async handleFriendRequest(notify: FriendRequest, accept: boolean) {
|
||||
this.context.session.getBuddyService()?.approvalFriendRequest({
|
||||
friendUid: friendUid,
|
||||
reqTime: reqTime,
|
||||
friendUid: notify.friendUid,
|
||||
reqTime: notify.reqTime,
|
||||
accept,
|
||||
});
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
GeneralCallResult,
|
||||
Group,
|
||||
GroupMember,
|
||||
NTGroupMemberRole,
|
||||
NTGroupRequestOperateTypes,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
KickMemberV2Req,
|
||||
MemberExtSourceType,
|
||||
NapCatCore,
|
||||
GroupNotify,
|
||||
GroupInfoSource,
|
||||
} from '@/core';
|
||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||
import { LimitedHashTable } from '@/common/message-unique';
|
||||
@@ -16,34 +17,35 @@ import { NTEventWrapper } from '@/common/event';
|
||||
export class NTQQGroupApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
groupCache: Map<string, Group> = new Map<string, Group>();
|
||||
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
|
||||
groups: Group[] = [];
|
||||
essenceLRU = new LimitedHashTable<number, string>(1000);
|
||||
session: any;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
async initApi() {
|
||||
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
}
|
||||
async initCache() {
|
||||
this.groups = await this.getGroups();
|
||||
for (const group of this.groups) {
|
||||
this.groupCache.set(group.groupCode, group);
|
||||
}
|
||||
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
|
||||
// process.pid 调试点
|
||||
|
||||
async fetchGroupDetail(groupCode: string) {
|
||||
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getGroupDetailInfo',
|
||||
'NodeIKernelGroupListener/onGroupDetailInfoChange',
|
||||
[groupCode, GroupInfoSource.KDATACARD],
|
||||
(ret) => ret.result === 0,
|
||||
(detailInfo) => detailInfo.groupCode === groupCode,
|
||||
1,
|
||||
5000
|
||||
);
|
||||
return detailInfo;
|
||||
}
|
||||
|
||||
async getCoreAndBaseInfo(uids: string[]) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
async initApi() {
|
||||
this.initCache().then().catch(e => this.context.logger.logError(e));
|
||||
}
|
||||
|
||||
async initCache() {
|
||||
for (const group of await this.getGroups(true)) {
|
||||
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
|
||||
}
|
||||
}
|
||||
|
||||
async fetchGroupEssenceList(groupCode: string) {
|
||||
@@ -54,20 +56,22 @@ export class NTQQGroupApi {
|
||||
pageLimit: 300,
|
||||
}, pskey);
|
||||
}
|
||||
|
||||
async getGroupShutUpMemberList(groupCode: string) {
|
||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||
return (await data)[1];
|
||||
}
|
||||
async clearGroupNotifiesUnreadCount(uk: boolean) {
|
||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
|
||||
|
||||
async clearGroupNotifiesUnreadCount(doubt: boolean) {
|
||||
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(doubt);
|
||||
}
|
||||
|
||||
async setGroupAvatar(gc: string, filePath: string) {
|
||||
return this.context.session.getGroupService().setHeader(gc, filePath);
|
||||
async setGroupAvatar(groupCode: string, filePath: string) {
|
||||
return this.context.session.getGroupService().setHeader(groupCode, filePath);
|
||||
}
|
||||
|
||||
async getGroups(forced = false) {
|
||||
async getGroups(forced: boolean = false) {
|
||||
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getGroupList',
|
||||
'NodeIKernelGroupListener/onGroupListUpdate',
|
||||
@@ -76,9 +80,9 @@ export class NTQQGroupApi {
|
||||
return groupList;
|
||||
}
|
||||
|
||||
async getGroupExtFE0Info(groupCode: string[], forced = true) {
|
||||
async getGroupExtFE0Info(groupCodes: Array<string>, forced = true) {
|
||||
return this.context.session.getGroupService().getGroupExt0xEF0Info(
|
||||
groupCode,
|
||||
groupCodes,
|
||||
[],
|
||||
{
|
||||
bindGuildId: 1,
|
||||
@@ -118,53 +122,51 @@ export class NTQQGroupApi {
|
||||
);
|
||||
}
|
||||
|
||||
async getGroup(groupCode: string, forced = false) {
|
||||
let group = this.groupCache.get(groupCode.toString());
|
||||
if (!group) {
|
||||
try {
|
||||
const groupList = await this.getGroups(forced);
|
||||
if (groupList.length) {
|
||||
groupList.forEach(g => {
|
||||
this.groupCache.set(g.groupCode, g);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
group = this.groupCache.get(groupCode.toString());
|
||||
return group;
|
||||
}
|
||||
|
||||
async getGroupMemberAll(groupCode: string, forced = false) {
|
||||
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
||||
}
|
||||
|
||||
async refreshGroupMemberCache(groupCode: string, isWait = true) {
|
||||
const updateCache = async () => {
|
||||
try {
|
||||
const members = await this.getGroupMemberAll(groupCode, true);
|
||||
this.groupMemberCache.set(groupCode, members.result.infos);
|
||||
} catch (e) {
|
||||
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (isWait) {
|
||||
await updateCache();
|
||||
} else {
|
||||
updateCache();
|
||||
}
|
||||
|
||||
return this.groupMemberCache.get(groupCode);
|
||||
}
|
||||
|
||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||
const groupCodeStr = groupCode.toString();
|
||||
const memberUinOrUidStr = memberUinOrUid.toString();
|
||||
|
||||
// 获取群成员缓存
|
||||
let members = this.groupMemberCache.get(groupCodeStr);
|
||||
if (!members) {
|
||||
try {
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
this.groupMemberCache.set(groupCodeStr, members);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function getMember() {
|
||||
let member: GroupMember | undefined;
|
||||
if (isNumeric(memberUinOrUidStr)) {
|
||||
member = Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
|
||||
} else {
|
||||
member = members!.get(memberUinOrUidStr);
|
||||
}
|
||||
return member;
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
|
||||
}
|
||||
|
||||
const getMember = () => {
|
||||
if (isNumeric(memberUinOrUidStr)) {
|
||||
return Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
|
||||
} else {
|
||||
return members!.get(memberUinOrUidStr);
|
||||
}
|
||||
};
|
||||
|
||||
let member = getMember();
|
||||
// 如果缓存中不存在该成员,尝试刷新缓存
|
||||
if (!member) {
|
||||
members = await this.getGroupMembers(groupCodeStr);
|
||||
members = (await this.refreshGroupMemberCache(groupCodeStr, true));
|
||||
member = getMember();
|
||||
}
|
||||
return member;
|
||||
@@ -174,26 +176,26 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
|
||||
}
|
||||
|
||||
async CreatGroupFileFolder(groupCode: string, folderName: string) {
|
||||
async creatGroupFileFolder(groupCode: string, folderName: string) {
|
||||
return this.context.session.getRichMediaService().createGroupFolder(groupCode, folderName);
|
||||
}
|
||||
|
||||
async DelGroupFile(groupCode: string, files: string[]) {
|
||||
async delGroupFile(groupCode: string, files: Array<string>) {
|
||||
return this.context.session.getRichMediaService().deleteGroupFile(groupCode, [102], files);
|
||||
}
|
||||
|
||||
async DelGroupFileFolder(groupCode: string, folderId: string) {
|
||||
async delGroupFileFolder(groupCode: string, folderId: string) {
|
||||
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
|
||||
}
|
||||
|
||||
async addGroupEssence(GroupCode: string, msgId: string) {
|
||||
async addGroupEssence(groupCode: string, msgId: string) {
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
guildId: '',
|
||||
peerUid: GroupCode,
|
||||
peerUid: groupCode,
|
||||
}, msgId, 1, false);
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
groupCode: groupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||
};
|
||||
@@ -204,9 +206,9 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getGroupService().kickMemberV2(param);
|
||||
}
|
||||
|
||||
async deleteGroupBulletin(GroupCode: string, noticeId: string) {
|
||||
async deleteGroupBulletin(groupCode: string, noticeId: string) {
|
||||
const psKey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||||
return this.context.session.getGroupService().deleteGroupBulletin(GroupCode, psKey, noticeId);
|
||||
return this.context.session.getGroupService().deleteGroupBulletin(groupCode, psKey, noticeId);
|
||||
}
|
||||
|
||||
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
|
||||
@@ -217,65 +219,42 @@ export class NTQQGroupApi {
|
||||
return this.context.session.getGroupService().quitGroupV2(param);
|
||||
}
|
||||
|
||||
async removeGroupEssenceBySeq(GroupCode: string, msgRandom: string, msgSeq: string) {
|
||||
async removeGroupEssenceBySeq(groupCode: string, msgRandom: string, msgSeq: string) {
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
groupCode: groupCode,
|
||||
msgRandom: parseInt(msgRandom),
|
||||
msgSeq: parseInt(msgSeq),
|
||||
};
|
||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
||||
}
|
||||
|
||||
async removeGroupEssence(GroupCode: string, msgId: string) {
|
||||
async removeGroupEssence(groupCode: string, msgId: string) {
|
||||
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||
chatType: 2,
|
||||
guildId: '',
|
||||
peerUid: GroupCode,
|
||||
peerUid: groupCode,
|
||||
}, msgId, 1, false);
|
||||
const param = {
|
||||
groupCode: GroupCode,
|
||||
groupCode: groupCode,
|
||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||
};
|
||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
||||
}
|
||||
|
||||
async getSingleScreenNotifies(doubt: boolean, num: number) {
|
||||
async getSingleScreenNotifies(doubt: boolean, count: number) {
|
||||
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getSingleScreenNotifies',
|
||||
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
|
||||
[
|
||||
doubt,
|
||||
'',
|
||||
num,
|
||||
count,
|
||||
],
|
||||
);
|
||||
return notifies;
|
||||
}
|
||||
|
||||
async getGroupMemberV2(GroupCode: string, uid: string, forced = false) {
|
||||
const Listener = this.core.eventWrapper.registerListen(
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
(params, _, members) => params === GroupCode && members.size > 0,
|
||||
1,
|
||||
forced ? 5000 : 250,
|
||||
);
|
||||
const retData = await (
|
||||
this.core.eventWrapper
|
||||
.createEventFunction('NodeIKernelGroupService/getMemberInfo')
|
||||
)!(GroupCode, [uid], forced);
|
||||
if (retData.result !== 0) {
|
||||
throw new Error(`${retData.errMsg}`);
|
||||
}
|
||||
const result = await Listener as unknown;
|
||||
let member: GroupMember | undefined;
|
||||
if (Array.isArray(result) && result?.[2] instanceof Map) {
|
||||
const members = result[2] as Map<string, GroupMember>;
|
||||
member = members.get(uid);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
async searchGroup(groupCode: string) {
|
||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelSearchService/searchGroup',
|
||||
@@ -294,178 +273,89 @@ export class NTQQGroupApi {
|
||||
return ret.groupInfos.find(g => g.groupCode === groupCode);
|
||||
}
|
||||
|
||||
async getGroupMemberEx(GroupCode: string, uid: string, forced = false, retry = 2) {
|
||||
async getGroupMemberEx(groupCode: string, uid: string, forced: boolean = false, retry: number = 2) {
|
||||
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
|
||||
return eventWrapper.callNormalEventV2(
|
||||
'NodeIKernelGroupService/getMemberInfo',
|
||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||
[GroupCode, [uid], forced],
|
||||
[groupCode, [uid], forced],
|
||||
(ret) => ret.result === 0,
|
||||
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||
1,
|
||||
forced ? 2500 : 250
|
||||
);
|
||||
}, this.core.eventWrapper, GroupCode, uid, forced);
|
||||
}, this.core.eventWrapper, groupCode, uid, forced);
|
||||
if (data && data[3] instanceof Map && data[3].has(uid)) {
|
||||
return data[3].get(uid);
|
||||
}
|
||||
if (retry > 0) {
|
||||
const trydata = await this.getGroupMemberEx(GroupCode, uid, true, retry - 1) as GroupMember | undefined;
|
||||
const trydata = await this.getGroupMemberEx(groupCode, uid, true, retry - 1) as GroupMember | undefined;
|
||||
if (trydata) return trydata;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (modeListener) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
};
|
||||
async getGroupFileCount(groupCodes: Array<string>) {
|
||||
return this.context.session.getRichMediaService().batchGetGroupFileCount(groupCodes);
|
||||
}
|
||||
|
||||
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
|
||||
infos: Map<string, GroupMember>;
|
||||
finish: boolean;
|
||||
hasNext: boolean | undefined;
|
||||
listenerMode: boolean;
|
||||
}> {
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
||||
.catch(() => { });
|
||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
let resMode2;
|
||||
if (result.result.finish && result.result.infos.size === 0) {
|
||||
const ret = (await once)?.[0];
|
||||
if (ret) {
|
||||
resMode2 = ret;
|
||||
}
|
||||
}
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
return {
|
||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
||||
finish: result.result.finish,
|
||||
hasNext: resMode2?.hasNext,
|
||||
listenerMode: resMode2?.hasNext !== undefined
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
|
||||
if (no_cache) {
|
||||
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
|
||||
}
|
||||
let res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
let ret = res.infos;
|
||||
if (res.infos.size === 0 && !res.listenerMode) {
|
||||
res = await this.GetGroupMembersV3(groupQQ, num);
|
||||
ret = res.infos;
|
||||
}
|
||||
if (res.infos.size === 0) {
|
||||
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
|
||||
const groupService = this.context.session.getGroupService();
|
||||
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
|
||||
const result = await groupService.getNextMemberList(sceneId, undefined, num);
|
||||
if (result.errCode !== 0) {
|
||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
||||
}
|
||||
this.context.logger.logDebug(`获取群(${groupQQ})成员列表结果:`, `members: ${result.result.infos.size}`);
|
||||
return result.result.infos;
|
||||
}
|
||||
|
||||
async getGroupFileCount(group_ids: Array<string>) {
|
||||
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
|
||||
}
|
||||
|
||||
async getArkJsonGroupShare(GroupCode: string) {
|
||||
async getArkJsonGroupShare(groupCode: string) {
|
||||
const ret = await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
|
||||
GroupCode,
|
||||
groupCode,
|
||||
) as GeneralCallResult & { arkJson: string };
|
||||
return ret.arkJson;
|
||||
}
|
||||
|
||||
//需要异常处理
|
||||
async uploadGroupBulletinPic(GroupCode: string, imageurl: string) {
|
||||
async uploadGroupBulletinPic(groupCode: string, imageurl: string) {
|
||||
const _Pskey = (await this.core.apis.UserApi.getPSkey(['qun.qq.com'])).domainPskeyMap.get('qun.qq.com')!;
|
||||
return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
|
||||
return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl);
|
||||
}
|
||||
|
||||
async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
|
||||
const flagitem = flag.split('|');
|
||||
const groupCode = flagitem[0];
|
||||
const seq = flagitem[1];
|
||||
const type = parseInt(flagitem[2]);
|
||||
|
||||
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) {
|
||||
return this.context.session.getGroupService().operateSysNotify(
|
||||
false,
|
||||
{
|
||||
operateType: operateType,
|
||||
targetMsg: {
|
||||
seq: seq, // 通知序列号
|
||||
type: type,
|
||||
groupCode: groupCode,
|
||||
seq: notify.seq, // 通知序列号
|
||||
type: notify.type,
|
||||
groupCode: notify.group.groupCode,
|
||||
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async quitGroup(groupQQ: string) {
|
||||
return this.context.session.getGroupService().quitGroup(groupQQ);
|
||||
async quitGroup(groupCode: string) {
|
||||
return this.context.session.getGroupService().quitGroup(groupCode);
|
||||
}
|
||||
|
||||
async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
|
||||
return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason);
|
||||
async kickMember(groupCode: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
|
||||
return this.context.session.getGroupService().kickMember(groupCode, kickUids, refuseForever, kickReason);
|
||||
}
|
||||
|
||||
async banMember(groupQQ: string, memList: Array<{ uid: string, timeStamp: number }>) {
|
||||
async banMember(groupCode: string, memList: Array<{ uid: string, timeStamp: number }>) {
|
||||
// timeStamp为秒数, 0为解除禁言
|
||||
return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList);
|
||||
return this.context.session.getGroupService().setMemberShutUp(groupCode, memList);
|
||||
}
|
||||
|
||||
async banGroup(groupQQ: string, shutUp: boolean) {
|
||||
return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp);
|
||||
async banGroup(groupCode: string, shutUp: boolean) {
|
||||
return this.context.session.getGroupService().setGroupShutUp(groupCode, shutUp);
|
||||
}
|
||||
|
||||
async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
|
||||
return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
|
||||
async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
|
||||
return this.context.session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName);
|
||||
}
|
||||
|
||||
async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
|
||||
return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
|
||||
async setMemberRole(groupCode: string, memberUid: string, role: NTGroupMemberRole) {
|
||||
return this.context.session.getGroupService().modifyMemberRole(groupCode, memberUid, role);
|
||||
}
|
||||
|
||||
async setGroupName(groupQQ: string, groupName: string) {
|
||||
return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
|
||||
async setGroupName(groupCode: string, groupName: string) {
|
||||
return this.context.session.getGroupService().modifyGroupName(groupCode, groupName, false);
|
||||
}
|
||||
|
||||
async publishGroupBulletin(groupQQ: string, content: string, picInfo: {
|
||||
async publishGroupBulletin(groupCode: string, content: string, picInfo: {
|
||||
id: string,
|
||||
width: number,
|
||||
height: number
|
||||
@@ -479,11 +369,11 @@ export class NTQQGroupApi {
|
||||
pinned: pinned,
|
||||
confirmRequired: confirmRequired,
|
||||
};
|
||||
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
|
||||
return this.context.session.getGroupService().publishGroupBulletin(groupCode, psKey!, data);
|
||||
}
|
||||
|
||||
async getGroupRemainAtTimes(GroupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
||||
async getGroupRemainAtTimes(groupCode: string) {
|
||||
return this.context.session.getGroupService().getGroupRemainAtTimes(groupCode);
|
||||
}
|
||||
|
||||
async getMemberExtInfo(groupCode: string, uin: string) {
|
||||
|
@@ -31,7 +31,7 @@ export class NTQQPacketApi {
|
||||
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||
.then()
|
||||
.catch((err) => {
|
||||
this.logger.logError.bind(this.core.context.logger);
|
||||
this.logger.logError(err);
|
||||
this.errStack.push(err);
|
||||
});
|
||||
}
|
||||
|
@@ -1,25 +0,0 @@
|
||||
import { InstanceContext, NapCatCore } from '..';
|
||||
|
||||
export class NTQQMusicSignApi {
|
||||
context: InstanceContext;
|
||||
core: NapCatCore;
|
||||
|
||||
constructor(context: InstanceContext, core: NapCatCore) {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
|
||||
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
|
||||
|
||||
//外域名不行得走qgroup中转
|
||||
//https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg
|
||||
|
||||
//可外域名
|
||||
//https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1
|
||||
|
||||
//QQ音乐gtimg接口
|
||||
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
|
||||
|
||||
//还有一处公告上传可以上传高质量图片 持久为qq域名
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
|
||||
import { RequestUtil } from '@/common/request';
|
||||
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
||||
import { solveAsyncProblem } from '@/common/helper';
|
||||
import { Fallback, FallbackUtil } from '@/common/fall-back';
|
||||
|
||||
export class NTQQUserApi {
|
||||
context: InstanceContext;
|
||||
@@ -11,37 +12,28 @@ export class NTQQUserApi {
|
||||
this.context = context;
|
||||
this.core = core;
|
||||
}
|
||||
//self_tind格式
|
||||
async createUidFromTinyId(tinyId: string) {
|
||||
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
|
||||
|
||||
async getCoreAndBaseInfo(uids: string[]) {
|
||||
return await this.core.eventWrapper.callNoListenerEvent(
|
||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||
'nodeStore',
|
||||
uids,
|
||||
);
|
||||
}
|
||||
async getStatusByUid(uid: string) {
|
||||
return this.context.session.getProfileService().getStatus(uid);
|
||||
}
|
||||
async getProfileLike(uid: string, start: number, count: number) {
|
||||
|
||||
// 默认获取自己的 type = 2 获取别人 type = 1
|
||||
async getProfileLike(uid: string, start: number, count: number, type: number = 2) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 1,
|
||||
type: 2,
|
||||
type: type,
|
||||
start: start,
|
||||
limit: count,
|
||||
});
|
||||
}
|
||||
async fetchOtherProfileLike(uid: string) {
|
||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||
friendUids: [uid],
|
||||
basic: 1,
|
||||
vote: 1,
|
||||
favorite: 0,
|
||||
userProfile: 0,
|
||||
type: 1,
|
||||
start: 0,
|
||||
limit: 20,
|
||||
});
|
||||
}
|
||||
async setLongNick(longNick: string) {
|
||||
return this.context.session.getProfileService().setLongNick(longNick);
|
||||
}
|
||||
@@ -115,6 +107,19 @@ export class NTQQUserApi {
|
||||
return retUser;
|
||||
}
|
||||
|
||||
async getUserDetailInfoV2(uid: string): Promise<User> {
|
||||
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
|
||||
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
|
||||
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
|
||||
const retUser = await fallback.run().then(async (user) => {
|
||||
if (user && user.uin === '0') {
|
||||
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
|
||||
}
|
||||
return user;
|
||||
});
|
||||
return retUser;
|
||||
}
|
||||
|
||||
async modifySelfProfile(param: ModifyProfileParams) {
|
||||
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
|
||||
}
|
||||
@@ -172,35 +177,39 @@ export class NTQQUserApi {
|
||||
if (!skey) {
|
||||
throw new Error('SKey is Empty');
|
||||
}
|
||||
|
||||
return skey;
|
||||
}
|
||||
|
||||
//后期改成流水线处理
|
||||
async getUidByUinV2(Uin: string) {
|
||||
let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin);
|
||||
if (uid) return uid;
|
||||
uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
|
||||
if (uid) return uid;
|
||||
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
|
||||
if (uid) return uid;
|
||||
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
|
||||
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
|
||||
//if (uid) return uid;
|
||||
return uid;
|
||||
async getUidByUinV2(uin: string) {
|
||||
if (!uin) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fallback =
|
||||
new Fallback<string | undefined>((uid) => FallbackUtil.boolchecker(uid, uid !== undefined && uid.indexOf('*') === -1 && uid !== ''))
|
||||
.add(() => this.context.session.getUixConvertService().getUid([uin]).then((data) => data.uidInfo.get(uin)))
|
||||
.add(() => this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [uin]).get(uin))
|
||||
.add(() => this.context.session.getGroupService().getUidByUins([uin]).then((data) => data.uids.get(uin)))
|
||||
.add(() => this.getUserDetailInfoByUin(uin).then((data) => data.detail.uid));
|
||||
|
||||
const uid = await fallback.run().catch(() => '');
|
||||
return uid ?? '';
|
||||
}
|
||||
|
||||
//后期改成流水线处理
|
||||
async getUinByUidV2(Uid: string) {
|
||||
let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid);
|
||||
if (uin) return uin;
|
||||
uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
|
||||
return uin;
|
||||
async getUinByUidV2(uid: string) {
|
||||
if (!uid) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const fallback = new Fallback<string | undefined>((uin) => FallbackUtil.boolchecker(uin, uin !== undefined && uin !== '0' && uin !== ''))
|
||||
.add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
|
||||
.add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
|
||||
.add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
|
||||
.add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
|
||||
|
||||
const uin = await fallback.run().catch(() => '0');
|
||||
return uin ?? '0';
|
||||
}
|
||||
|
||||
async getRecentContactListSnapShot(count: number) {
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
WebHonorType,
|
||||
} from '@/core';
|
||||
import { NapCatCore } from '..';
|
||||
import { createReadStream, readFileSync, statSync } from 'node:fs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { basename } from 'node:path';
|
||||
|
||||
@@ -366,50 +366,4 @@ export class NTQQWebApi {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
92
src/core/external/appid.json
vendored
92
src/core/external/appid.json
vendored
@@ -86,5 +86,97 @@
|
||||
"6.9.59-29456": {
|
||||
"appid": 537249961,
|
||||
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
|
||||
},
|
||||
"9.9.16-29927": {
|
||||
"appid": 537255812,
|
||||
"qua": "V1_WIN_NQ_9.9.16_29927_GW_B"
|
||||
},
|
||||
"3.2.13-29927": {
|
||||
"appid": 537255847,
|
||||
"qua": "V1_LNX_NQ_3.2.13_29927_GW_B"
|
||||
},
|
||||
"6.9.61-29927": {
|
||||
"appid": 537255836,
|
||||
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B"
|
||||
},
|
||||
"9.9.17-30366": {
|
||||
"appid": 537258389,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30366_GW_B"
|
||||
},
|
||||
"3.2.15-30366": {
|
||||
"appid": 537258413,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30366_GW_B"
|
||||
},
|
||||
"6.9.62-30366": {
|
||||
"appid": 537258401,
|
||||
"qua": "V1_MAC_NQ_6.9.62_30366_GW_B"
|
||||
},
|
||||
"9.9.17-30483": {
|
||||
"appid": 537258439,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30483_GW_B"
|
||||
},
|
||||
"6.9.62-30483": {
|
||||
"appid": 537258463,
|
||||
"qua": "V1_MAC_NQ_6.9.62_30483_GW_B"
|
||||
},
|
||||
"3.2.15-30483": {
|
||||
"appid": 537258474,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30483_GW_B"
|
||||
},
|
||||
"9.9.17-30594": {
|
||||
"appid": 537258439,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30594_GW_B"
|
||||
},
|
||||
"6.9.62-30594": {
|
||||
"appid": 537258463,
|
||||
"qua": "V1_MAC_NQ_6.9.62_30594_GW_B"
|
||||
},
|
||||
"3.2.15-30594": {
|
||||
"appid": 537258474,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30594_GW_B"
|
||||
},
|
||||
"9.9.17-30851": {
|
||||
"appid": 537263796,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30851_GW_B"
|
||||
},
|
||||
"3.2.15-30851": {
|
||||
"appid": 537263831,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30851_GW_B"
|
||||
},
|
||||
"6.9.63-30851": {
|
||||
"appid": 537263820,
|
||||
"qua": "V1_MAC_NQ_6.9.63_30851_GW_B"
|
||||
},
|
||||
"9.9.17-30899": {
|
||||
"appid": 537263796,
|
||||
"qua": "V1_WIN_NQ_9.9.17_30899_GW_B"
|
||||
},
|
||||
"3.2.15-30899": {
|
||||
"appid": 537263831,
|
||||
"qua": "V1_LNX_NQ_3.2.15_30899_GW_B"
|
||||
},
|
||||
"6.9.63-30899": {
|
||||
"appid": 537263820,
|
||||
"qua": "V1_MAC_NQ_6.9.63_30899_GW_B"
|
||||
},
|
||||
"9.9.17-31219": {
|
||||
"appid": 537266450,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31219_GW_B"
|
||||
},
|
||||
"9.9.17-31245": {
|
||||
"appid": 537266450,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31245_GW_B"
|
||||
},
|
||||
"3.2.15-31245": {
|
||||
"appid": 537266485,
|
||||
"qua": "V1_LNX_NQ_3.2.15_31245_GW_B"
|
||||
},
|
||||
"6.9.63-31245": {
|
||||
"appid": 537266474,
|
||||
"qua": "V1_MAC_NQ_6.9.63_31245_GW_B"
|
||||
},
|
||||
"9.9.17-31363": {
|
||||
"appid": 537266500,
|
||||
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
|
||||
}
|
||||
}
|
148
src/core/external/offset.json
vendored
148
src/core/external/offset.json
vendored
@@ -82,5 +82,153 @@
|
||||
"6.9.59-29456-arm64": {
|
||||
"send": "4005FE8",
|
||||
"recv": "4008800"
|
||||
},
|
||||
"9.9.16-29927-x64": {
|
||||
"send": "3869C50",
|
||||
"recv": "386E084"
|
||||
},
|
||||
"3.2.13-29927-x64": {
|
||||
"send": "A1913A0",
|
||||
"recv": "A194CA0"
|
||||
},
|
||||
"3.2.13-29927-arm64": {
|
||||
"send": "6F1C7E0",
|
||||
"recv": "6F20018"
|
||||
},
|
||||
"6.9.61-29927-x64": {
|
||||
"send": "44FCC60",
|
||||
"recv": "44FF4CC"
|
||||
},
|
||||
"6.9.61-29927-arm64": {
|
||||
"send": "4038740",
|
||||
"recv": "403AF58"
|
||||
},
|
||||
"9.9.17-30366-x64": {
|
||||
"send": "39AB0B0",
|
||||
"recv": "39AF4E4"
|
||||
},
|
||||
"3.2.15-30366-x64": {
|
||||
"send": "A402380",
|
||||
"recv": "A405C80"
|
||||
},
|
||||
"3.2.15-30366-arm64": {
|
||||
"send": "70C3FA8",
|
||||
"recv": "70C77E0"
|
||||
},
|
||||
"6.9.62-30366-x64": {
|
||||
"send": "4669760",
|
||||
"recv": "466BFCC"
|
||||
},
|
||||
"6.9.62-30366-arm64": {
|
||||
"send": "4189770",
|
||||
"recv": "418BF88"
|
||||
},
|
||||
"9.9.17-30483-x64": {
|
||||
"send": "39AC1B0",
|
||||
"recv": "39B05E4"
|
||||
},
|
||||
"6.9.62-30483-arm64": {
|
||||
"send": "41896B0",
|
||||
"recv": "418bec8"
|
||||
},
|
||||
"6.9.62-30483-x64": {
|
||||
"send": "4669460",
|
||||
"recv": "466BCCC"
|
||||
},
|
||||
"3.2.15-30483-x64": {
|
||||
"send": "A402540",
|
||||
"recv": "A405E40"
|
||||
},
|
||||
"3.2.15-30483-arm64": {
|
||||
"send": "70C40E8",
|
||||
"recv": "70C7920"
|
||||
},
|
||||
"9.9.17-30594-x64": {
|
||||
"send": "39AC1B0",
|
||||
"recv": "39B05E4"
|
||||
},
|
||||
"6.9.62-30594-arm64": {
|
||||
"send": "41896B0",
|
||||
"recv": "418bec8"
|
||||
},
|
||||
"6.9.62-30594-x64": {
|
||||
"send": "4669460",
|
||||
"recv": "466BCCC"
|
||||
},
|
||||
"3.2.15-30594-x64": {
|
||||
"send": "A402540",
|
||||
"recv": "A405E40"
|
||||
},
|
||||
"3.2.15-30594-arm64": {
|
||||
"send": "70C40E8",
|
||||
"recv": "70C7920"
|
||||
},
|
||||
"9.9.17-30851-x64": {
|
||||
"send": "395C150",
|
||||
"recv": "3960584"
|
||||
},
|
||||
"3.2.15-30851-x64": {
|
||||
"send": "A4A03E0",
|
||||
"recv": "A4A3CE0"
|
||||
},
|
||||
"3.2.15-30851-arm64": {
|
||||
"send": "713A318",
|
||||
"recv": "713DB50"
|
||||
},
|
||||
"6.9.63.30851-x64": {
|
||||
"send": "46C8040",
|
||||
"recv": "46CA8AC"
|
||||
},
|
||||
"6.9.63-30851-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "41DF3F0"
|
||||
},
|
||||
"9.9.17-30899-x64": {
|
||||
"send": "395C150",
|
||||
"recv": "3960584"
|
||||
},
|
||||
"3.2.15-30899-x64": {
|
||||
"send": "A4A03E0",
|
||||
"recv": "A4A3CE0"
|
||||
},
|
||||
"3.2.15-30899-arm64": {
|
||||
"send": "713A318",
|
||||
"recv": "713DB50"
|
||||
},
|
||||
"6.9.63.30899-x64": {
|
||||
"send": "46C8040",
|
||||
"recv": "46CA8AC"
|
||||
},
|
||||
"6.9.63-30899-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "41DF3F0"
|
||||
},
|
||||
"9.9.17-31219-x64": {
|
||||
"send": "39C1350",
|
||||
"recv": "39C5784"
|
||||
},
|
||||
"9.9.17-31245-x64": {
|
||||
"send": "39C1350",
|
||||
"recv": "39C5784"
|
||||
},
|
||||
"6.9.63.31245-x64": {
|
||||
"send": "4720A40",
|
||||
"recv": "47232AC"
|
||||
},
|
||||
"6.9.63-31245-arm64": {
|
||||
"send": "41DCBD8",
|
||||
"recv": "422D4E8"
|
||||
},
|
||||
"3.2.15-31245-x64": {
|
||||
"send": "A550F80",
|
||||
"recv": "A554880"
|
||||
},
|
||||
"3.2.15-31245-arm64": {
|
||||
"send": "71BEBB8",
|
||||
"recv": "71C23F0"
|
||||
},
|
||||
"9.9.17-31363-x64": {
|
||||
"send": "39C1910",
|
||||
"recv": "39C5d44"
|
||||
}
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
// 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);
|
||||
}
|
14
src/core/helper/msg.ts
Normal file
14
src/core/helper/msg.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { fileTypeFromFile } from 'file-type';
|
||||
import { PicType } from '../types';
|
||||
export async function getFileTypeForSendType(picPath: string): Promise<PicType> {
|
||||
const fileTypeResult = (await fileTypeFromFile(picPath))?.ext ?? 'jpg';
|
||||
const picTypeMap: { [key: string]: PicType } = {
|
||||
//'webp': PicType.NEWPIC_WEBP,
|
||||
'gif': PicType.NEWPIC_GIF,
|
||||
// 'png': PicType.NEWPIC_APNG,
|
||||
// 'jpg': PicType.NEWPIC_JPEG,
|
||||
// 'jpeg': PicType.NEWPIC_JPEG,
|
||||
// 'bmp': PicType.NEWPIC_BMP,
|
||||
};
|
||||
return picTypeMap[fileTypeResult] ?? PicType.NEWPIC_JPEG;
|
||||
}
|
@@ -15,6 +15,10 @@ export class RkeyManager {
|
||||
private_rkey: '',
|
||||
expired_time: 0,
|
||||
};
|
||||
private failureCount: number = 0;
|
||||
private lastFailureTimestamp: number = 0;
|
||||
private readonly FAILURE_LIMIT: number = 8;
|
||||
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor(serverUrl: string[], logger: LogWrapper) {
|
||||
this.logger = logger;
|
||||
@@ -22,12 +26,21 @@ export class RkeyManager {
|
||||
}
|
||||
|
||||
async getRkey() {
|
||||
const now = new Date().getTime();
|
||||
if (now - this.lastFailureTimestamp > this.ONE_DAY) {
|
||||
this.failureCount = 0; // 重置失败计数器
|
||||
}
|
||||
|
||||
if (this.failureCount >= this.FAILURE_LIMIT) {
|
||||
this.logger.logError(`[Rkey] 服务存在异常, 图片使用FallBack机制`);
|
||||
throw new Error('获取rkey失败次数过多,请稍后再试');
|
||||
}
|
||||
|
||||
if (this.isExpired()) {
|
||||
try {
|
||||
await this.refreshRkey();
|
||||
} catch (e) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
|
||||
throw new Error(`${e}`);//外抛
|
||||
}
|
||||
}
|
||||
return this.rkeyData;
|
||||
@@ -35,7 +48,6 @@ export class RkeyManager {
|
||||
|
||||
isExpired(): boolean {
|
||||
const now = new Date().getTime() / 1000;
|
||||
// console.log(`now: ${now}, expired_time: ${this.rkeyData.expired_time}`);
|
||||
return now > this.rkeyData.expired_time;
|
||||
}
|
||||
|
||||
@@ -49,14 +61,17 @@ export class RkeyManager {
|
||||
private_rkey: temp.private_rkey.slice(6),
|
||||
expired_time: temp.expired_time
|
||||
};
|
||||
this.failureCount = 0;
|
||||
return;
|
||||
} catch (e) {
|
||||
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
|
||||
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
|
||||
this.failureCount++;
|
||||
this.lastFailureTimestamp = new Date().getTime();
|
||||
//是否为最后一个url
|
||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
138
src/core/helper/status.ts
Normal file
138
src/core/helper/status.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import os from "node:os";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
export interface SystemStatus {
|
||||
cpu: {
|
||||
model: string,
|
||||
speed: string
|
||||
usage: {
|
||||
system: string
|
||||
qq: string
|
||||
},
|
||||
core: number
|
||||
},
|
||||
memory: {
|
||||
total: string
|
||||
usage: {
|
||||
system: string
|
||||
qq: string
|
||||
}
|
||||
},
|
||||
arch: string
|
||||
}
|
||||
|
||||
export class StatusHelper {
|
||||
private psCpuUsage = process.cpuUsage();
|
||||
private psCurrentTime = process.hrtime();
|
||||
private cpuTimes = os.cpus().map(cpu => cpu.times);
|
||||
|
||||
private replaceNaN(value: number) {
|
||||
return isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
private sysCpuInfo() {
|
||||
const currentTimes = os.cpus().map(cpu => cpu.times);
|
||||
const { total, active } = currentTimes.map((times, index) => {
|
||||
const prevTimes = this.cpuTimes[index];
|
||||
const totalCurrent = times.user + times.nice + times.sys + times.idle + times.irq;
|
||||
const totalPrev = prevTimes.user + prevTimes.nice + prevTimes.sys + prevTimes.idle + prevTimes.irq;
|
||||
const activeCurrent = totalCurrent - times.idle;
|
||||
const activePrev = totalPrev - prevTimes.idle;
|
||||
return {
|
||||
total: totalCurrent - totalPrev,
|
||||
active: activeCurrent - activePrev
|
||||
};
|
||||
}).reduce((acc, cur) => ({
|
||||
total: acc.total + cur.total,
|
||||
active: acc.active + cur.active
|
||||
}), { total: 0, active: 0 });
|
||||
this.cpuTimes = currentTimes;
|
||||
return {
|
||||
usage: this.replaceNaN(((active / total) * 100)).toFixed(2),
|
||||
model: os.cpus()[0].model,
|
||||
speed: os.cpus()[0].speed,
|
||||
core: os.cpus().length
|
||||
};
|
||||
}
|
||||
|
||||
private sysMemoryUsage() {
|
||||
const { total, free } = { total: os.totalmem(), free: os.freemem() };
|
||||
return ((total - free) / 1024 / 1024).toFixed(2);
|
||||
}
|
||||
|
||||
private qqUsage() {
|
||||
const mem = process.memoryUsage();
|
||||
const numCpus = os.cpus().length;
|
||||
const usageDiff = process.cpuUsage(this.psCpuUsage);
|
||||
const endTime = process.hrtime(this.psCurrentTime);
|
||||
this.psCpuUsage = process.cpuUsage();
|
||||
this.psCurrentTime = process.hrtime();
|
||||
const usageMS = (usageDiff.user + usageDiff.system) / 1e3;
|
||||
const totalMS = endTime[0] * 1e3 + endTime[1] / 1e6;
|
||||
const normPercent = (usageMS / totalMS / numCpus) * 100;
|
||||
return {
|
||||
cpu: this.replaceNaN(normPercent).toFixed(2),
|
||||
memory: ((mem.heapTotal + mem.external + mem.arrayBuffers) / 1024 / 1024).toFixed(2)
|
||||
};
|
||||
}
|
||||
|
||||
systemStatus(): SystemStatus {
|
||||
const qqUsage = this.qqUsage();
|
||||
const sysCpuInfo = this.sysCpuInfo();
|
||||
return {
|
||||
cpu: {
|
||||
core: sysCpuInfo.core,
|
||||
model: sysCpuInfo.model,
|
||||
speed: (sysCpuInfo.speed / 1000).toFixed(2),
|
||||
usage: {
|
||||
system: sysCpuInfo.usage,
|
||||
qq: qqUsage.cpu
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
total: (os.totalmem() / 1024 / 1024).toFixed(2),
|
||||
usage: {
|
||||
system: this.sysMemoryUsage(),
|
||||
qq: qqUsage.memory
|
||||
}
|
||||
},
|
||||
arch: `${os.platform()} ${os.arch()} ${os.release()}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class StatusHelperSubscription extends EventEmitter {
|
||||
private statusHelper: StatusHelper;
|
||||
private interval: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(time: number = 3000) {
|
||||
super();
|
||||
this.statusHelper = new StatusHelper();
|
||||
this.on('newListener', (event: string) => {
|
||||
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
|
||||
this.startInterval(time);
|
||||
}
|
||||
});
|
||||
this.on('removeListener', (event: string) => {
|
||||
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
|
||||
this.stopInterval();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startInterval(time: number) {
|
||||
this.interval ??= setInterval(() => {
|
||||
const status = this.statusHelper.systemStatus();
|
||||
this.emit('statusUpdate', status);
|
||||
}, time);
|
||||
}
|
||||
|
||||
private stopInterval() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const statusHelperSubscription = new StatusHelperSubscription();
|
@@ -24,10 +24,10 @@ import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||
import { NTEventWrapper } from '@/common/event';
|
||||
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||||
import { GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
|
||||
import { NapCatConfigLoader } from '@/core/helper/config';
|
||||
import os from 'node:os';
|
||||
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
|
||||
import { proxiedListenerOf } from '@/common/proxy-handler';
|
||||
import { NTQQPacketApi } from './apis/packet';
|
||||
export * from './wrapper';
|
||||
@@ -127,7 +127,7 @@ export class NapCatCore {
|
||||
await api.initApi();
|
||||
}
|
||||
}
|
||||
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
|
||||
this.initNapCatCoreListeners().then().catch((e) => this.context.logger.logError(e));
|
||||
|
||||
this.context.logger.setFileLogEnabled(
|
||||
this.configLoader.configData.fileLog,
|
||||
@@ -152,9 +152,10 @@ export class NapCatCore {
|
||||
// Renamed from 'InitDataListener'
|
||||
async initNapCatCoreListeners() {
|
||||
const msgListener = new NodeIKernelMsgListener();
|
||||
|
||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||
// 下线通知
|
||||
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
|
||||
this.selfInfo.online = false;
|
||||
};
|
||||
msgListener.onRecvMsg = (msgs) => {
|
||||
@@ -163,7 +164,6 @@ export class NapCatCore {
|
||||
msgListener.onAddSendMsg = (msg) => {
|
||||
this.context.logger.logMessage(msg, this.selfInfo);
|
||||
};
|
||||
//await sleep(2500);
|
||||
this.context.session.getMsgService().addKernelMsgListener(
|
||||
proxiedListenerOf(msgListener, this.context.logger),
|
||||
);
|
||||
@@ -185,92 +185,6 @@ export class NapCatCore {
|
||||
this.context.session.getProfileService().addKernelProfileListener(
|
||||
proxiedListenerOf(profileListener, this.context.logger),
|
||||
);
|
||||
|
||||
// 群相关
|
||||
const groupListener = new NodeIKernelGroupListener();
|
||||
groupListener.onGroupListUpdate = (updateType, groupList) => {
|
||||
// console.log("onGroupListUpdate", updateType, groupList)
|
||||
groupList.map(g => {
|
||||
const existGroup = this.apis.GroupApi.groupCache.get(g.groupCode);
|
||||
//群成员数量变化 应该刷新缓存
|
||||
if (existGroup && g.memberCount === existGroup.memberCount) {
|
||||
Object.assign(existGroup, g);
|
||||
} else {
|
||||
this.apis.GroupApi.groupCache.set(g.groupCode, g);
|
||||
// 获取群成员
|
||||
}
|
||||
const sceneId = this.context.session.getGroupService().createMemberListScene(g.groupCode, 'groupMemberList_MainWindow');
|
||||
this.context.session.getGroupService().getNextMemberList(sceneId, undefined, 3000).then( /* r => {
|
||||
// console.log(`get group ${g.groupCode} members`, r);
|
||||
// r.result.infos.forEach(member => {
|
||||
// });
|
||||
// groupMembers.set(g.groupCode, r.result.infos);
|
||||
} */);
|
||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
||||
});
|
||||
};
|
||||
groupListener.onMemberListChange = (arg) => {
|
||||
// TODO: 应该加一个内部自己维护的成员变动callback,用于判断成员变化通知
|
||||
const groupCode = arg.sceneId.split('_')[0];
|
||||
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
|
||||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
|
||||
arg.infos.forEach((member, uid) => {
|
||||
//console.log('onMemberListChange', member);
|
||||
const existMember = existMembers.get(uid);
|
||||
if (existMember) {
|
||||
Object.assign(existMember, member);
|
||||
} else {
|
||||
existMembers.set(uid, member);
|
||||
}
|
||||
//移除成员
|
||||
if (member.isDelete) {
|
||||
existMembers.delete(uid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.apis.GroupApi.groupMemberCache.set(groupCode, arg.infos);
|
||||
}
|
||||
};
|
||||
groupListener.onMemberInfoChange = (groupCode, dataSource, members) => {
|
||||
if (dataSource === DataSource.LOCAL && members.get(this.selfInfo.uid)?.isDelete) {
|
||||
// 自身退群或者被踢退群 5s用于Api操作 之后不再出现
|
||||
setTimeout(() => {
|
||||
this.apis.GroupApi.groupCache.delete(groupCode);
|
||||
}, 5000);
|
||||
|
||||
}
|
||||
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode);
|
||||
if (existMembers) {
|
||||
members.forEach((member, uid) => {
|
||||
const existMember = existMembers.get(uid);
|
||||
if (existMember) {
|
||||
// 检查管理变动
|
||||
member.isChangeRole = this.checkAdminEvent(groupCode, member, existMember);
|
||||
// 更新成员信息
|
||||
Object.assign(existMember, member);
|
||||
} else {
|
||||
existMembers.set(uid, member);
|
||||
}
|
||||
//移除成员
|
||||
if (member.isDelete) {
|
||||
existMembers.delete(uid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.apis.GroupApi.groupMemberCache.set(groupCode, members);
|
||||
}
|
||||
};
|
||||
this.context.session.getGroupService().addKernelGroupListener(
|
||||
proxiedListenerOf(groupListener, this.context.logger),
|
||||
);
|
||||
}
|
||||
|
||||
checkAdminEvent(groupCode: string, memberNew: GroupMember, memberOld: GroupMember | undefined): boolean {
|
||||
if (memberNew.role !== memberOld?.role) {
|
||||
this.context.logger.logDebug(`群 ${groupCode} ${memberNew.nick} 角色变更为 ${memberNew.role === 3 ? '管理员' : '群员'}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
|
||||
import { DataSource, Group, GroupDetailInfo, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
|
||||
|
||||
export class NodeIKernelGroupListener {
|
||||
onGroupListInited(listEmpty: boolean): any { }
|
||||
@@ -28,7 +28,7 @@ export class NodeIKernelGroupListener {
|
||||
onGroupConfMemberChange(...args: unknown[]): any {
|
||||
}
|
||||
|
||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
||||
onGroupDetailInfoChange(detailInfo: GroupDetailInfo): any {
|
||||
}
|
||||
|
||||
onGroupExtListUpdate(...args: unknown[]): any {
|
||||
|
@@ -255,7 +255,7 @@ export class NodeIKernelMsgListener {
|
||||
|
||||
}
|
||||
|
||||
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
|
||||
onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
|
||||
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import crypto, { createHash } from "crypto";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { OidbPacket, PacketHexStr } from "@/core/packet/transformer/base";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
export interface RecvPacket {
|
||||
type: string, // 仅recv
|
||||
@@ -27,13 +28,15 @@ function randText(len: number): string {
|
||||
|
||||
|
||||
export abstract class IPacketClient {
|
||||
protected readonly context: PacketContext;
|
||||
protected readonly napcore: NapCoreContext;
|
||||
protected readonly logger: PacketLogger;
|
||||
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||
logStack: LogStack;
|
||||
available: boolean = false;
|
||||
|
||||
protected constructor(context: PacketContext, logStack: LogStack) {
|
||||
this.context = context;
|
||||
protected constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
this.napcore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = logStack;
|
||||
}
|
||||
|
||||
@@ -81,7 +84,7 @@ export abstract class IPacketClient {
|
||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
||||
const trace_id = (randText(4) + md5 + data).slice(0, data.length / 2);
|
||||
return this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
||||
await this.context.napcore.sendSsoCmdReqByContend(cmd, trace_id);
|
||||
await this.napcore.sendSsoCmdReqByContend(cmd, trace_id);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -5,8 +5,9 @@ import fs from "fs";
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { constants } from "node:os";
|
||||
import { LRUCache } from "@/common/lru-cache";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
// 0 send 1 recv
|
||||
export interface NativePacketExportType {
|
||||
@@ -19,8 +20,8 @@ export class NativePacketClient extends IPacketClient {
|
||||
private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} };
|
||||
private readonly sendEvent = new LRUCache<number, string>(500); // seq->trace_id
|
||||
|
||||
constructor(context: PacketContext, logStack: LogStack) {
|
||||
super(context, logStack);
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
super(napCore, logger, logStack);
|
||||
}
|
||||
|
||||
check(): boolean {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Data, WebSocket, ErrorEvent } from "ws";
|
||||
import { IPacketClient, RecvPacket } from "@/core/packet/client/baseClient";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { LogStack } from "@/core/packet/context/clientContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
|
||||
export class WsPacketClient extends IPacketClient {
|
||||
private websocket: WebSocket | null = null;
|
||||
@@ -13,15 +14,15 @@ export class WsPacketClient extends IPacketClient {
|
||||
private isInitialized: boolean = false;
|
||||
private initPayload: { pid: number, recv: string, send: string } | null = null;
|
||||
|
||||
constructor(context: PacketContext, logStack: LogStack) {
|
||||
super(context, logStack);
|
||||
this.clientUrl = this.context.napcore.config.packetServer
|
||||
? this.clientUrlWrap(this.context.napcore.config.packetServer)
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
|
||||
super(napCore, logger, logStack);
|
||||
this.clientUrl = this.napcore.config.packetServer
|
||||
? this.clientUrlWrap(this.napcore.config.packetServer)
|
||||
: this.clientUrlWrap('127.0.0.1:8083');
|
||||
}
|
||||
|
||||
check(): boolean {
|
||||
if (!this.context.napcore.config.packetServer) {
|
||||
if (!this.napcore.config.packetServer) {
|
||||
this.logStack.pushLogWarn(`wsPacketClient 未配置服务器地址`);
|
||||
return false;
|
||||
}
|
||||
@@ -67,7 +68,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
this.websocket.onopen = () => {
|
||||
this.available = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.context.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
|
||||
this.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
|
||||
if (!this.isInitialized && this.initPayload) {
|
||||
this.websocket!.send(JSON.stringify({
|
||||
action: 'init',
|
||||
@@ -79,15 +80,15 @@ export class WsPacketClient extends IPacketClient {
|
||||
};
|
||||
this.websocket.onclose = () => {
|
||||
this.available = false;
|
||||
this.context.logger.warn(`WebSocket 连接关闭,尝试重连...`);
|
||||
this.logger.warn(`WebSocket 连接关闭,尝试重连...`);
|
||||
reject(new Error('WebSocket 连接关闭'));
|
||||
};
|
||||
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
|
||||
this.context.logger.error(`处理消息时出错: ${err}`);
|
||||
this.logger.error(`处理消息时出错: ${err}`);
|
||||
});
|
||||
this.websocket.onerror = (event: ErrorEvent) => {
|
||||
this.available = false;
|
||||
this.context.logger.error(`WebSocket 出错: ${event.message}`);
|
||||
this.logger.error(`WebSocket 出错: ${event.message}`);
|
||||
this.websocket?.close();
|
||||
reject(new Error(`WebSocket 出错: ${event.message}`));
|
||||
};
|
||||
@@ -106,7 +107,7 @@ export class WsPacketClient extends IPacketClient {
|
||||
const event = this.cb.get(`${trace_id_md5}${action}`);
|
||||
if (event) await event(json.data);
|
||||
} catch (error) {
|
||||
this.context.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
|
||||
this.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { IPacketClient } from "@/core/packet/client/baseClient";
|
||||
import { NativePacketClient } from "@/core/packet/client/nativeClient";
|
||||
import { WsPacketClient } from "@/core/packet/client/wsClient";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
|
||||
type clientPriority = {
|
||||
[key: number]: (context: PacketContext, logStack: LogStack) => IPacketClient;
|
||||
[key: number]: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => IPacketClient;
|
||||
}
|
||||
|
||||
const clientPriority: clientPriority = {
|
||||
10: (context: PacketContext, logStack: LogStack) => new NativePacketClient(context, logStack),
|
||||
1: (context: PacketContext, logStack: LogStack) => new WsPacketClient(context, logStack),
|
||||
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
|
||||
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
|
||||
};
|
||||
|
||||
export class LogStack {
|
||||
@@ -51,13 +51,15 @@ export class LogStack {
|
||||
}
|
||||
|
||||
export class PacketClientContext {
|
||||
private readonly context: PacketContext;
|
||||
private readonly napCore: NapCoreContext;
|
||||
private readonly logger: PacketLogger;
|
||||
private readonly logStack: LogStack;
|
||||
private readonly _client: IPacketClient;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
this.logStack = new LogStack(context.logger);
|
||||
constructor(napCore: NapCoreContext, logger: PacketLogger) {
|
||||
this.napCore = napCore;
|
||||
this.logger = logger;
|
||||
this.logStack = new LogStack(logger);
|
||||
this._client = this.newClient();
|
||||
}
|
||||
|
||||
@@ -79,23 +81,23 @@ export class PacketClientContext {
|
||||
}
|
||||
|
||||
private newClient(): IPacketClient {
|
||||
const prefer = this.context.napcore.config.packetBackend;
|
||||
const prefer = this.napCore.config.packetBackend;
|
||||
let client: IPacketClient | null;
|
||||
switch (prefer) {
|
||||
case "native":
|
||||
this.context.logger.info("使用指定的 NativePacketClient 作为后端");
|
||||
client = new NativePacketClient(this.context, this.logStack);
|
||||
this.logger.info("使用指定的 NativePacketClient 作为后端");
|
||||
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "frida":
|
||||
this.context.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
|
||||
client = new WsPacketClient(this.context, this.logStack);
|
||||
this.logger.info("[Core] [Packet] 使用指定的 FridaPacketClient 作为后端");
|
||||
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
|
||||
break;
|
||||
case "auto":
|
||||
case undefined:
|
||||
client = this.judgeClient();
|
||||
break;
|
||||
default:
|
||||
this.context.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
|
||||
this.logger.error(`未知的PacketBackend ${prefer},请检查配置文件!`);
|
||||
client = null;
|
||||
}
|
||||
if (!client?.check()) {
|
||||
@@ -110,7 +112,7 @@ export class PacketClientContext {
|
||||
private judgeClient(): IPacketClient {
|
||||
const sortedClients = Object.entries(clientPriority)
|
||||
.map(([priority, clientFactory]) => {
|
||||
const client = clientFactory(this.context, this.logStack);
|
||||
const client = clientFactory(this.napCore, this.logger, this.logStack);
|
||||
const score = +priority * +client.check();
|
||||
return { client, score };
|
||||
})
|
||||
@@ -120,7 +122,7 @@ export class PacketClientContext {
|
||||
if (!selectedClient) {
|
||||
throw new Error("[Core] [Packet] 无可用的后端,NapCat.Packet将不会加载!");
|
||||
}
|
||||
this.context.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
|
||||
this.logger.info(`自动选择 ${selectedClient.constructor.name} 作为后端`);
|
||||
return selectedClient;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { LogLevel, LogWrapper } from "@/common/log";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
|
||||
// TODO: check bind?
|
||||
export class PacketLogger {
|
||||
private readonly napLogger: LogWrapper;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.napLogger = context.napcore.logger;
|
||||
constructor(napcore: NapCoreContext) {
|
||||
this.napLogger = napcore.logger;
|
||||
}
|
||||
|
||||
private _log(level: LogLevel, ...msg: any[]): void {
|
||||
|
@@ -13,15 +13,22 @@ import { MiniAppRawData, MiniAppReqParams } from "@/core/packet/entities/miniApp
|
||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||
import { NapProtoDecodeStructType, NapProtoEncodeStructType } from "@napneko/nap-proto-core";
|
||||
import { IndexNode, MsgInfo } from "@/core/packet/transformer/proto";
|
||||
import { OidbPacket } from "@/core/packet/transformer/base";
|
||||
import { ImageOcrResult } from "@/core/packet/entities/ocrResult";
|
||||
|
||||
export class PacketOperationContext {
|
||||
private readonly context: PacketContext;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
async sendPacket<T extends boolean = false>(pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
|
||||
return await this.context.client.sendOidbPacket(pkt, rsp);
|
||||
}
|
||||
|
||||
async GroupPoke(groupUin: number, uin: number) {
|
||||
const req = trans.SendPoke.build(groupUin, uin);
|
||||
const req = trans.SendPoke.build(uin, groupUin);
|
||||
await this.context.client.sendOidbPacket(req);
|
||||
}
|
||||
|
||||
@@ -90,6 +97,46 @@ export class PacketOperationContext {
|
||||
});
|
||||
}
|
||||
|
||||
async UploadImage(img: PacketMsgPicElement) {
|
||||
await this.context.highway.uploadImage({
|
||||
chatType: ChatType.KCHATTYPEC2C,
|
||||
peerUid: this.context.napcore.basicInfo.uid
|
||||
}, img);
|
||||
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
|
||||
if (!index) {
|
||||
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
|
||||
}
|
||||
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
|
||||
}
|
||||
|
||||
async GetImageUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||
const req = trans.DownloadImage.build(selfUid, node);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.DownloadImage.parse(resp);
|
||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
||||
}
|
||||
|
||||
async ImageOCR(imgUrl: string) {
|
||||
const req = trans.ImageOCR.build(imgUrl);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = trans.ImageOCR.parse(resp);
|
||||
return {
|
||||
texts: res.ocrRspBody.textDetections.map((item) => {
|
||||
return {
|
||||
text: item.detectedText,
|
||||
confidence: item.confidence,
|
||||
coordinates: item.polygon.coordinates.map((c) => {
|
||||
return {
|
||||
x: c.x,
|
||||
y: c.y
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
language: res.ocrRspBody.language
|
||||
} as ImageOcrResult;
|
||||
}
|
||||
|
||||
async UploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
|
||||
await this.UploadResources(msg, groupUin);
|
||||
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
|
||||
|
@@ -7,19 +7,19 @@ import { PacketOperationContext } from "@/core/packet/context/operationContext";
|
||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||
|
||||
export class PacketContext {
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
readonly napcore: NapCoreContext;
|
||||
readonly logger: PacketLogger;
|
||||
readonly client: PacketClientContext;
|
||||
readonly highway: PacketHighwayContext;
|
||||
readonly msgConverter: PacketMsgConverter;
|
||||
readonly operation: PacketOperationContext;
|
||||
|
||||
constructor(core: NapCatCore) {
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this);
|
||||
this.client = new PacketClientContext(this);
|
||||
this.highway = new PacketHighwayContext(this);
|
||||
this.msgConverter = new PacketMsgConverter();
|
||||
this.napcore = new NapCoreContext(core);
|
||||
this.logger = new PacketLogger(this.napcore);
|
||||
this.client = new PacketClientContext(this.napcore, this.logger);
|
||||
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
|
||||
this.operation = new PacketOperationContext(this);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ export interface MiniAppReqCustomParams {
|
||||
desc: string;
|
||||
picUrl: string;
|
||||
jumpUrl: string;
|
||||
webUrl?: string;
|
||||
}
|
||||
|
||||
export interface MiniAppReqTemplateParams {
|
||||
|
15
src/core/packet/entities/ocrResult.ts
Normal file
15
src/core/packet/entities/ocrResult.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface ImageOcrResult {
|
||||
texts: TextDetection[];
|
||||
language: string;
|
||||
}
|
||||
|
||||
export interface TextDetection {
|
||||
text: string;
|
||||
confidence: number;
|
||||
coordinates: Coordinate[];
|
||||
}
|
||||
|
||||
export interface Coordinate {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
import { PacketHighwayClient } from "@/core/packet/highway/client";
|
||||
import { PacketContext } from "@/core/packet/context/packetContext";
|
||||
import { PacketLogger } from "@/core/packet/context/loggerContext";
|
||||
import FetchSessionKey from "@/core/packet/transformer/highway/FetchSessionKey";
|
||||
import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils";
|
||||
@@ -16,6 +15,8 @@ import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import * as trans from "@/core/packet/transformer";
|
||||
import fs from "fs";
|
||||
import { NapCoreContext } from "@/core/packet/context/napCoreContext";
|
||||
import { PacketClientContext } from "@/core/packet/context/clientContext";
|
||||
|
||||
export const BlockSize = 1024 * 1024;
|
||||
|
||||
@@ -33,23 +34,25 @@ export interface PacketHighwaySig {
|
||||
}
|
||||
|
||||
export class PacketHighwayContext {
|
||||
private readonly context: PacketContext;
|
||||
private readonly napcore: NapCoreContext;
|
||||
private readonly client: PacketClientContext;
|
||||
protected sig: PacketHighwaySig;
|
||||
protected logger: PacketLogger;
|
||||
protected hwClient: PacketHighwayClient;
|
||||
private cachedPrepareReq: Promise<void> | null = null;
|
||||
|
||||
constructor(context: PacketContext) {
|
||||
this.context = context;
|
||||
constructor(napcore: NapCoreContext, logger: PacketLogger, client: PacketClientContext) {
|
||||
this.napcore = napcore;
|
||||
this.client = client;
|
||||
this.sig = {
|
||||
uin: String(context.napcore.basicInfo.uin),
|
||||
uid: context.napcore.basicInfo.uid,
|
||||
uin: String(this.napcore.basicInfo.uin),
|
||||
uid: this.napcore.basicInfo.uid,
|
||||
sigSession: null,
|
||||
sessionKey: null,
|
||||
serverAddr: [],
|
||||
};
|
||||
this.logger = context.logger;
|
||||
this.hwClient = new PacketHighwayClient(this.sig, context.logger);
|
||||
this.logger = logger;
|
||||
this.hwClient = new PacketHighwayClient(this.sig, this.logger);
|
||||
}
|
||||
|
||||
private async checkAvailable() {
|
||||
@@ -66,7 +69,7 @@ export class PacketHighwayContext {
|
||||
private async prepareUpload(): Promise<void> {
|
||||
this.logger.debug('[Highway] on prepareUpload!');
|
||||
const packet = FetchSessionKey.build();
|
||||
const req = await this.context.client.sendOidbPacket(packet, true);
|
||||
const req = await this.client.sendOidbPacket(packet, true);
|
||||
const rsp = FetchSessionKey.parse(req);
|
||||
this.sig.sigSession = rsp.httpConn.sigSession;
|
||||
this.sig.sessionKey = rsp.httpConn.sessionKey;
|
||||
@@ -136,7 +139,7 @@ export class PacketHighwayContext {
|
||||
private async uploadGroupImage(groupUin: number, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const req = UploadGroupImage.build(groupUin, img);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = UploadGroupImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -173,7 +176,7 @@ export class PacketHighwayContext {
|
||||
private async uploadC2CImage(peerUid: string, img: PacketMsgPicElement): Promise<void> {
|
||||
img.sha1 = Buffer.from(await calculateSha1(img.path)).toString('hex');
|
||||
const req = trans.UploadPrivateImage.build(peerUid, img);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateImage.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -211,7 +214,7 @@ export class PacketHighwayContext {
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadGroupVideo.build(groupUin, video);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -276,7 +279,7 @@ export class PacketHighwayContext {
|
||||
video.fileSha1 = Buffer.from(await calculateSha1(video.filePath)).toString('hex');
|
||||
video.thumbSha1 = Buffer.from(await calculateSha1(video.thumbPath)).toString('hex');
|
||||
const req = trans.UploadPrivateVideo.build(peerUid, video);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateVideo.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -339,7 +342,7 @@ export class PacketHighwayContext {
|
||||
private async uploadGroupPtt(groupUin: number, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const req = trans.UploadGroupPtt.build(groupUin, ptt);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupPtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -375,7 +378,7 @@ export class PacketHighwayContext {
|
||||
private async uploadC2CPtt(peerUid: string, ptt: PacketMsgPttElement): Promise<void> {
|
||||
ptt.fileSha1 = Buffer.from(await calculateSha1(ptt.filePath)).toString('hex');
|
||||
const req = trans.UploadPrivatePtt.build(peerUid, ptt);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivatePtt.parse(resp);
|
||||
const ukey = preRespData.upload.uKey;
|
||||
if (ukey && ukey != "") {
|
||||
@@ -413,7 +416,7 @@ export class PacketHighwayContext {
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const req = trans.UploadGroupFile.build(groupUin, file);
|
||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||
const resp = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadGroupFile.parse(resp);
|
||||
if (!preRespData?.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadGroupFileReq file not exist, need upload!`);
|
||||
@@ -476,7 +479,7 @@ export class PacketHighwayContext {
|
||||
file.fileMd5 = await computeMd5AndLengthWithLimit(file.filePath);
|
||||
file.fileSha1 = await calculateSha1(file.filePath);
|
||||
const req = await trans.UploadPrivateFile.build(this.sig.uid, peerUid, file);
|
||||
const res = await this.context.client.sendOidbPacket(req, true);
|
||||
const res = await this.client.sendOidbPacket(req, true);
|
||||
const preRespData = trans.UploadPrivateFile.parse(res);
|
||||
if (!preRespData.upload?.boolFileExist) {
|
||||
this.logger.debug(`[Highway] uploadC2CFileReq file not exist, need upload!`);
|
||||
@@ -531,7 +534,7 @@ export class PacketHighwayContext {
|
||||
file.fileUuid = preRespData.upload?.uuid;
|
||||
file.fileHash = preRespData.upload?.fileAddon;
|
||||
const fileExistReq = trans.DownloadOfflineFile.build(file.fileUuid!, file.fileHash!, this.sig.uid, peerUid);
|
||||
const fileExistRes = await this.context.client.sendOidbPacket(fileExistReq, true);
|
||||
const fileExistRes = await this.client.sendOidbPacket(fileExistReq, true);
|
||||
file._e37_800_rsp = trans.DownloadOfflineFile.parse(fileExistRes);
|
||||
file._private_send_uid = this.sig.uid;
|
||||
file._private_recv_uid = peerUid;
|
||||
|
@@ -14,6 +14,8 @@ import {
|
||||
GroupFileExtra
|
||||
} from "@/core/packet/transformer/proto";
|
||||
import {
|
||||
BaseEmojiType,
|
||||
FaceType,
|
||||
NTMsgAtType,
|
||||
PicType,
|
||||
SendArkElement,
|
||||
@@ -122,7 +124,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
}
|
||||
|
||||
get isGroupReply(): boolean {
|
||||
return this.messageClientSeq !== 0;
|
||||
return this.messageClientSeq === 0;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
@@ -158,11 +160,13 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
||||
export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
faceId: number;
|
||||
isLargeFace: boolean;
|
||||
resultId?: string;
|
||||
|
||||
constructor(element: SendFaceElement) {
|
||||
super(element);
|
||||
this.faceId = element.faceElement.faceIndex;
|
||||
this.isLargeFace = element.faceElement.faceType === 3;
|
||||
this.resultId = element.faceElement.resultId;
|
||||
this.isLargeFace = element.faceElement.faceType === FaceType.AniSticke;
|
||||
}
|
||||
|
||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||
@@ -174,10 +178,10 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
||||
aniStickerPackId: "1",
|
||||
aniStickerId: "8",
|
||||
faceId: this.faceId,
|
||||
field4: 1,
|
||||
field6: "",
|
||||
sourceType: 1,
|
||||
resultId: this.resultId,
|
||||
preview: "",
|
||||
field9: 1
|
||||
randomType: 1
|
||||
}),
|
||||
businessType: 1
|
||||
}
|
||||
@@ -254,6 +258,8 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
width: number;
|
||||
height: number;
|
||||
picType: PicType;
|
||||
picSubType: number;
|
||||
summary: string;
|
||||
sha1: string | null = null;
|
||||
msgInfo: NapProtoEncodeStructType<typeof MsgInfo> | null = null;
|
||||
groupPicExt: NapProtoEncodeStructType<typeof CustomFace> | null = null;
|
||||
@@ -268,6 +274,10 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
this.width = element.picElement.picWidth;
|
||||
this.height = element.picElement.picHeight;
|
||||
this.picType = element.picElement.picType;
|
||||
this.picSubType = element.picElement.picSubType ?? 0;
|
||||
this.summary = element.picElement.summary === '' ? (
|
||||
element.picElement.picSubType === 0 ? '[图片]' : '[动画表情]'
|
||||
) : element.picElement.summary;
|
||||
}
|
||||
|
||||
get valid(): boolean {
|
||||
@@ -286,7 +296,7 @@ export class PacketMsgPicElement extends IPacketMsgElement<SendPicElement> {
|
||||
}
|
||||
|
||||
toPreview(): string {
|
||||
return "[图片]";
|
||||
return this.summary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,7 +30,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
|
||||
shareType: req.shareType,
|
||||
versionId: req.versionId,
|
||||
withShareTicket: req.withShareTicket,
|
||||
webURL: "",
|
||||
webURL: req.webUrl ?? "",
|
||||
appidRich: Buffer.alloc(0),
|
||||
template: {
|
||||
templateId: "",
|
||||
|
37
src/core/packet/transformer/action/ImageOCR.ts
Normal file
37
src/core/packet/transformer/action/ImageOCR.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
|
||||
class ImageOCR extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0xE07_0_Response> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(url: string): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0).encode(
|
||||
{
|
||||
version: 1,
|
||||
client: 0,
|
||||
entrance: 1,
|
||||
ocrReqBody: {
|
||||
imageUrl: url,
|
||||
originMd5: "",
|
||||
afterCompressMd5: "",
|
||||
afterCompressFileSize: "",
|
||||
afterCompressWeight: "",
|
||||
afterCompressHeight: "",
|
||||
isCut: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
return OidbBase.build(0XEB7, 1, body, false, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const base = OidbBase.parse(data);
|
||||
return new NapProtoMsg(proto.OidbSvcTrpcTcp0xE07_0_Response).decode(base.body);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ImageOCR();
|
@@ -5,3 +5,4 @@ export { default as GroupSign } from './GroupSign';
|
||||
export { default as GetStrangerInfo } from './GetStrangerInfo';
|
||||
export { default as SendPoke } from './SendPoke';
|
||||
export { default as SetSpecialTitle } from './SetSpecialTitle';
|
||||
export { default as ImageOCR } from './ImageOCR';
|
||||
|
51
src/core/packet/transformer/highway/DownloadImage.ts
Normal file
51
src/core/packet/transformer/highway/DownloadImage.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as proto from "@/core/packet/transformer/proto";
|
||||
import { NapProtoEncodeStructType, NapProtoMsg } from "@napneko/nap-proto-core";
|
||||
import { OidbPacket, PacketTransformer } from "@/core/packet/transformer/base";
|
||||
import OidbBase from "@/core/packet/transformer/oidb/oidbBase";
|
||||
import { IndexNode } from "@/core/packet/transformer/proto";
|
||||
|
||||
class DownloadImage extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
|
||||
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
|
||||
reqHead: {
|
||||
common: {
|
||||
requestId: 1,
|
||||
command: 100
|
||||
},
|
||||
scene: {
|
||||
requestType: 2,
|
||||
businessType: 1,
|
||||
sceneType: 1,
|
||||
c2C: {
|
||||
accountType: 2,
|
||||
targetUid: selfUid
|
||||
},
|
||||
},
|
||||
client: {
|
||||
agentType: 2,
|
||||
}
|
||||
},
|
||||
download: {
|
||||
node: node,
|
||||
download: {
|
||||
video: {
|
||||
busiType: 0,
|
||||
sceneType: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return OidbBase.build(0x11C5, 200, body, true, false);
|
||||
}
|
||||
|
||||
parse(data: Buffer) {
|
||||
const oidbBody = OidbBase.parse(data).body;
|
||||
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
|
||||
}
|
||||
}
|
||||
|
||||
export default new DownloadImage();
|
@@ -58,8 +58,11 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
|
||||
compatQMsgSceneType: 2,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
bizType: img.picSubType,
|
||||
bytesPbReserveTroop: {
|
||||
subType: img.picSubType,
|
||||
},
|
||||
textSummary: img.summary,
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
|
@@ -58,8 +58,11 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
|
||||
compatQMsgSceneType: 1,
|
||||
extBizInfo: {
|
||||
pic: {
|
||||
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
|
||||
textSummary: "Nya~", // TODO:
|
||||
bizType: img.picSubType,
|
||||
bytesPbReserveC2C: {
|
||||
subType: img.picSubType,
|
||||
},
|
||||
textSummary: img.summary,
|
||||
},
|
||||
video: {
|
||||
bytesPbReserve: Buffer.alloc(0),
|
||||
|
@@ -11,3 +11,4 @@ export { default as UploadPrivateFile } from './UploadPrivateFile';
|
||||
export { default as UploadPrivateImage } from './UploadPrivateImage';
|
||||
export { default as UploadPrivatePtt } from './UploadPrivatePtt';
|
||||
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
|
||||
export { default as DownloadImage } from './DownloadImage';
|
||||
|
@@ -29,3 +29,4 @@ export * from "./oidb/Oidb.0xEB7";
|
||||
export * from "./oidb/Oidb.0xED3_1";
|
||||
export * from "./oidb/Oidb.0XFE1_2";
|
||||
export * from "./oidb/OidbBase";
|
||||
export * from "./oidb/Oidb.0xE07";
|
||||
|
@@ -342,11 +342,11 @@ export const QBigFaceExtra = {
|
||||
AniStickerPackId: ProtoField(1, ScalarType.STRING, true),
|
||||
AniStickerId: ProtoField(2, ScalarType.STRING, true),
|
||||
faceId: ProtoField(3, ScalarType.INT32, true),
|
||||
Field4: ProtoField(4, ScalarType.INT32, true),
|
||||
sourceType: ProtoField(4, ScalarType.INT32, true),
|
||||
AniStickerType: ProtoField(5, ScalarType.INT32, true),
|
||||
field6: ProtoField(6, ScalarType.STRING, true),
|
||||
resultId: ProtoField(6, ScalarType.STRING, true),
|
||||
preview: ProtoField(7, ScalarType.STRING, true),
|
||||
field9: ProtoField(9, ScalarType.INT32, true),
|
||||
randomType: ProtoField(9, ScalarType.INT32, true),
|
||||
};
|
||||
|
||||
export const QSmallFaceExtra = {
|
||||
|
18
src/core/packet/transformer/proto/message/groupAdmin.ts
Normal file
18
src/core/packet/transformer/proto/message/groupAdmin.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||
|
||||
export const GroupAdminExtra = {
|
||||
adminUid: ProtoField(1, ScalarType.STRING),
|
||||
isPromote: ProtoField(2, ScalarType.BOOL),
|
||||
};
|
||||
|
||||
export const GroupAdminBody = {
|
||||
extraDisable: ProtoField(1, () => GroupAdminExtra),
|
||||
extraEnable: ProtoField(2, () => GroupAdminExtra),
|
||||
};
|
||||
|
||||
export const GroupAdmin = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
flag: ProtoField(2, ScalarType.UINT32),
|
||||
isPromote: ProtoField(3, ScalarType.BOOL),
|
||||
body: ProtoField(4, () => GroupAdminBody),
|
||||
};
|
@@ -54,6 +54,32 @@ export const PushMsg = {
|
||||
generalFlag: ProtoField(9, ScalarType.INT32, true),
|
||||
};
|
||||
|
||||
export const GroupChangeInfo = {
|
||||
operator: ProtoField(1, () => GroupChangeOperator, true),
|
||||
};
|
||||
|
||||
export const GroupChangeOperator = {
|
||||
operatorUid: ProtoField(1, ScalarType.STRING, true),
|
||||
};
|
||||
|
||||
export const GroupChange = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
flag: ProtoField(2, ScalarType.UINT32),
|
||||
memberUid: ProtoField(3, ScalarType.STRING, true),
|
||||
decreaseType: ProtoField(4, ScalarType.UINT32),
|
||||
operatorInfo: ProtoField(5, ScalarType.BYTES, true),
|
||||
increaseType: ProtoField(6, ScalarType.UINT32),
|
||||
field7: ProtoField(7, ScalarType.BYTES, true),
|
||||
};
|
||||
|
||||
export const GroupInvite = {
|
||||
groupUin: ProtoField(1, ScalarType.UINT32),
|
||||
field2: ProtoField(2, ScalarType.UINT32),
|
||||
field3: ProtoField(2, ScalarType.UINT32),
|
||||
field4: ProtoField(2, ScalarType.UINT32),
|
||||
invitorUid: ProtoField(5, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const PushMsgBody = {
|
||||
responseHead: ProtoField(1, () => ResponseHead),
|
||||
contentHead: ProtoField(2, () => ContentHead),
|
||||
|
59
src/core/packet/transformer/proto/oidb/Oidb.0xE07.ts
Normal file
59
src/core/packet/transformer/proto/oidb/Oidb.0xE07.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ProtoField, ScalarType } from "@napneko/nap-proto-core";
|
||||
|
||||
export const OidbSvcTrpcTcp0xE07_0 = {
|
||||
version: ProtoField(1, ScalarType.UINT32),
|
||||
client: ProtoField(2, ScalarType.UINT32),
|
||||
entrance: ProtoField(3, ScalarType.UINT32),
|
||||
ocrReqBody: ProtoField(10, () => OcrReqBody, true),
|
||||
};
|
||||
|
||||
export const OcrReqBody = {
|
||||
imageUrl: ProtoField(1, ScalarType.STRING),
|
||||
languageType: ProtoField(2, ScalarType.UINT32),
|
||||
scene: ProtoField(3, ScalarType.UINT32),
|
||||
originMd5: ProtoField(10, ScalarType.STRING),
|
||||
afterCompressMd5: ProtoField(11, ScalarType.STRING),
|
||||
afterCompressFileSize: ProtoField(12, ScalarType.STRING),
|
||||
afterCompressWeight: ProtoField(13, ScalarType.STRING),
|
||||
afterCompressHeight: ProtoField(14, ScalarType.STRING),
|
||||
isCut: ProtoField(15, ScalarType.BOOL),
|
||||
};
|
||||
|
||||
export const OidbSvcTrpcTcp0xE07_0_Response = {
|
||||
retCode: ProtoField(1, ScalarType.INT32),
|
||||
errMsg: ProtoField(2, ScalarType.STRING),
|
||||
wording: ProtoField(3, ScalarType.STRING),
|
||||
ocrRspBody: ProtoField(10, () => OcrRspBody),
|
||||
};
|
||||
|
||||
export const OcrRspBody = {
|
||||
textDetections: ProtoField(1, () => TextDetection, false, true),
|
||||
language: ProtoField(2, ScalarType.STRING),
|
||||
requestId: ProtoField(3, ScalarType.STRING),
|
||||
ocrLanguageList: ProtoField(101, ScalarType.STRING, false, true),
|
||||
dstTranslateLanguageList: ProtoField(102, ScalarType.STRING, false, true),
|
||||
languageList: ProtoField(103, () => Language, false, true),
|
||||
afterCompressWeight: ProtoField(111, ScalarType.UINT32),
|
||||
afterCompressHeight: ProtoField(112, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const TextDetection = {
|
||||
detectedText: ProtoField(1, ScalarType.STRING),
|
||||
confidence: ProtoField(2, ScalarType.UINT32),
|
||||
polygon: ProtoField(3, () => Polygon),
|
||||
advancedInfo: ProtoField(4, ScalarType.STRING),
|
||||
};
|
||||
|
||||
export const Polygon = {
|
||||
coordinates: ProtoField(1, () => Coordinate, false, true),
|
||||
};
|
||||
|
||||
export const Coordinate = {
|
||||
x: ProtoField(1, ScalarType.INT32),
|
||||
y: ProtoField(2, ScalarType.INT32),
|
||||
};
|
||||
|
||||
export const Language = {
|
||||
languageCode: ProtoField(1, ScalarType.STRING),
|
||||
languageDesc: ProtoField(2, ScalarType.STRING),
|
||||
};
|
@@ -189,8 +189,8 @@ export const VideoExtBizInfo = {
|
||||
export const PicExtBizInfo = {
|
||||
BizType: ProtoField(1, ScalarType.UINT32),
|
||||
TextSummary: ProtoField(2, ScalarType.STRING),
|
||||
BytesPbReserveC2c: ProtoField(11, ScalarType.BYTES),
|
||||
BytesPbReserveTroop: ProtoField(12, ScalarType.BYTES),
|
||||
BytesPbReserveC2c: ProtoField(11, () => BytesPbReserveC2c),
|
||||
BytesPbReserveTroop: ProtoField(12, () => BytesPbReserveTroop),
|
||||
FromScene: ProtoField(1001, ScalarType.UINT32),
|
||||
ToScene: ProtoField(1002, ScalarType.UINT32),
|
||||
OldFileId: ProtoField(1003, ScalarType.UINT32),
|
||||
@@ -211,3 +211,27 @@ export const UploadInfo = {
|
||||
FileInfo: ProtoField(1, () => FileInfo),
|
||||
SubFileType: ProtoField(2, ScalarType.UINT32),
|
||||
};
|
||||
|
||||
export const BytesPbReserveC2c = {
|
||||
subType: ProtoField(1, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
field4: ProtoField(4, ScalarType.UINT32),
|
||||
field8: ProtoField(8, ScalarType.STRING),
|
||||
field10: ProtoField(10, ScalarType.UINT32),
|
||||
field12: ProtoField(12, ScalarType.STRING),
|
||||
field18: ProtoField(18, ScalarType.STRING),
|
||||
field19: ProtoField(19, ScalarType.STRING),
|
||||
field20: ProtoField(20, ScalarType.BYTES),
|
||||
};
|
||||
|
||||
export const BytesPbReserveTroop = {
|
||||
subType: ProtoField(1, ScalarType.UINT32),
|
||||
field3: ProtoField(3, ScalarType.UINT32),
|
||||
field4: ProtoField(4, ScalarType.UINT32),
|
||||
field9: ProtoField(9, ScalarType.STRING),
|
||||
field10: ProtoField(10, ScalarType.UINT32),
|
||||
field12: ProtoField(12, ScalarType.STRING),
|
||||
field18: ProtoField(18, ScalarType.STRING),
|
||||
field19: ProtoField(19, ScalarType.STRING),
|
||||
field21: ProtoField(21, ScalarType.BYTES),
|
||||
};
|
||||
|
17
src/core/services/NodeIKernelBaseEmojiService.ts
Normal file
17
src/core/services/NodeIKernelBaseEmojiService.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DownloadBaseEmojiByIdReq, DownloadBaseEmojiByUrlReq, GetBaseEmojiPathReq, PullSysEmojisReq } from '../types';
|
||||
|
||||
export interface NodeIKernelBaseEmojiService {
|
||||
removeKernelBaseEmojiListener(listenerId: number): void;
|
||||
|
||||
addKernelBaseEmojiListener(listener: unknown): number;
|
||||
|
||||
isBaseEmojiPathExist(args: Array<string>): unknown;
|
||||
|
||||
fetchFullSysEmojis(pullSysEmojisReq: PullSysEmojisReq): unknown;
|
||||
|
||||
getBaseEmojiPathByIds(getBaseEmojiPathReqs: Array<GetBaseEmojiPathReq>): unknown;
|
||||
|
||||
downloadBaseEmojiByIdWithUrl(downloadBaseEmojiByUrlReq: DownloadBaseEmojiByUrlReq): unknown;
|
||||
|
||||
downloadBaseEmojiById(downloadBaseEmojiByIdReq: DownloadBaseEmojiByIdReq): unknown;
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
import { NodeIKernelBuddyListener } from '@/core/listeners';
|
||||
import { BuddyListReqType } from '../types/user';
|
||||
import { BuddyListReqType } from '@/core/types/user';
|
||||
|
||||
export interface NodeIKernelBuddyService {
|
||||
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
|
||||
|
@@ -149,7 +149,7 @@ export interface NodeIKernelGroupService {
|
||||
|
||||
getGroupExtList(force: boolean): Promise<GeneralCallResult>;
|
||||
|
||||
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<unknown>;
|
||||
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<GeneralCallResult>;
|
||||
|
||||
getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req
|
||||
|
||||
@@ -163,7 +163,7 @@ export interface NodeIKernelGroupService {
|
||||
|
||||
getGroupPortrait(): void;
|
||||
|
||||
modifyGroupName(groupCode: string, groupName: string, arg: false): void;
|
||||
modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise<GeneralCallResult>;
|
||||
|
||||
modifyGroupRemark(groupCode: string, remark: string): void;
|
||||
|
||||
@@ -187,13 +187,13 @@ export interface NodeIKernelGroupService {
|
||||
|
||||
destroyGroup(groupCode: string): void;
|
||||
|
||||
getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise<GeneralCallResult>;
|
||||
getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise<GeneralCallResult>;
|
||||
|
||||
clearGroupNotifies(groupCode: string): void;
|
||||
|
||||
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>;
|
||||
getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>;
|
||||
|
||||
clearGroupNotifiesUnreadCount(unknown: boolean): void;
|
||||
clearGroupNotifiesUnreadCount(doubt: boolean): void;
|
||||
|
||||
operateSysNotify(
|
||||
doubt: boolean,
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/types';
|
||||
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
|
||||
import { GeneralCallResult } from '@/core/services/common';
|
||||
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '../types/msg';
|
||||
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg';
|
||||
|
||||
export interface NodeIKernelMsgService {
|
||||
buildMultiForwardMsg(req: { srcMsgIds: Array<string>, srcContact: Peer }): Promise<GeneralCallResult & { rspInfo: { elements: unknown } }>;
|
||||
|
||||
generateMsgUniqueId(chatType: number, time: string): string;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user