mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
341 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 反馈前,请确保您:
|
在提交新的 Bug 反馈前,请确保您:
|
||||||
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
* 已经搜索了现有的 issues,并且没有找到可以解决您问题的方法
|
||||||
* 不与现有的某一 issue 重复
|
* 不与现有的某一 issue 重复
|
||||||
* 不涉及[已经停止维护的特性](https://github.com/NapNeko/NapCatQQ?tab=readme-ov-file#挥别昨日),例如 CQ 码
|
|
||||||
- type: input
|
- type: input
|
||||||
id: system-version
|
id: system-version
|
||||||
attributes:
|
attributes:
|
||||||
label: 系统版本
|
label: 系统版本
|
||||||
description: 运行 QQNT 的系统版本
|
description: 运行 QQNT 的系统版本
|
||||||
placeholder: Windows 10 Pro Workstation 22H2
|
placeholder: Windows 11 24H2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -24,7 +23,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: QQNT 版本
|
label: QQNT 版本
|
||||||
description: 可在 QQNT 的「关于」的设置页中找到
|
description: 可在 QQNT 的「关于」的设置页中找到
|
||||||
placeholder: 9.9.7-21804
|
placeholder: 9.9.16-29927
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -40,21 +39,21 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: OneBot 客户端
|
label: OneBot 客户端
|
||||||
description: 连接至 NapCat 的客户端版本信息
|
description: 连接至 NapCat 的客户端版本信息
|
||||||
placeholder: Overflow 2.16.0-2cf7991-SNAPSHOT
|
placeholder: Karin 1.0.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
label: 发生了什么?
|
label: 发生了什么?
|
||||||
description: 填写你认为的 NapCat 的不正常行为
|
description: 填写你认为的 NapCat 的异常行为
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: how-reproduce
|
id: how-reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: 如何复现
|
label: 如何复现
|
||||||
description: 填写应当如何操作才能触发这个不正常行为
|
description: 填写应当如何操作才能触发这个异常行为
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. xxx
|
1. xxx
|
||||||
2. 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
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "npm" # See documentation for possible values
|
- package-ecosystem: "npm"
|
||||||
directory: "/" # Location of package manifests
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "weekly"
|
||||||
|
210
LICENSE
210
LICENSE
@@ -1,201 +1,19 @@
|
|||||||
Apache License
|
Limited Redistribution License for NapCat
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
3. Non-Commercial Use:
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
- This code is not to be used for any commercial purposes.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
4. Additional Permissions:
|
||||||
the copyright owner that is granting the License.
|
- Any rights not explicitly addressed in this license must be requested from and granted by the main author of the NapCat repository.
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
5. Disclaimer:
|
||||||
other entities that control, are controlled by, or are under common
|
- 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.
|
||||||
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.
|
|
||||||
|
|
||||||
"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.
|
|
||||||
|
28
README.md
28
README.md
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,11 +30,25 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
|||||||
|
|
||||||
[Cloudflare.Pages](https://napneko.pages.dev/)
|
[Cloudflare.Pages](https://napneko.pages.dev/)
|
||||||
|
|
||||||
[Server.Other](https://napcat.cyou/)
|
[Server.Other](https://docs.napcat.cyou/)
|
||||||
|
|
||||||
|
[Qbot.News](https://neko.qbot.news)
|
||||||
|
|
||||||
## 回家旅途
|
## 回家旅途
|
||||||
[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/uqh4I87KoM)
|
||||||
|
|
||||||
|
[Telegram](https://t.me/MelodicMoonlight)
|
||||||
|
|
||||||
|
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
|
||||||
|
|
||||||
|
## 性能设计/协议标准
|
||||||
|
NapCat 已实现90%+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
|
||||||
|
|
||||||
|
由此设计带来一系列好处,在开发中,获取群员列表通常小于50Ms,单条文本消息发送在320Ms以内,在1k+的群聊流畅运行,同时带来一些副作用,上报数据中大量使用Magic生成字段,消息Id无法持久,无法上报撤回消息原始内容。
|
||||||
|
|
||||||
|
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
|
||||||
|
|
||||||
## 感谢他们
|
## 感谢他们
|
||||||
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
|
||||||
@@ -45,12 +59,8 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 延缓Native模块与NapCat对新版QQ适配
|
## 特殊感谢
|
||||||
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
|
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
|
||||||
|
|
||||||
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
|
|
||||||
|
|
||||||
因此此时推荐使用release指定版本
|
|
||||||
|
|
||||||
## 开源附加
|
## 开源附加
|
||||||
|
|
||||||
|
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",
|
"name": "qq-chat",
|
||||||
"version": "9.9.16-29456",
|
"version": "9.9.17-30899",
|
||||||
"verHash": "dd395162",
|
"verHash": "ececf273",
|
||||||
"linuxVersion": "3.2.13-29456",
|
"linuxVersion": "3.2.15-30899",
|
||||||
"linuxVerHash": "e379390a",
|
"linuxVerHash": "63c751e8",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "QQ",
|
"description": "QQ",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"qd": "externals/devtools/cli/index.js"
|
"qd": "externals/devtools/cli/index.js"
|
||||||
},
|
},
|
||||||
"main": "./loadNapCat.js",
|
"main": "./loadNapCat.js",
|
||||||
"buildVersion": "29456",
|
"buildVersion": "30899",
|
||||||
"isPureShell": true,
|
"isPureShell": true,
|
||||||
"isByteCodeShell": true,
|
"isByteCodeShell": true,
|
||||||
"platform": "win32",
|
"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,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.1.12",
|
"version": "4.2.51",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NapCat WebUI</title>
|
<title>NapCat WebUI</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="./src/main.ts"></script>
|
<script type="module" src="./src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -5,21 +5,24 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
|
||||||
"webui:dev": "vite",
|
"webui:dev": "vite --host",
|
||||||
"webui:build": "vite build",
|
"webui:build": "vite build",
|
||||||
"webui:preview": "vite preview"
|
"webui:preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"event-source-polyfill": "^1.0.31",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"tdesign-icons-vue-next": "^0.3.3",
|
"tdesign-icons-vue-next": "^0.3.3",
|
||||||
"tdesign-vue-next": "^1.10.3",
|
"tdesign-vue-next": "^1.10.3",
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@types/event-source-polyfill": "^1.0.5",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@vitejs/plugin-legacy": "^5.4.3",
|
"@vitejs/plugin-legacy": "^5.4.3",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@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>
|
<template>
|
||||||
<div id="app">
|
<div id="app" theme-mode="dark">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</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>
|
</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 { OneBotConfig } from '../../../src/onebot/config/config';
|
||||||
|
import { ResponseCode } from '../../../src/webui/src/const/status';
|
||||||
export class QQLoginManager {
|
export class QQLoginManager {
|
||||||
private retCredential: string;
|
private retCredential: string;
|
||||||
private readonly apiPrefix: string;
|
private readonly apiPrefix: string;
|
||||||
@@ -22,8 +22,8 @@ export class QQLoginManager {
|
|||||||
});
|
});
|
||||||
if (ConfigResponse.status == 200) {
|
if (ConfigResponse.status == 200) {
|
||||||
const ConfigResponseJson = await ConfigResponse.json();
|
const ConfigResponseJson = await ConfigResponse.json();
|
||||||
if (ConfigResponseJson.code == 0) {
|
if (ConfigResponseJson.code == ResponseCode.Success) {
|
||||||
return ConfigResponseJson?.data as OneBotConfig;
|
return ConfigResponseJson.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -74,6 +74,26 @@ export class QQLoginManager {
|
|||||||
}
|
}
|
||||||
return false;
|
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> {
|
public async checkWebUiLogined(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
|
@@ -1,16 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="dashboard-container">
|
<t-layout class="dashboard-container">
|
||||||
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
|
<div v-if="!mediaQuery.matches">
|
||||||
<div class="content">
|
<SidebarMenu
|
||||||
<router-view />
|
:menu-items="menuItems"
|
||||||
|
class="sidebar-menu"
|
||||||
|
:menu-width="sidebarWidth"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<t-layout>
|
||||||
|
<router-view />
|
||||||
|
</t-layout>
|
||||||
|
<div v-if="mediaQuery.matches" class="bottom-menu">
|
||||||
|
<BottomMenu :menu-items="menuItems" />
|
||||||
|
</div>
|
||||||
|
</t-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import SidebarMenu from './webui/Nav.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 {
|
interface MenuItem {
|
||||||
value: string;
|
value: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -25,6 +37,19 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
|
||||||
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -32,19 +57,18 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu {
|
.sidebar-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
.bottom-menu {
|
||||||
.content {
|
position: fixed;
|
||||||
flex: 1;
|
bottom: 0;
|
||||||
/* padding: 20px; */
|
width: 100%;
|
||||||
overflow: auto;
|
z-index: 2;
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@@ -53,3 +77,19 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="login-container">
|
<t-card class="layout" :bordered="false">
|
||||||
<h2 class="sotheby-font">QQ Login</h2>
|
<div class="login-container">
|
||||||
<div class="login-methods">
|
<h2 class="sotheby-font">QQ Login</h2>
|
||||||
<t-button
|
<div class="login-methods">
|
||||||
id="quick-login"
|
<t-tooltip content="快速登录">
|
||||||
class="login-method"
|
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
|
||||||
:class="{ active: loginMethod === 'quick' }"
|
@click="loginMethod = 'quick'">Quick Login</t-button>
|
||||||
@click="loginMethod = 'quick'"
|
</t-tooltip>
|
||||||
>Quick Login</t-button
|
<t-tooltip content="二维码登录">
|
||||||
>
|
<t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
|
||||||
<t-button
|
@click="loginMethod = 'qrcode'">QR Code</t-button>
|
||||||
id="qrcode-login"
|
</t-tooltip>
|
||||||
class="login-method"
|
</div>
|
||||||
:class="{ active: loginMethod === 'qrcode' }"
|
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
||||||
@click="loginMethod = 'qrcode'"
|
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
|
||||||
>QR Code</t-button
|
@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>
|
||||||
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
|
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||||
<t-select
|
</t-card>
|
||||||
id="quick-login-select"
|
|
||||||
v-model="selectedAccount"
|
|
||||||
placeholder="Select Account"
|
|
||||||
@change="selectAccount"
|
|
||||||
>
|
|
||||||
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
|
|
||||||
</t-select>
|
|
||||||
</div>
|
|
||||||
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
|
|
||||||
<canvas ref="qrcodeCanvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
@@ -47,10 +40,14 @@ const selectedAccount = ref<string>('');
|
|||||||
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
|
||||||
let heartBeatTimer: number | null = null;
|
let heartBeatTimer: number | null = null;
|
||||||
|
let qrcodeUrl: string = '';
|
||||||
|
|
||||||
const selectAccount = async (accountName: string): Promise<void> => {
|
const selectAccount = async (accountName: string): Promise<void> => {
|
||||||
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (heartBeatTimer) {
|
||||||
|
clearInterval(heartBeatTimer);
|
||||||
|
}
|
||||||
await MessagePlugin.success('登录成功即将跳转');
|
await MessagePlugin.success('登录成功即将跳转');
|
||||||
await router.push({ path: '/dashboard/basic-info' });
|
await router.push({ path: '/dashboard/basic-info' });
|
||||||
} else {
|
} else {
|
||||||
@@ -73,36 +70,61 @@ const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const HeartBeat = async (): Promise<void> => {
|
const HeartBeat = async (): Promise<void> => {
|
||||||
const isLogined = await qqLoginManager.checkQQLoginStatus();
|
const isLogined = await qqLoginManager.checkQQLoginStatusWithQrcode();
|
||||||
if (isLogined) {
|
if (isLogined?.isLogin) {
|
||||||
if (heartBeatTimer) {
|
if (heartBeatTimer) {
|
||||||
clearInterval(heartBeatTimer);
|
clearInterval(heartBeatTimer);
|
||||||
}
|
}
|
||||||
|
await MessagePlugin.success('登录成功即将跳转');
|
||||||
await router.push({ path: '/dashboard/basic-info' });
|
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> => {
|
const InitPages = async (): Promise<void> => {
|
||||||
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
|
||||||
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
|
qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
|
||||||
generateQrCode(qrcodeData, qrcodeCanvas.value);
|
await nextTick();
|
||||||
heartBeatTimer = window.setInterval(HeartBeat, 3000);
|
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.layout {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: white;
|
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 50px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@@ -161,7 +183,5 @@ onMounted(() => {
|
|||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
|
||||||
background-color: white;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@@ -1,20 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<t-card class="layout" :bordered="false">
|
||||||
<h2 class="sotheby-font">WebUi Login</h2>
|
<div class="login-container">
|
||||||
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
<h2 class="sotheby-font">WebUi Login</h2>
|
||||||
<t-form-item name="password">
|
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
|
||||||
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
<t-form-item name="password">
|
||||||
<template #prefix-icon>
|
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
|
||||||
<lock-on-icon />
|
<template #prefix-icon>
|
||||||
</template>
|
<lock-on-icon />
|
||||||
</t-input>
|
</template>
|
||||||
</t-form-item>
|
</t-input>
|
||||||
<t-form-item>
|
</t-form-item>
|
||||||
<t-button theme="primary" type="submit" block>登录</t-button>
|
<t-form-item>
|
||||||
</t-form-item>
|
<t-button theme="primary" type="submit" block>登录</t-button>
|
||||||
</t-form>
|
</t-form-item>
|
||||||
</div>
|
</t-form>
|
||||||
<div class="footer">Power By NapCat.WebUi</div>
|
</div>
|
||||||
|
<t-footer class="footer">Power By NapCat.WebUi</t-footer>
|
||||||
|
</t-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -94,14 +96,16 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.layout {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
.login-container {
|
.login-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: white;
|
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 50px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@@ -145,7 +149,5 @@ const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
|
|||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100%;
|
|
||||||
background-color: white;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,16 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
|
<t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
|
||||||
<template #logo> </template>
|
<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">
|
<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">
|
<t-tooltip :disabled="!collapsed" :content="item.label" placement="right">
|
||||||
<template #icon>
|
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
|
||||||
<t-icon :name="item.icon" />
|
<template #icon>
|
||||||
</template>
|
<t-icon :name="item.icon" />
|
||||||
{{ item.label }}
|
</template>
|
||||||
</t-menu-item>
|
{{ item.label }}
|
||||||
|
</t-menu-item>
|
||||||
|
</t-tooltip>
|
||||||
</router-link>
|
</router-link>
|
||||||
<template #operations>
|
<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>
|
<template #icon><t-icon :name="iconName" /></template>
|
||||||
</t-button>
|
</t-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,7 +33,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineProps } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import emitter from '@/ts/event-bus';
|
||||||
|
|
||||||
type MenuItem = {
|
type MenuItem = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -27,19 +43,42 @@ type MenuItem = {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
menuItems: MenuItem[];
|
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 collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
|
||||||
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
|
||||||
|
const disBtn = ref<boolean>(false);
|
||||||
|
|
||||||
const changeCollapsed = (): void => {
|
const changeCollapsed = (): void => {
|
||||||
collapsed.value = !collapsed.value;
|
collapsed.value = !collapsed.value;
|
||||||
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
|
||||||
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -57,12 +96,28 @@ const changeCollapsed = (): void => {
|
|||||||
width: 100px; /* 移动端侧边栏宽度 */
|
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 {
|
.logo-text {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: Sotheby, Helvetica, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item {
|
.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>
|
@@ -3,4 +3,11 @@
|
|||||||
src: url('../assets/Sotheby.ttf') format('truetype');
|
src: url('../assets/Sotheby.ttf') format('truetype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: 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,
|
List as TList,
|
||||||
Alert as TAlert,
|
Alert as TAlert,
|
||||||
Tag as TTag,
|
Tag as TTag,
|
||||||
|
Descriptions as TDescriptionsProps,
|
||||||
|
DescriptionsItem as TDescriptionsItem,
|
||||||
|
Collapse as TCollapse,
|
||||||
|
CollapsePanel as TCollapsePanel,
|
||||||
ListItem as TListItem,
|
ListItem as TListItem,
|
||||||
Tabs as TTabs,
|
Tabs as TTabs,
|
||||||
TabPanel as TTabPanel,
|
TabPanel as TTabPanel,
|
||||||
@@ -27,10 +31,23 @@ import {
|
|||||||
Popup as TPopup,
|
Popup as TPopup,
|
||||||
Dialog as TDialog,
|
Dialog as TDialog,
|
||||||
Switch as TSwitch,
|
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';
|
} from 'tdesign-vue-next';
|
||||||
import { router } from './router';
|
import router from './router';
|
||||||
import 'tdesign-vue-next/es/style/index.css';
|
import 'tdesign-vue-next/es/style/index.css';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(TButton);
|
app.use(TButton);
|
||||||
@@ -51,6 +68,10 @@ app.use(TLink);
|
|||||||
app.use(TList);
|
app.use(TList);
|
||||||
app.use(TAlert);
|
app.use(TAlert);
|
||||||
app.use(TTag);
|
app.use(TTag);
|
||||||
|
app.use(TDescriptionsProps);
|
||||||
|
app.use(TDescriptionsItem);
|
||||||
|
app.use(TCollapse);
|
||||||
|
app.use(TCollapsePanel);
|
||||||
app.use(TListItem);
|
app.use(TListItem);
|
||||||
app.use(TTabs);
|
app.use(TTabs);
|
||||||
app.use(TTabPanel);
|
app.use(TTabPanel);
|
||||||
@@ -59,4 +80,18 @@ app.use(TCheckbox);
|
|||||||
app.use(TPopup);
|
app.use(TPopup);
|
||||||
app.use(TDialog);
|
app.use(TDialog);
|
||||||
app.use(TSwitch);
|
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');
|
app.mount('#app');
|
||||||
|
@@ -1,23 +1,101 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="about-us">
|
<div class="about-us">
|
||||||
<div>
|
<div>
|
||||||
<t-divider content="面板关于信息" align="left" />
|
<t-divider content="面板关于信息" align="left">
|
||||||
<t-alert theme="success" message="NapCat.WebUi is running" />
|
<template #content>
|
||||||
<t-list class="list">
|
<div style="display: flex; justify-content: center; align-items: center">
|
||||||
<t-list-item class="list-item">
|
<info-circle-icon></info-circle-icon>
|
||||||
<span class="item-label">开发人员:</span>
|
<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">
|
<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>
|
</span>
|
||||||
</t-list-item>
|
</t-list-item>
|
||||||
<t-list-item class="list-item">
|
<t-list-item>
|
||||||
<span class="item-label">版本信息:</span>
|
<tips-filled-icon class="item-icon" size="large" />
|
||||||
|
<span class="item-label">issues:</span>
|
||||||
<span class="item-content">
|
<span class="item-content">
|
||||||
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag>
|
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
|
||||||
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
|
githubBastData?.open_issues_count
|
||||||
<t-tag class="tag-item" theme="success">
|
}}</t-link>
|
||||||
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
|
</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>
|
||||||
|
<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>
|
</span>
|
||||||
</t-list-item>
|
</t-list-item>
|
||||||
</t-list>
|
</t-list>
|
||||||
@@ -28,6 +106,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import pkg from '../../package.json';
|
import pkg from '../../package.json';
|
||||||
import { napCatVersion } from '../../../src/common/version';
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -35,23 +158,26 @@ import { napCatVersion } from '../../../src/common/version';
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.label-box {
|
||||||
.list {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: center;
|
||||||
}
|
|
||||||
|
|
||||||
.list-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: 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 {
|
.item-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: bold;
|
margin-left: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-content {
|
.item-content {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -64,3 +190,37 @@ import { napCatVersion } from '../../../src/common/version';
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="log-view">
|
<div class="title">
|
||||||
<h1>面板日志信息</h1>
|
<t-divider content="日志查看" align="left">
|
||||||
<p>这里显示面板的日志信息。</p>
|
<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>
|
||||||
|
<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>
|
</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,400 @@
|
|||||||
<template>
|
<template>
|
||||||
<t-space class="full-space">
|
<div ref="headerBox" class="title">
|
||||||
<template v-if="clientPanelData.length > 0">
|
<t-divider content="网络配置" align="left">
|
||||||
<t-tabs
|
<template #content>
|
||||||
v-model="activeTab"
|
<div style="display: flex; justify-content: center; align-items: center">
|
||||||
:addable="true"
|
<wifi1-icon />
|
||||||
theme="card"
|
<div style="margin-left: 5px">网络配置</div>
|
||||||
@add="showAddTabDialog"
|
</div>
|
||||||
@remove="removeTab"
|
</template>
|
||||||
class="full-tabs"
|
</t-divider>
|
||||||
|
<t-divider align="right">
|
||||||
|
<t-button @click="addConfig()">
|
||||||
|
<template #icon><add-icon /></template>
|
||||||
|
添加配置</t-button
|
||||||
>
|
>
|
||||||
<t-tab-panel
|
</t-divider>
|
||||||
v-for="(config, idx) in clientPanelData"
|
</div>
|
||||||
:key="idx"
|
<div v-if="loadPage" ref="setting" class="setting">
|
||||||
:label="config.name"
|
<t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
|
||||||
:removable="true"
|
<t-tab-panel value="all" label="全部"></t-tab-panel>
|
||||||
:value="idx"
|
<t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
|
||||||
class="full-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"
|
||||||
>
|
>
|
||||||
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
|
<template #actions>
|
||||||
<div class="button-container">
|
<t-space>
|
||||||
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button>
|
<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>
|
</div>
|
||||||
</t-tab-panel>
|
</t-card>
|
||||||
</t-tabs>
|
</div>
|
||||||
</template>
|
<div style="height: 20vh"></div>
|
||||||
<template v-else>
|
</div>
|
||||||
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
|
<t-card v-else>
|
||||||
</template>
|
<t-empty class="card-none" title="暂无网络配置"> </t-empty>
|
||||||
<t-dialog
|
</t-card>
|
||||||
v-model:visible="isDialogVisible"
|
</div>
|
||||||
header="添加网络配置"
|
<t-dialog
|
||||||
@close="isDialogVisible = false"
|
v-model:visible="visibleBody"
|
||||||
@confirm="addTab"
|
:header="dialogTitle"
|
||||||
>
|
:destroy-on-close="true"
|
||||||
<t-form ref="form" :model="newTab">
|
:show-in-attached-element="true"
|
||||||
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
|
: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-input v-model="newTab.name" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
|
<t-form-item
|
||||||
<t-select v-model="newTab.type">
|
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="httpServers">HTTP 服务器</t-option>
|
||||||
<t-option value="httpClients">HTTP 客户端</t-option>
|
<t-option value="httpClients">HTTP 客户端</t-option>
|
||||||
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
<t-option value="websocketServers">WebSocket 服务器</t-option>
|
||||||
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
<t-option value="websocketClients">WebSocket 客户端</t-option>
|
||||||
</t-select>
|
</t-select>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<div>
|
||||||
|
<component
|
||||||
|
:is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
|
||||||
|
:config="newTab.data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
</t-dialog>
|
</div>
|
||||||
</t-space>
|
</t-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted } from 'vue';
|
|
||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
|
||||||
import {
|
import {
|
||||||
httpServerDefaultConfigs,
|
AddIcon,
|
||||||
httpClientDefaultConfigs,
|
DeleteIcon,
|
||||||
websocketServerDefaultConfigs,
|
Edit2Icon,
|
||||||
websocketClientDefaultConfigs,
|
ServerFilledIcon,
|
||||||
HttpClientConfig,
|
CopyIcon,
|
||||||
HttpServerConfig,
|
BrowseOffIcon,
|
||||||
WebsocketClientConfig,
|
BrowseIcon,
|
||||||
WebsocketServerConfig,
|
Wifi1Icon,
|
||||||
|
} from 'tdesign-icons-vue-next';
|
||||||
|
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
|
||||||
|
import emitter from '@/ts/event-bus';
|
||||||
|
import {
|
||||||
|
mergeNetworkDefaultConfig,
|
||||||
|
mergeOneBotConfigs,
|
||||||
NetworkConfig,
|
NetworkConfig,
|
||||||
OneBotConfig,
|
OneBotConfig,
|
||||||
mergeOneBotConfigs,
|
|
||||||
} from '../../../src/onebot/config/config';
|
} from '../../../src/onebot/config/config';
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
|
||||||
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
|
||||||
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
|
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
|
||||||
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
|
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
|
||||||
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.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';
|
||||||
|
|
||||||
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
|
const showToken = ref<boolean>(false);
|
||||||
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
|
const infoOneCol = ref<boolean>(true);
|
||||||
type ComponentUnion =
|
const tabsWidth = ref<number>(0);
|
||||||
|
const menuWidth = ref<number>(0);
|
||||||
|
const cardWidth = ref<number>(0);
|
||||||
|
const cardHeight = ref<number>(0);
|
||||||
|
const mediumScreen = window.matchMedia('(min-width: 768px) and (max-width: 1024px)');
|
||||||
|
const largeScreen = window.matchMedia('(min-width: 1025px)');
|
||||||
|
const headerBox = ref<HTMLDivElement | null>(null);
|
||||||
|
const setting = ref<HTMLDivElement | null>(null);
|
||||||
|
const loadPage = ref<boolean>(false);
|
||||||
|
const visibleBody = ref<boolean>(false);
|
||||||
|
const newTab = ref<{ name: string; data: any; type: string }>({ name: '', data: {}, type: '' });
|
||||||
|
const dialogTitle = ref<string>('');
|
||||||
|
|
||||||
|
type ComponentKey = keyof typeof mergeNetworkDefaultConfig;
|
||||||
|
|
||||||
|
const componentMap: Record<
|
||||||
|
ComponentKey,
|
||||||
| typeof HttpServerComponent
|
| typeof HttpServerComponent
|
||||||
| typeof HttpClientComponent
|
| typeof HttpClientComponent
|
||||||
| typeof WebsocketServerComponent
|
| typeof WebsocketServerComponent
|
||||||
| typeof WebsocketClientComponent;
|
| typeof WebsocketClientComponent
|
||||||
|
> = {
|
||||||
const componentMap: Record<ConfigKey, ComponentUnion> = {
|
|
||||||
httpServers: HttpServerComponent,
|
httpServers: HttpServerComponent,
|
||||||
httpClients: HttpClientComponent,
|
httpClients: HttpClientComponent,
|
||||||
websocketServers: WebsocketServerComponent,
|
websocketServers: WebsocketServerComponent,
|
||||||
websocketClients: WebsocketClientComponent,
|
websocketClients: WebsocketClientComponent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
|
//操作类型
|
||||||
httpServers: httpServerDefaultConfigs,
|
const operateType = ref<string>('');
|
||||||
httpClients: httpClientDefaultConfigs,
|
//配置项索引
|
||||||
websocketServers: websocketServerDefaultConfigs,
|
const configIndex = ref<number>(0);
|
||||||
websocketClients: websocketClientDefaultConfigs,
|
//保存时所用数据
|
||||||
|
const networkConfig: NetworkConfig & { [key: string]: any } = {
|
||||||
|
websocketClients: [],
|
||||||
|
websocketServers: [],
|
||||||
|
httpClients: [],
|
||||||
|
httpServers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ConfigMap {
|
//挂载的数据
|
||||||
httpServers: HttpServerConfig;
|
const WebConfg = ref(
|
||||||
httpClients: HttpClientConfig;
|
new Map<string, Array<null>>([
|
||||||
websocketServers: WebsocketServerConfig;
|
['all', []],
|
||||||
websocketClients: WebsocketClientConfig;
|
['httpServers', []],
|
||||||
}
|
['httpClients', []],
|
||||||
|
['websocketServers', []],
|
||||||
interface ClientPanel<K extends ConfigKey = ConfigKey> {
|
['websocketClients', []],
|
||||||
name: string;
|
])
|
||||||
key: K;
|
);
|
||||||
data: ConfigMap[K];
|
const typeCh: Record<ComponentKey, string> = {
|
||||||
}
|
httpServers: 'HTTP 服务器',
|
||||||
|
httpClients: 'HTTP 客户端',
|
||||||
const activeTab = ref<number>(0);
|
websocketServers: 'WebSocket 服务器',
|
||||||
const isDialogVisible = ref(false);
|
websocketClients: 'WebSocket 客户端',
|
||||||
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
|
};
|
||||||
const clientPanelData: Ref<ClientPanel[]> = ref([]);
|
const cardConfig = ref<any>([]);
|
||||||
|
const getComponent = (type: ComponentKey) => {
|
||||||
const getComponent = (type: ConfigKey) => {
|
|
||||||
return componentMap[type];
|
return componentMap[type];
|
||||||
};
|
};
|
||||||
|
const getKeyByValue = (obj: typeof typeCh, value: string): string | undefined => {
|
||||||
|
return Object.entries(obj).find(([_, v]) => v === value)?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const addConfig = () => {
|
||||||
|
dialogTitle.value = '添加配置';
|
||||||
|
newTab.value = { name: '', data: {}, type: '' };
|
||||||
|
operateType.value = 'add';
|
||||||
|
visibleBody.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const editConfig = (item: any) => {
|
||||||
|
dialogTitle.value = '修改配置';
|
||||||
|
const type = getKeyByValue(typeCh, item.type);
|
||||||
|
if (type) {
|
||||||
|
newTab.value = { name: item.name, data: item, type: type };
|
||||||
|
}
|
||||||
|
operateType.value = 'edit';
|
||||||
|
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||||
|
visibleBody.value = true;
|
||||||
|
};
|
||||||
|
const toggleProperty = async (item: any, tagData: string) => {
|
||||||
|
const type = getKeyByValue(typeCh, item.type);
|
||||||
|
const newData = { ...item };
|
||||||
|
newData[tagData] = !item[tagData];
|
||||||
|
if (type) {
|
||||||
|
newTab.value = { name: item.name, data: newData, type: type };
|
||||||
|
}
|
||||||
|
operateType.value = 'edit';
|
||||||
|
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
|
||||||
|
await saveConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
const delConfig = (item: any) => {
|
||||||
|
const type = getKeyByValue(typeCh, item.type);
|
||||||
|
if (type) {
|
||||||
|
newTab.value = { name: item.name, data: item, type: type };
|
||||||
|
}
|
||||||
|
configIndex.value = configIndex.value = networkConfig[newTab.value.type].findIndex(
|
||||||
|
(obj: any) => obj.name === item.name
|
||||||
|
);
|
||||||
|
operateType.value = 'delete';
|
||||||
|
saveConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectType = (key: ComponentKey) => {
|
||||||
|
cardConfig.value = WebConfg.value.get(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onloadDefault = (key: ComponentKey) => {
|
||||||
|
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
|
||||||
|
};
|
||||||
|
//检测重名
|
||||||
|
const checkName = (name: string) => {
|
||||||
|
const allConfigs = WebConfg.value.get('all')?.findIndex((obj: any) => obj.name === name);
|
||||||
|
if (newTab.value.name === '' || newTab.value.type === '') {
|
||||||
|
MessagePlugin.error('请填写完整信息');
|
||||||
|
return false;
|
||||||
|
} else if (allConfigs === -1 || newTab.value.data.name === name) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error('名称已存在');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//保存
|
||||||
|
const saveConfig = async () => {
|
||||||
|
if (operateType.value == 'add') {
|
||||||
|
if (!checkName(newTab.value.name)) return;
|
||||||
|
newTab.value.data.name = newTab.value.name;
|
||||||
|
networkConfig[newTab.value.type].push(newTab.value.data);
|
||||||
|
} else if (operateType.value == 'edit') {
|
||||||
|
if (!checkName(newTab.value.name)) return;
|
||||||
|
newTab.value.data.name = newTab.value.name;
|
||||||
|
networkConfig[newTab.value.type][configIndex.value] = newTab.value.data;
|
||||||
|
} else if (operateType.value == 'delete') {
|
||||||
|
networkConfig[newTab.value.type].splice(configIndex.value, 1);
|
||||||
|
}
|
||||||
|
const userConfig = await getOB11Config();
|
||||||
|
if (!userConfig) return;
|
||||||
|
userConfig.network = networkConfig;
|
||||||
|
const success = await setOB11Config(userConfig);
|
||||||
|
if (success) {
|
||||||
|
operateType.value = '';
|
||||||
|
configIndex.value = 0;
|
||||||
|
MessagePlugin.success('配置保存成功');
|
||||||
|
await loadConfig();
|
||||||
|
visibleBody.value = false;
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error('配置保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||||
const storedCredential = localStorage.getItem('auth');
|
const storedCredential = localStorage.getItem('auth');
|
||||||
if (!storedCredential) {
|
if (!storedCredential) {
|
||||||
@@ -137,27 +415,27 @@ const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
|
|||||||
return await loginManager.SetOB11Config(config);
|
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 getAllData = (data: NetworkConfig) => {
|
||||||
};
|
cardConfig.value = [];
|
||||||
|
WebConfg.value.set('all', []);
|
||||||
const addConfigDataToPanel = (data: NetworkConfig) => {
|
for (const key in data) {
|
||||||
(Object.keys(data) as ConfigKey[]).forEach((key) => {
|
const configs = data[key as keyof NetworkConfig];
|
||||||
addToPanel(data[key], key);
|
if (key in mergeNetworkDefaultConfig) {
|
||||||
});
|
networkConfig[key] = [...configs];
|
||||||
};
|
const newConfigsArray = configs.map((config: any) => ({
|
||||||
|
...config,
|
||||||
const parsePanelData = (): NetworkConfig => {
|
type: typeCh[key as ComponentKey],
|
||||||
const result: NetworkConfig = {
|
}));
|
||||||
httpServers: [],
|
WebConfg.value.set(key, newConfigsArray);
|
||||||
httpClients: [],
|
const allConfigs = WebConfg.value.get('all');
|
||||||
websocketServers: [],
|
if (allConfigs) {
|
||||||
websocketClients: [],
|
const newAllConfigs = [...allConfigs, ...newConfigsArray];
|
||||||
};
|
WebConfg.value.set('all', newAllConfigs);
|
||||||
clientPanelData.value.forEach((panel) => {
|
}
|
||||||
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
|
cardConfig.value = WebConfg.value.get('all');
|
||||||
});
|
}
|
||||||
return result;
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
@@ -165,85 +443,212 @@ const loadConfig = async () => {
|
|||||||
const userConfig = await getOB11Config();
|
const userConfig = await getOB11Config();
|
||||||
if (!userConfig) return;
|
if (!userConfig) return;
|
||||||
const mergedConfig = mergeOneBotConfigs(userConfig);
|
const mergedConfig = mergeOneBotConfigs(userConfig);
|
||||||
addConfigDataToPanel(mergedConfig.network);
|
getAllData(mergedConfig.network);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading config:', error);
|
console.error('Error loading config:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveConfig = async () => {
|
const copyText = async (text: string) => {
|
||||||
const config = parsePanelData();
|
const textarea = document.createElement('textarea');
|
||||||
const userConfig = await getOB11Config();
|
textarea.value = text;
|
||||||
if (!userConfig) {
|
document.body.appendChild(textarea);
|
||||||
await MessagePlugin.error('无法获取配置!');
|
textarea.select();
|
||||||
return;
|
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) {
|
const handleResize = () => {
|
||||||
await MessagePlugin.success('配置保存成功');
|
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 {
|
} 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)
|
||||||
};
|
};
|
||||||
|
emitter.on('sendWidth', (width) => {
|
||||||
const showAddTabDialog = () => {
|
if (typeof width === 'string') {
|
||||||
newTab.value = { name: '', type: 'httpServers' };
|
const strWidth = width as string;
|
||||||
isDialogVisible.value = true;
|
menuWidth.value = parseInt(strWidth);
|
||||||
};
|
|
||||||
|
|
||||||
const addTab = async () => {
|
|
||||||
const { name, type } = newTab.value;
|
|
||||||
if (clientPanelData.value.some((panel) => panel.name === name)) {
|
|
||||||
await MessagePlugin.error('选项卡名称已存在');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const defaultConfig = structuredClone(defaultConfigMap[type]);
|
});
|
||||||
defaultConfig.name = name;
|
watch(menuWidth, (newValue, oldValue) => {
|
||||||
clientPanelData.value.push({ name, data: defaultConfig, key: type });
|
loadPage.value = false;
|
||||||
isDialogVisible.value = false;
|
setTimeout(()=>{
|
||||||
await nextTick();
|
handleResize();
|
||||||
activeTab.value = clientPanelData.value.length - 1;
|
},300)
|
||||||
await MessagePlugin.success('选项卡添加成功');
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
|
|
||||||
clientPanelData.value.splice(payload.index, 1);
|
|
||||||
activeTab.value = Math.max(0, activeTab.value - 1);
|
|
||||||
await saveConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadConfig();
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.full-space {
|
.title {
|
||||||
width: 100%;
|
padding: 20px 20px 0 20px;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-tabs {
|
.setting {
|
||||||
width: 100%;
|
margin: 0 20px;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
flex: 1;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
.local {
|
||||||
|
flex: 6;
|
||||||
|
margin: 0 10px 0 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-container {
|
.copy-icon {
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
margin-top: 20px;
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
@@ -1,22 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="title">
|
||||||
<t-divider content="其余配置" align="left" />
|
<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>
|
||||||
<div class="other-config-container">
|
<t-card class="card">
|
||||||
<div class="other-config">
|
<div class="other-config-container">
|
||||||
<t-form ref="form" :model="otherConfig" class="form">
|
<div class="other-config">
|
||||||
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
<t-form ref="form" :model="otherConfig" :label-align="labelAlign" label-width="auto" colon>
|
||||||
<t-input v-model="otherConfig.musicSignUrl" />
|
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
|
||||||
</t-form-item>
|
<t-input v-model="otherConfig.musicSignUrl" />
|
||||||
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
</t-form-item>
|
||||||
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
|
||||||
</t-form-item>
|
<t-switch v-model="otherConfig.enableLocalFile2Url" />
|
||||||
</t-form>
|
</t-form-item>
|
||||||
<div class="button-container">
|
<t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
|
||||||
<t-button @click="saveConfig">保存</t-button>
|
<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>
|
</div>
|
||||||
</div>
|
</t-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -24,12 +36,15 @@ import { ref, onMounted } from 'vue';
|
|||||||
import { MessagePlugin } from 'tdesign-vue-next';
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
import { OneBotConfig } from '../../../src/onebot/config/config';
|
import { OneBotConfig } from '../../../src/onebot/config/config';
|
||||||
import { QQLoginManager } from '@/backend/shell';
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
import { SettingIcon } from 'tdesign-icons-vue-next';
|
||||||
|
|
||||||
const otherConfig = ref<Partial<OneBotConfig>>({
|
const otherConfig = ref<Partial<OneBotConfig>>({
|
||||||
musicSignUrl: '',
|
musicSignUrl: '',
|
||||||
enableLocalFile2Url: false,
|
enableLocalFile2Url: false,
|
||||||
|
parseMultMsg: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const labelAlign = ref<string>();
|
||||||
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
|
||||||
const storedCredential = localStorage.getItem('auth');
|
const storedCredential = localStorage.getItem('auth');
|
||||||
if (!storedCredential) {
|
if (!storedCredential) {
|
||||||
@@ -56,6 +71,7 @@ const loadConfig = async () => {
|
|||||||
if (userConfig) {
|
if (userConfig) {
|
||||||
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
|
||||||
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
|
||||||
|
otherConfig.value.parseMultMsg = userConfig.parseMultMsg;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading config:', error);
|
console.error('Error loading config:', error);
|
||||||
@@ -68,6 +84,7 @@ const saveConfig = async () => {
|
|||||||
if (userConfig) {
|
if (userConfig) {
|
||||||
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
|
||||||
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
|
||||||
|
userConfig.parseMultMsg = otherConfig.value.parseMultMsg ?? true;
|
||||||
const success = await setOB11Config(userConfig);
|
const success = await setOB11Config(userConfig);
|
||||||
if (success) {
|
if (success) {
|
||||||
MessagePlugin.success('配置保存成功');
|
MessagePlugin.success('配置保存成功');
|
||||||
@@ -80,55 +97,60 @@ const saveConfig = async () => {
|
|||||||
MessagePlugin.error('配置保存失败');
|
MessagePlugin.error('配置保存失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadConfig();
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.title {
|
||||||
|
padding: 20px 20px 0 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin: 0 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
.other-config-container {
|
.other-config-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.other-config {
|
.other-config {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 500px;
|
||||||
background: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
.form-item {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
margin-top: 20px;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.form-item {
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item t-input,
|
|
||||||
.form-item t-switch {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</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,33 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div class="form-container">
|
<t-form labelAlign="left">
|
||||||
<h3>HTTP Client 配置</h3>
|
<t-form-item label="启用">
|
||||||
<t-form>
|
<t-switch v-model="config.enable" />
|
||||||
<t-form-item label="启用">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-form-item label="URL">
|
||||||
</t-form-item>
|
<t-input v-model="config.url" />
|
||||||
<t-form-item label="URL">
|
</t-form-item>
|
||||||
<t-input v-model="config.url" />
|
<t-form-item label="消息格式">
|
||||||
</t-form-item>
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
<t-form-item label="消息格式">
|
</t-form-item>
|
||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-form-item label="报告自身消息">
|
||||||
</t-form-item>
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
<t-form-item label="报告自身消息">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-form-item label="Token">
|
||||||
</t-form-item>
|
<t-input v-model="config.token" />
|
||||||
<t-form-item label="Token">
|
</t-form-item>
|
||||||
<t-input v-model="config.token" />
|
<t-form-item label="调试模式">
|
||||||
</t-form-item>
|
<t-switch v-model="config.debug" />
|
||||||
<t-form-item label="调试模式">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.debug" />
|
</t-form>
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
import { HttpClientConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -49,20 +46,4 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.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>
|
|
||||||
|
@@ -1,39 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div class="form-container">
|
<t-form labelAlign="left">
|
||||||
<h3>HTTP Server 配置</h3>
|
<t-form-item label="启用">
|
||||||
<t-form>
|
<t-switch v-model="config.enable" />
|
||||||
<t-form-item label="启用">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-form-item label="端口">
|
||||||
</t-form-item>
|
<t-input v-model.number="config.port" type="number" />
|
||||||
<t-form-item label="端口">
|
</t-form-item>
|
||||||
<t-input v-model.number="config.port" type="number" />
|
<t-form-item label="主机">
|
||||||
</t-form-item>
|
<t-input v-model="config.host" type="text" />
|
||||||
<t-form-item label="主机">
|
</t-form-item>
|
||||||
<t-input v-model="config.host" type="text" />
|
<t-form-item label="启用 CORS">
|
||||||
</t-form-item>
|
<t-switch v-model="config.enableCors" />
|
||||||
<t-form-item label="启用 CORS">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enableCors" />
|
<t-form-item label="启用 WS">
|
||||||
</t-form-item>
|
<t-switch v-model="config.enableWebsocket" />
|
||||||
<t-form-item label="启用 WS">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enableWebsocket" />
|
<t-form-item label="消息格式">
|
||||||
</t-form-item>
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
<t-form-item label="消息格式">
|
</t-form-item>
|
||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-form-item label="Token">
|
||||||
</t-form-item>
|
<t-input v-model="config.token" type="text" />
|
||||||
<t-form-item label="Token">
|
</t-form-item>
|
||||||
<t-input v-model="config.token" type="text" />
|
<t-form-item label="调试模式">
|
||||||
</t-form-item>
|
<t-switch v-model="config.debug" />
|
||||||
<t-form-item label="调试模式">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.debug" />
|
</t-form>
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
import { HttpServerConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -55,20 +52,4 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.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>
|
|
||||||
|
@@ -1,36 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div class="form-container">
|
<t-form labelAlign="left">
|
||||||
<h3>WebSocket Client 配置</h3>
|
<t-form-item label="启用">
|
||||||
<t-form>
|
<t-switch v-model="config.enable" />
|
||||||
<t-form-item label="启用">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-form-item label="URL">
|
||||||
</t-form-item>
|
<t-input v-model="config.url" />
|
||||||
<t-form-item label="URL">
|
</t-form-item>
|
||||||
<t-input v-model="config.url" />
|
<t-form-item label="消息格式">
|
||||||
</t-form-item>
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
<t-form-item label="消息格式">
|
</t-form-item>
|
||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-form-item label="报告自身消息">
|
||||||
</t-form-item>
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
<t-form-item label="报告自身消息">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-form-item label="Token">
|
||||||
</t-form-item>
|
<t-input v-model="config.token" />
|
||||||
<t-form-item label="Token">
|
</t-form-item>
|
||||||
<t-input v-model="config.token" />
|
<t-form-item label="调试模式">
|
||||||
</t-form-item>
|
<t-switch v-model="config.debug" />
|
||||||
<t-form-item label="调试模式">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-form-item label="心跳间隔">
|
||||||
</t-form-item>
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
<t-form-item label="心跳间隔">
|
</t-form-item>
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
</t-form>
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -52,20 +49,4 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.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>
|
|
||||||
|
@@ -1,42 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div>
|
||||||
<div class="form-container">
|
<t-form labelAlign="left">
|
||||||
<h3>WebSocket Server 配置</h3>
|
<t-form-item label="启用">
|
||||||
<t-form>
|
<t-switch v-model="config.enable" />
|
||||||
<t-form-item label="启用">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enable" />
|
<t-form-item label="主机">
|
||||||
</t-form-item>
|
<t-input v-model="config.host" />
|
||||||
<t-form-item label="主机">
|
</t-form-item>
|
||||||
<t-input v-model="config.host" />
|
<t-form-item label="端口">
|
||||||
</t-form-item>
|
<t-input v-model.number="config.port" type="number" />
|
||||||
<t-form-item label="端口">
|
</t-form-item>
|
||||||
<t-input v-model.number="config.port" type="number" />
|
<t-form-item label="消息格式">
|
||||||
</t-form-item>
|
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
||||||
<t-form-item label="消息格式">
|
</t-form-item>
|
||||||
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
|
<t-form-item label="上报自身消息">
|
||||||
</t-form-item>
|
<t-switch v-model="config.reportSelfMessage" />
|
||||||
<t-form-item label="上报自身消息">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.reportSelfMessage" />
|
<t-form-item label="Token">
|
||||||
</t-form-item>
|
<t-input v-model="config.token" />
|
||||||
<t-form-item label="Token">
|
</t-form-item>
|
||||||
<t-input v-model="config.token" />
|
<t-form-item label="强制推送事件">
|
||||||
</t-form-item>
|
<t-switch v-model="config.enableForcePushEvent" />
|
||||||
<t-form-item label="强制推送事件">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.enableForcePushEvent" />
|
<t-form-item label="调试模式">
|
||||||
</t-form-item>
|
<t-switch v-model="config.debug" />
|
||||||
<t-form-item label="调试模式">
|
</t-form-item>
|
||||||
<t-checkbox v-model="config.debug" />
|
<t-form-item label="心跳间隔">
|
||||||
</t-form-item>
|
<t-input v-model.number="config.heartInterval" type="number" />
|
||||||
<t-form-item label="心跳间隔">
|
</t-form-item>
|
||||||
<t-input v-model.number="config.heartInterval" type="number" />
|
</t-form>
|
||||||
</t-form-item>
|
|
||||||
</t-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -58,20 +55,4 @@ watch(
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.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>
|
|
||||||
|
@@ -7,6 +7,8 @@ import NetWork from '../pages/NetWork.vue';
|
|||||||
import QQLogin from '../components/QQLogin.vue';
|
import QQLogin from '../components/QQLogin.vue';
|
||||||
import WebUiLogin from '../components/WebUiLogin.vue';
|
import WebUiLogin from '../components/WebUiLogin.vue';
|
||||||
import OtherConfig from '../pages/OtherConfig.vue';
|
import OtherConfig from '../pages/OtherConfig.vue';
|
||||||
|
import { MessagePlugin } from 'tdesign-vue-next';
|
||||||
|
import { QQLoginManager } from '@/backend/shell';
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{ path: '/', redirect: '/webui' },
|
{ path: '/', redirect: '/webui' },
|
||||||
@@ -26,7 +28,27 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
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",
|
"target": "ESNext",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "vue",
|
"jsxImportSource": "vue",
|
||||||
"lib": [
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
"DOM",
|
|
||||||
"DOM.Iterable"
|
|
||||||
],
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["src/*"]
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"types": [
|
"types": ["vite/client"],
|
||||||
"vite/client"
|
|
||||||
],
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
@@ -30,5 +23,5 @@
|
|||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules"],
|
"exclude": ["node_modules"],
|
||||||
"references": [{"path": "./tsconfig.node.json"}]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
20
package.json
20
package.json
@@ -2,27 +2,32 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.1.12",
|
"version": "4.2.51",
|
||||||
"scripts": {
|
"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:framework": "npm run build:webui && vite build --mode framework || exit 1",
|
||||||
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
|
||||||
"build:webui": "cd napcat.webui && vite build",
|
"build:webui": "cd napcat.webui && vite build",
|
||||||
|
"dev:universal": "vite build --mode universal",
|
||||||
"dev:framework": "vite build --mode framework",
|
"dev:framework": "vite build --mode framework",
|
||||||
"dev:shell": "vite build --mode shell",
|
"dev:shell": "vite build --mode shell",
|
||||||
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
"dev:webui": "cd napcat.webui && npm run webui:dev",
|
||||||
"lint": "eslint --fix src/**/*.{js,ts,vue}",
|
"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": {
|
"devDependencies": {
|
||||||
|
"esbuild": "0.24.0",
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.24.7",
|
||||||
"@eslint/compat": "^1.2.2",
|
"@eslint/compat": "^1.2.2",
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
"@eslint/js": "^9.14.0",
|
"@eslint/js": "^9.14.0",
|
||||||
"@log4js-node/log4js-api": "^1.0.2",
|
"@log4js-node/log4js-api": "^1.0.2",
|
||||||
"@napneko/nap-proto-core": "^0.0.4",
|
"@napneko/nap-proto-core": "^0.0.4",
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@types/cors": "^2.8.17",
|
"@types/cors": "^2.8.17",
|
||||||
|
"@sinclair/typebox": "^0.34.9",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/fluent-ffmpeg": "^2.1.24",
|
"@types/fluent-ffmpeg": "^2.1.24",
|
||||||
"@types/node": "^22.0.1",
|
"@types/node": "^22.0.1",
|
||||||
@@ -41,10 +46,9 @@
|
|||||||
"file-type": "^19.0.0",
|
"file-type": "^19.0.0",
|
||||||
"globals": "^15.12.0",
|
"globals": "^15.12.0",
|
||||||
"image-size": "^1.1.1",
|
"image-size": "^1.1.1",
|
||||||
"json-schema-to-ts": "^3.1.1",
|
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"typescript-eslint": "^8.13.0",
|
"typescript-eslint": "^8.13.0",
|
||||||
"vite": "^5.2.6",
|
"vite": "^6.0.1",
|
||||||
"vite-plugin-cp": "^4.0.8",
|
"vite-plugin-cp": "^4.0.8",
|
||||||
"vite-tsconfig-paths": "^5.1.0",
|
"vite-tsconfig-paths": "^5.1.0",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
@@ -52,9 +56,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
|
"piscina": "^4.7.0",
|
||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"silk-wasm": "^3.6.1",
|
"silk-wasm": "^3.6.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0"
|
||||||
"piscina": "^4.7.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -96,7 +96,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.logError.bind(logger)('convert silk failed', error.stack);
|
logger.logError('convert silk failed', error.stack);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,27 +33,27 @@ export abstract class ConfigBase<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
read(copy_default: boolean = true): T {
|
read(copy_default: boolean = true): T {
|
||||||
const logger = this.core.context.logger;
|
|
||||||
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
const configPath = this.getConfigPath(this.core.selfInfo.uin);
|
||||||
if (!fs.existsSync(configPath) && copy_default) {
|
if (!fs.existsSync(configPath) && copy_default) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
|
||||||
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
this.core.context.logger.log(`[Core] [Config] 配置文件创建成功!\n`);
|
||||||
} catch (e: any) {
|
} 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) {
|
} else if (!fs.existsSync(configPath) && !copy_default) {
|
||||||
fs.writeFileSync(configPath, '{}');
|
fs.writeFileSync(configPath, '{}');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
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;
|
return this.configData;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
logger.logError.bind(logger)(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
this.core.context.logger.logError(`[Core] [Config] 配置文件格式错误,请检查配置文件:`, e.message);
|
||||||
} else {
|
} else {
|
||||||
logger.logError.bind(logger)(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
this.core.context.logger.logError(`[Core] [Config] 读取配置文件时发生错误:`, e.message);
|
||||||
}
|
}
|
||||||
return {} as T;
|
return {} as T;
|
||||||
}
|
}
|
||||||
@@ -61,14 +61,13 @@ export abstract class ConfigBase<T> {
|
|||||||
|
|
||||||
|
|
||||||
save(newConfigData: T = this.configData) {
|
save(newConfigData: T = this.configData) {
|
||||||
const logger = this.core.context.logger;
|
|
||||||
const selfInfo = this.core.selfInfo;
|
const selfInfo = this.core.selfInfo;
|
||||||
this.configData = newConfigData;
|
this.configData = newConfigData;
|
||||||
const configPath = this.getConfigPath(selfInfo.uin);
|
const configPath = this.getConfigPath(selfInfo.uin);
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
fs.writeFileSync(configPath, JSON.stringify(newConfigData, this.getKeys(), 2));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.logError.bind(logger)(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
this.core.context.logger.logError(`保存配置文件 ${configPath} 时发生错误:`, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { stat } from 'fs/promises';
|
import { stat } from 'fs/promises';
|
||||||
import crypto, { randomUUID } from 'crypto';
|
import crypto, { randomUUID } from 'crypto';
|
||||||
import util from 'util';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import * as fileType from 'file-type';
|
|
||||||
import { solveProblem } from '@/common/helper';
|
import { solveProblem } from '@/common/helper';
|
||||||
|
|
||||||
export interface HttpDownloadOptions {
|
export interface HttpDownloadOptions {
|
||||||
@@ -15,18 +13,9 @@ type Uri2LocalRes = {
|
|||||||
success: boolean,
|
success: boolean,
|
||||||
errMsg: string,
|
errMsg: string,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
ext: string,
|
|
||||||
path: 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> {
|
export function checkFileExist(path: string, timeout: number = 3000): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -81,27 +70,6 @@ async function checkFile(path: string): Promise<void> {
|
|||||||
// 如果文件存在,则无需做任何事情,Promise 解决(resolve)自身
|
// 如果文件存在,则无需做任何事情,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> {
|
export function calculateFileMD5(filePath: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 创建一个流式读取器
|
// 创建一个流式读取器
|
||||||
@@ -168,20 +136,6 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
|
|||||||
return Buffer.from(buffer);
|
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 {
|
export enum FileUriType {
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Local = 1,
|
Local = 1,
|
||||||
@@ -190,7 +144,6 @@ export enum FileUriType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUriType(Uri: string) {
|
export async function checkUriType(Uri: string) {
|
||||||
|
|
||||||
const LocalFileRet = await solveProblem((uri: string) => {
|
const LocalFileRet = await solveProblem((uri: string) => {
|
||||||
if (fs.existsSync(uri)) {
|
if (fs.existsSync(uri)) {
|
||||||
return { Uri: uri, Type: FileUriType.Local };
|
return { Uri: uri, Type: FileUriType.Local };
|
||||||
@@ -199,23 +152,17 @@ export async function checkUriType(Uri: string) {
|
|||||||
}, Uri);
|
}, Uri);
|
||||||
if (LocalFileRet) return LocalFileRet;
|
if (LocalFileRet) return LocalFileRet;
|
||||||
const OtherFileRet = await solveProblem((uri: string) => {
|
const OtherFileRet = await solveProblem((uri: string) => {
|
||||||
//再判断是否是Http
|
// 再判断是否是Http
|
||||||
if (uri.startsWith('http://') || uri.startsWith('https://')) {
|
if (uri.startsWith('http:') || uri.startsWith('https:')) {
|
||||||
return { Uri: uri, Type: FileUriType.Remote };
|
return { Uri: uri, Type: FileUriType.Remote };
|
||||||
}
|
}
|
||||||
//再判断是否是Base64
|
// 再判断是否是Base64
|
||||||
if (uri.startsWith('base64://')) {
|
if (uri.startsWith('base64:')) {
|
||||||
return { Uri: uri, Type: FileUriType.Base64 };
|
return { Uri: uri, Type: FileUriType.Base64 };
|
||||||
}
|
}
|
||||||
if (uri.startsWith('file://')) {
|
// 默认file://
|
||||||
let filePath: string;
|
if (uri.startsWith('file:')) {
|
||||||
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
|
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
|
||||||
if (process.platform === 'win32') {
|
|
||||||
filePath = pathname.slice(1);
|
|
||||||
} else {
|
|
||||||
filePath = pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { Uri: filePath, Type: FileUriType.Local };
|
return { Uri: filePath, Type: FileUriType.Local };
|
||||||
}
|
}
|
||||||
if (uri.startsWith('data:')) {
|
if (uri.startsWith('data:')) {
|
||||||
@@ -228,60 +175,34 @@ export async function checkUriType(Uri: string) {
|
|||||||
return { Uri: Uri, Type: FileUriType.Unknown };
|
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 { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||||
//解析失败
|
|
||||||
const tempName = randomUUID();
|
|
||||||
if (!filename) filename = randomUUID();
|
|
||||||
//解析Http和Https协议
|
|
||||||
|
|
||||||
if (UriType == FileUriType.Unknown) {
|
const filePath = path.join(dir, filename);
|
||||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
|
||||||
}
|
switch (UriType) {
|
||||||
//解析File协议和本地文件
|
case FileUriType.Local: {
|
||||||
if (UriType == FileUriType.Local) {
|
|
||||||
const fileExt = path.extname(HandledUri);
|
const fileExt = path.extname(HandledUri);
|
||||||
let filename = path.basename(HandledUri, fileExt);
|
const localFileName = path.basename(HandledUri, fileExt) + fileExt;
|
||||||
filename += fileExt;
|
const tempFilePath = path.join(dir, filename + fileExt);
|
||||||
//复制文件到临时文件并保持后缀
|
fs.copyFileSync(HandledUri, tempFilePath);
|
||||||
const filenameTemp = tempName + fileExt;
|
return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
|
||||||
const filePath = path.join(dir, filenameTemp);
|
|
||||||
fs.copyFileSync(HandledUri, filePath);
|
|
||||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
|
||||||
}
|
}
|
||||||
//接下来都要有文件名
|
|
||||||
|
|
||||||
if (UriType == FileUriType.Remote) {
|
case FileUriType.Remote: {
|
||||||
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
|
const buffer = await httpDownload({ url: HandledUri, headers: headers });
|
||||||
if (pathInfo.name) {
|
|
||||||
const pathlen = 200 - dir.length - pathInfo.name.length;
|
|
||||||
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
|
|
||||||
if (pathInfo.ext) {
|
|
||||||
filename += pathInfo.ext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
|
|
||||||
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
|
|
||||||
const filePath = path.join(dir, tempName + fileExt);
|
|
||||||
const buffer = await httpDownload(HandledUri);
|
|
||||||
//没有文件就创建
|
|
||||||
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
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 base64 = HandledUri.replace(/^base64:\/\//, '');
|
||||||
const buffer = Buffer.from(base64, 'base64');
|
const base64Buffer = Buffer.from(base64, 'base64');
|
||||||
let filePath = path.join(dir, filename);
|
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
|
||||||
let fileExt = '';
|
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||||
fs.writeFileSync(filePath, buffer);
|
}
|
||||||
const { success, ext, path: fileTypePath } = await checkFileV2(filePath);
|
|
||||||
if (success) {
|
default:
|
||||||
filePath = fileTypePath;
|
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
|
||||||
fileExt = ext;
|
|
||||||
filename = filename + '.' + ext;
|
|
||||||
}
|
|
||||||
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
|
|
||||||
}
|
}
|
||||||
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import winston, { format, transports } from 'winston';
|
import winston, { format, transports } from 'winston';
|
||||||
import { truncateString } from '@/common/helper';
|
import { truncateString } from '@/common/helper';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs/promises';
|
||||||
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
|
||||||
|
import EventEmitter from 'node:events';
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
DEBUG = 'debug',
|
DEBUG = 'debug',
|
||||||
INFO = 'info',
|
INFO = 'info',
|
||||||
@@ -24,6 +24,36 @@ function getFormattedTimestamp() {
|
|||||||
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
|
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 {
|
export class LogWrapper {
|
||||||
fileLogEnabled = true;
|
fileLogEnabled = true;
|
||||||
consoleLogEnabled = true;
|
consoleLogEnabled = true;
|
||||||
@@ -47,7 +77,7 @@ export class LogWrapper {
|
|||||||
filename: logPath,
|
filename: logPath,
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
maxsize: 5 * 1024 * 1024, // 5MB
|
maxsize: 5 * 1024 * 1024, // 5MB
|
||||||
maxFiles: 5
|
maxFiles: 5,
|
||||||
}),
|
}),
|
||||||
new transports.Console({
|
new transports.Console({
|
||||||
format: format.combine(
|
format: format.combine(
|
||||||
@@ -56,9 +86,9 @@ export class LogWrapper {
|
|||||||
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
|
||||||
return `${timestamp} [${level}] ${userInfo}${message}`;
|
return `${timestamp} [${level}] ${userInfo}${message}`;
|
||||||
})
|
})
|
||||||
)
|
),
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setLogSelfInfo({ nick: '', uid: '' });
|
this.setLogSelfInfo({ nick: '', uid: '' });
|
||||||
@@ -67,26 +97,20 @@ export class LogWrapper {
|
|||||||
|
|
||||||
cleanOldLogs(logDir: string) {
|
cleanOldLogs(logDir: string) {
|
||||||
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||||
fs.readdir(logDir, (err, files) => {
|
fs.readdir(logDir).then((files) => {
|
||||||
if (err) {
|
files.forEach((file) => {
|
||||||
this.logger.error('Failed to read log directory', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
files.forEach(file => {
|
|
||||||
const filePath = path.join(logDir, file);
|
const filePath = path.join(logDir, file);
|
||||||
this.deleteOldLogFile(filePath, oneWeekAgo);
|
this.deleteOldLogFile(filePath, oneWeekAgo);
|
||||||
});
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
this.logger.error('Failed to read log directory', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
|
||||||
fs.stat(filePath, (err, stats) => {
|
fs.stat(filePath).then((stats) => {
|
||||||
if (err) {
|
|
||||||
this.logger.error('Failed to get file stats', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (stats.mtime.getTime() < oneWeekAgo) {
|
if (stats.mtime.getTime() < oneWeekAgo) {
|
||||||
fs.unlink(filePath, err => {
|
fs.unlink(filePath).catch((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
this.logger.warn(`File already deleted: ${filePath}`);
|
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}`;
|
const userInfo = `${selfInfo.nick}`;
|
||||||
this.logger.defaultMeta = { userInfo };
|
this.logger.defaultMeta = { userInfo };
|
||||||
}
|
}
|
||||||
@@ -135,14 +161,16 @@ export class LogWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatMsg(msg: any[]) {
|
formatMsg(msg: any[]) {
|
||||||
return msg.map(msgItem => {
|
return msg
|
||||||
if (msgItem instanceof Error) {
|
.map((msgItem) => {
|
||||||
return msgItem.stack;
|
if (msgItem instanceof Error) {
|
||||||
} else if (typeof msgItem === 'object') {
|
return msgItem.stack;
|
||||||
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
} else if (typeof msgItem === 'object') {
|
||||||
}
|
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
|
||||||
return msgItem;
|
}
|
||||||
}).join(' ');
|
return msgItem;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
_log(level: LogLevel, ...args: any[]) {
|
_log(level: LogLevel, ...args: any[]) {
|
||||||
@@ -155,6 +183,7 @@ export class LogWrapper {
|
|||||||
// eslint-disable-next-line no-control-regex
|
// eslint-disable-next-line no-control-regex
|
||||||
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
|
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
|
||||||
}
|
}
|
||||||
|
logSubscription.notify(JSON.stringify({ level, message }));
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
@@ -282,13 +311,9 @@ function textElementToText(textElement: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
|
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
|
||||||
const recordMsgOrNull = msg.records.find(
|
const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId);
|
||||||
record => replyElement.sourceMsgIdInRecords === record.msgId,
|
return `[回复消息 ${recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
||||||
);
|
? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
|
||||||
return `[回复消息 ${recordMsgOrNull &&
|
: `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
||||||
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
|
|
||||||
?
|
|
||||||
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
|
|
||||||
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
|
|
||||||
}]`;
|
}]`;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import https from 'node:https';
|
import https from 'node:https';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
|
|
||||||
export class RequestUtil {
|
export class RequestUtil {
|
||||||
// 适用于获取服务器下发cookies时获取,仅GET
|
// 适用于获取服务器下发cookies时获取,仅GET
|
||||||
@@ -69,7 +68,7 @@ export class RequestUtil {
|
|||||||
// 'Content-Length': Buffer.byteLength(postData),
|
// 'Content-Length': Buffer.byteLength(postData),
|
||||||
// },
|
// },
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const req = protocol.request(options, (res: any) => {
|
const req = protocol.request(options, (res: http.IncomingMessage) => {
|
||||||
let responseBody = '';
|
let responseBody = '';
|
||||||
res.on('data', (chunk: string | Buffer) => {
|
res.on('data', (chunk: string | Buffer) => {
|
||||||
responseBody += chunk.toString();
|
responseBody += chunk.toString();
|
||||||
@@ -112,24 +111,4 @@ export class RequestUtil {
|
|||||||
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
static async HttpGetText(url: string, method: string = 'GET', data?: any, headers: { [key: string]: string } = {}) {
|
||||||
return this.HttpGetJson<string>(url, method, data, headers, false, false);
|
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'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
126
src/common/umami.ts
Normal file
126
src/common/umami.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import https from 'node:https';
|
||||||
|
import { napCatVersion } from './version';
|
||||||
|
import os from 'os';
|
||||||
|
export class UmamiTraceCore {
|
||||||
|
napcatVersion = napCatVersion;
|
||||||
|
qqversion = '1.0.0';
|
||||||
|
guid = 'default-user';
|
||||||
|
heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
|
website: string = '1fabb2b1-c3a3-4416-b1be-31e2cbdce978';
|
||||||
|
referrer: string = 'https://trace.napneko.icu/';
|
||||||
|
hostname: string = 'trace.napneko.icu';
|
||||||
|
ua: string = '';
|
||||||
|
|
||||||
|
init(qqversion: string, guid: string) {
|
||||||
|
this.qqversion = qqversion;
|
||||||
|
let UaList = {
|
||||||
|
'linux': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/124.0.0.0 Safari/537.36 PTST/240508.140043',
|
||||||
|
'win32': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36',
|
||||||
|
'darwin': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const ntVersion = os.release();
|
||||||
|
UaList.win32 = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36`;
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
const macVersion = os.release();
|
||||||
|
UaList.darwin = `Mozilla/5.0 (Macintosh; Intel Mac OS X ${macVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.ua = UaList.win32;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ua = UaList[process.platform as keyof typeof UaList] || UaList.win32;
|
||||||
|
|
||||||
|
this.identifyUser(guid);
|
||||||
|
this.startHeartbeat();
|
||||||
|
}
|
||||||
|
|
||||||
|
identifyUser(guid: string) {
|
||||||
|
this.guid = guid;
|
||||||
|
const data = {
|
||||||
|
napcat_version: this.napcatVersion,
|
||||||
|
qq_version: this.qqversion,
|
||||||
|
guid: guid
|
||||||
|
};
|
||||||
|
this.sendRequest({ website: this.website, ...data }, 'identify');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendEvent(event: string, data?: object) {
|
||||||
|
const env = process.env;
|
||||||
|
const language = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES;
|
||||||
|
const payload = {
|
||||||
|
name: event,
|
||||||
|
hostname: this.hostname,
|
||||||
|
referrer: this.referrer,
|
||||||
|
website: this.website,
|
||||||
|
language: language || 'es-US',
|
||||||
|
napcat_version: this.napcatVersion,
|
||||||
|
qq_version: this.qqversion,
|
||||||
|
...data
|
||||||
|
};
|
||||||
|
this.sendRequest(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTrace(eventName: string) {
|
||||||
|
const payload = {
|
||||||
|
website: this.website,
|
||||||
|
hostname: this.hostname,
|
||||||
|
title: 'NapCat ' + this.napcatVersion,
|
||||||
|
url: `/${this.qqversion}/${this.napcatVersion}/${eventName}`,
|
||||||
|
referrer: this.referrer,
|
||||||
|
};
|
||||||
|
this.sendRequest(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRequest(payload: object, type = 'event') {
|
||||||
|
const options = {
|
||||||
|
hostname: '104.19.42.72', // 固定 IP 或者从 hostUrl 获取
|
||||||
|
port: 443,
|
||||||
|
path: '/api/send',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Host": "umami.napneko.icu",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": this.ua
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = https.request(options, (res) => {
|
||||||
|
res.on('error', (error) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
res.on('data', (data) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(JSON.stringify({ type, payload }));
|
||||||
|
request.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
startHeartbeat() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
}
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
this.sendEvent('heartbeat', {
|
||||||
|
title: 'NapCat ' + this.napcatVersion,
|
||||||
|
language: process.env.LANG || 'en-US',
|
||||||
|
url: `/${this.qqversion}/${this.napcatVersion}/heartbeat`,
|
||||||
|
version: this.napcatVersion,
|
||||||
|
qq_version: this.qqversion,
|
||||||
|
user_id: this.guid
|
||||||
|
});
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const UmamiTrace = new UmamiTraceCore();
|
@@ -1 +1 @@
|
|||||||
export const napCatVersion = '4.1.12';
|
export const napCatVersion = '4.2.51';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { MsfChangeReasonType, MsfStatusType } from "../types/adapter";
|
import { MsfChangeReasonType, MsfStatusType } from "@/core/types/adapter";
|
||||||
|
|
||||||
export class NodeIDependsAdapter {
|
export class NodeIDependsAdapter {
|
||||||
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {
|
||||||
|
@@ -6,7 +6,6 @@ import {
|
|||||||
Peer,
|
Peer,
|
||||||
PicElement,
|
PicElement,
|
||||||
PicSubType,
|
PicSubType,
|
||||||
PicType,
|
|
||||||
RawMessage,
|
RawMessage,
|
||||||
SendFileElement,
|
SendFileElement,
|
||||||
SendPicElement,
|
SendPicElement,
|
||||||
@@ -17,16 +16,17 @@ import path from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
|
||||||
import * as fileType from 'file-type';
|
import { fileTypeFromFile } from 'file-type';
|
||||||
import imageSize from 'image-size';
|
import imageSize from 'image-size';
|
||||||
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
|
||||||
import { RkeyManager } from '../helper/rkey';
|
import { RkeyManager } from '@/core/helper/rkey';
|
||||||
import { calculateFileMD5, isGIF } from '@/common/file';
|
import { calculateFileMD5 } from '@/common/file';
|
||||||
import pathLib from 'node:path';
|
import pathLib from 'node:path';
|
||||||
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
|
import { defaultVideoThumbB64, getVideoInfo } from '@/common/video';
|
||||||
import ffmpeg from 'fluent-ffmpeg';
|
import ffmpeg from 'fluent-ffmpeg';
|
||||||
import { encodeSilk } from '@/common/audio';
|
import { encodeSilk } from '@/common/audio';
|
||||||
import { MessageContext } from '@/onebot/api';
|
import { SendMessageContext } from '@/onebot/api';
|
||||||
|
import { getFileTypeForSendType } from '../helper/msg';
|
||||||
|
|
||||||
export class NTQQFileApi {
|
export class NTQQFileApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -40,7 +40,7 @@ export class NTQQFileApi {
|
|||||||
this.rkeyManager = new RkeyManager([
|
this.rkeyManager = new RkeyManager([
|
||||||
'https://rkey.napneko.icu/rkeys'
|
'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) {
|
async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC, elementSubType: number = 0) {
|
||||||
const fileMd5 = await calculateFileMD5(filePath);
|
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}` : '';
|
const ext = extOrEmpty ? `.${extOrEmpty}` : '';
|
||||||
let fileName = `${path.basename(filePath)}`;
|
let fileName = `${path.basename(filePath)}`;
|
||||||
if (fileName.indexOf('.') === -1) {
|
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 {
|
const {
|
||||||
fileName: _fileName,
|
fileName: _fileName,
|
||||||
path,
|
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);
|
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
|
||||||
if (fileSize === 0) {
|
if (fileSize === 0) {
|
||||||
throw new Error('文件异常,大小为0');
|
throw new Error('文件异常,大小为0');
|
||||||
@@ -130,7 +130,7 @@ export class NTQQFileApi {
|
|||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
sourcePath: path,
|
sourcePath: path,
|
||||||
original: true,
|
original: true,
|
||||||
picType: isGIF(picPath) ? PicType.NEWPIC_GIF : PicType.NEWPIC_JPEG,
|
picType: await getFileTypeForSendType(picPath),
|
||||||
picSubType: subType,
|
picSubType: subType,
|
||||||
fileUuid: '',
|
fileUuid: '',
|
||||||
fileSubId: '',
|
fileSubId: '',
|
||||||
@@ -140,8 +140,7 @@ export class NTQQFileApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
async createValidSendVideoElement(context: SendMessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
|
||||||
const logger = this.core.context.logger;
|
|
||||||
let videoInfo = {
|
let videoInfo = {
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
@@ -151,17 +150,17 @@ export class NTQQFileApi {
|
|||||||
filePath,
|
filePath,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
videoInfo = await getVideoInfo(filePath, logger);
|
videoInfo = await getVideoInfo(filePath, this.context.logger);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.logError.bind(logger)('获取视频信息失败,将使用默认值', e);
|
this.context.logger.logError('获取视频信息失败,将使用默认值', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileExt = 'mp4';
|
let fileExt = 'mp4';
|
||||||
try {
|
try {
|
||||||
const tempExt = (await fileType.fileTypeFromFile(filePath))?.ext;
|
const tempExt = (await fileTypeFromFile(filePath))?.ext;
|
||||||
if (tempExt) fileExt = tempExt;
|
if (tempExt) fileExt = tempExt;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.context.logger.logError.bind(logger)('获取文件类型失败', e);
|
this.context.logger.logError('获取文件类型失败', e);
|
||||||
}
|
}
|
||||||
const newFilePath = filePath + '.' + fileExt;
|
const newFilePath = filePath + '.' + fileExt;
|
||||||
fs.copyFileSync(filePath, newFilePath);
|
fs.copyFileSync(filePath, newFilePath);
|
||||||
@@ -182,7 +181,7 @@ export class NTQQFileApi {
|
|||||||
ffmpeg(filePath)
|
ffmpeg(filePath)
|
||||||
.on('error', (err) => {
|
.on('error', (err) => {
|
||||||
try {
|
try {
|
||||||
logger.logDebug('获取视频封面失败,使用默认封面', err);
|
this.context.logger.logDebug('获取视频封面失败,使用默认封面', err);
|
||||||
if (diyThumbPath) {
|
if (diyThumbPath) {
|
||||||
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
|
||||||
resolve(thumbPath);
|
resolve(thumbPath);
|
||||||
@@ -192,7 +191,7 @@ export class NTQQFileApi {
|
|||||||
resolve(thumbPath);
|
resolve(thumbPath);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
|
this.context.logger.logError('获取视频封面失败,使用默认封面失败', error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.screenshots({
|
.screenshots({
|
||||||
@@ -229,6 +228,7 @@ export class NTQQFileApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
|
async createValidSendPttElement(pttPath: string): Promise<SendPttElement> {
|
||||||
|
|
||||||
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
|
||||||
if (!silkPath) {
|
if (!silkPath) {
|
||||||
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
throw new Error('语音转换失败, 请检查语音文件是否正常');
|
||||||
@@ -238,8 +238,7 @@ export class NTQQFileApi {
|
|||||||
throw new Error('文件异常,大小为0');
|
throw new Error('文件异常,大小为0');
|
||||||
}
|
}
|
||||||
if (converted) {
|
if (converted) {
|
||||||
fsPromises.unlink(silkPath).then().catch(
|
fsPromises.unlink(silkPath).then().catch((e) => this.context.logger.logError('删除临时文件失败', e)
|
||||||
(e) => this.context.logger.logError.bind(this.context.logger)('删除临时文件失败', e)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -306,18 +305,18 @@ export class NTQQFileApi {
|
|||||||
element.elementType === ElementType.FILE
|
element.elementType === ElementType.FILE
|
||||||
) {
|
) {
|
||||||
switch (element.elementType) {
|
switch (element.elementType) {
|
||||||
case ElementType.PIC:
|
case ElementType.PIC:
|
||||||
element.picElement!.sourcePath = elementResults[elementIndex];
|
element.picElement!.sourcePath = elementResults[elementIndex];
|
||||||
break;
|
break;
|
||||||
case ElementType.VIDEO:
|
case ElementType.VIDEO:
|
||||||
element.videoElement!.filePath = elementResults[elementIndex];
|
element.videoElement!.filePath = elementResults[elementIndex];
|
||||||
break;
|
break;
|
||||||
case ElementType.PTT:
|
case ElementType.PTT:
|
||||||
element.pttElement!.filePath = elementResults[elementIndex];
|
element.pttElement!.filePath = elementResults[elementIndex];
|
||||||
break;
|
break;
|
||||||
case ElementType.FILE:
|
case ElementType.FILE:
|
||||||
element.fileElement!.filePath = elementResults[elementIndex];
|
element.fileElement!.filePath = elementResults[elementIndex];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
elementIndex++;
|
elementIndex++;
|
||||||
}
|
}
|
||||||
@@ -453,7 +452,7 @@ export class NTQQFileApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} 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) {
|
if (!rkeyData.online_rkey) {
|
||||||
@@ -463,7 +462,7 @@ export class NTQQFileApi {
|
|||||||
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
rkeyData.private_rkey = tempRkeyData.private_rkey;
|
||||||
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
|
this.context.logger.logError('获取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 { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
|
||||||
import { LimitedHashTable } from '@/common/message-unique';
|
import { LimitedHashTable } from '@/common/message-unique';
|
||||||
|
|
||||||
@@ -79,16 +79,10 @@ export class NTQQFriendApi {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleFriendRequest(flag: string, accept: boolean) {
|
async handleFriendRequest(notify: FriendRequest, accept: boolean) {
|
||||||
const data = flag.split('|');
|
|
||||||
if (data.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const friendUid = data[0];
|
|
||||||
const reqTime = data[1];
|
|
||||||
this.context.session.getBuddyService()?.approvalFriendRequest({
|
this.context.session.getBuddyService()?.approvalFriendRequest({
|
||||||
friendUid: friendUid,
|
friendUid: notify.friendUid,
|
||||||
reqTime: reqTime,
|
reqTime: notify.reqTime,
|
||||||
accept,
|
accept,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
GeneralCallResult,
|
GeneralCallResult,
|
||||||
Group,
|
|
||||||
GroupMember,
|
GroupMember,
|
||||||
NTGroupMemberRole,
|
NTGroupMemberRole,
|
||||||
NTGroupRequestOperateTypes,
|
NTGroupRequestOperateTypes,
|
||||||
@@ -8,6 +7,8 @@ import {
|
|||||||
KickMemberV2Req,
|
KickMemberV2Req,
|
||||||
MemberExtSourceType,
|
MemberExtSourceType,
|
||||||
NapCatCore,
|
NapCatCore,
|
||||||
|
GroupNotify,
|
||||||
|
GroupInfoSource,
|
||||||
} from '@/core';
|
} from '@/core';
|
||||||
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
import { isNumeric, solveAsyncProblem } from '@/common/helper';
|
||||||
import { LimitedHashTable } from '@/common/message-unique';
|
import { LimitedHashTable } from '@/common/message-unique';
|
||||||
@@ -16,34 +17,35 @@ import { NTEventWrapper } from '@/common/event';
|
|||||||
export class NTQQGroupApi {
|
export class NTQQGroupApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
groupCache: Map<string, Group> = new Map<string, Group>();
|
|
||||||
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
|
groupMemberCache: Map<string, Map<string, GroupMember>> = new Map<string, Map<string, GroupMember>>();
|
||||||
groups: Group[] = [];
|
|
||||||
essenceLRU = new LimitedHashTable<number, string>(1000);
|
essenceLRU = new LimitedHashTable<number, string>(1000);
|
||||||
session: any;
|
|
||||||
|
|
||||||
constructor(context: InstanceContext, core: NapCatCore) {
|
constructor(context: InstanceContext, core: NapCatCore) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
async initApi() {
|
|
||||||
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
|
async fetchGroupDetail(groupCode: string) {
|
||||||
}
|
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
async initCache() {
|
'NodeIKernelGroupService/getGroupDetailInfo',
|
||||||
this.groups = await this.getGroups();
|
'NodeIKernelGroupListener/onGroupDetailInfoChange',
|
||||||
for (const group of this.groups) {
|
[groupCode, GroupInfoSource.KDATACARD],
|
||||||
this.groupCache.set(group.groupCode, group);
|
(ret) => ret.result === 0,
|
||||||
}
|
(detailInfo) => detailInfo.groupCode === groupCode,
|
||||||
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
|
1,
|
||||||
// process.pid 调试点
|
5000
|
||||||
|
);
|
||||||
|
return detailInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoreAndBaseInfo(uids: string[]) {
|
async initApi() {
|
||||||
return await this.core.eventWrapper.callNoListenerEvent(
|
this.initCache().then().catch(e => this.context.logger.logError(e));
|
||||||
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
}
|
||||||
'nodeStore',
|
|
||||||
uids,
|
async initCache() {
|
||||||
);
|
for (const group of await this.getGroups(true)) {
|
||||||
|
this.refreshGroupMemberCache(group.groupCode).then().catch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchGroupEssenceList(groupCode: string) {
|
async fetchGroupEssenceList(groupCode: string) {
|
||||||
@@ -54,20 +56,22 @@ export class NTQQGroupApi {
|
|||||||
pageLimit: 300,
|
pageLimit: 300,
|
||||||
}, pskey);
|
}, pskey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupShutUpMemberList(groupCode: string) {
|
async getGroupShutUpMemberList(groupCode: string) {
|
||||||
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
|
||||||
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
|
||||||
return (await data)[1];
|
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) {
|
async setGroupAvatar(groupCode: string, filePath: string) {
|
||||||
return this.context.session.getGroupService().setHeader(gc, filePath);
|
return this.context.session.getGroupService().setHeader(groupCode, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroups(forced = false) {
|
async getGroups(forced: boolean = false) {
|
||||||
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
const [, , groupList] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
'NodeIKernelGroupService/getGroupList',
|
'NodeIKernelGroupService/getGroupList',
|
||||||
'NodeIKernelGroupListener/onGroupListUpdate',
|
'NodeIKernelGroupListener/onGroupListUpdate',
|
||||||
@@ -76,9 +80,9 @@ export class NTQQGroupApi {
|
|||||||
return groupList;
|
return groupList;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupExtFE0Info(groupCode: string[], forced = true) {
|
async getGroupExtFE0Info(groupCodes: Array<string>, forced = true) {
|
||||||
return this.context.session.getGroupService().getGroupExt0xEF0Info(
|
return this.context.session.getGroupService().getGroupExt0xEF0Info(
|
||||||
groupCode,
|
groupCodes,
|
||||||
[],
|
[],
|
||||||
{
|
{
|
||||||
bindGuildId: 1,
|
bindGuildId: 1,
|
||||||
@@ -118,53 +122,42 @@ 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) {
|
async getGroupMemberAll(groupCode: string, forced = false) {
|
||||||
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
return this.context.session.getGroupService().getAllMemberList(groupCode, forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshGroupMemberCache(groupCode: string) {
|
||||||
|
try {
|
||||||
|
const members = await this.getGroupMemberAll(groupCode, true);
|
||||||
|
this.groupMemberCache.set(groupCode, members.result.infos);
|
||||||
|
} catch (e) {
|
||||||
|
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
|
||||||
|
}
|
||||||
|
return this.groupMemberCache;
|
||||||
|
}
|
||||||
|
|
||||||
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
|
||||||
const groupCodeStr = groupCode.toString();
|
const groupCodeStr = groupCode.toString();
|
||||||
const memberUinOrUidStr = memberUinOrUid.toString();
|
const memberUinOrUidStr = memberUinOrUid.toString();
|
||||||
|
|
||||||
|
// 获取群成员缓存
|
||||||
let members = this.groupMemberCache.get(groupCodeStr);
|
let members = this.groupMemberCache.get(groupCodeStr);
|
||||||
if (!members) {
|
if (!members) {
|
||||||
try {
|
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMember = () => {
|
||||||
|
if (isNumeric(memberUinOrUidStr)) {
|
||||||
|
return Array.from(members!.values()).find(member => member.uin === memberUinOrUidStr);
|
||||||
|
} else {
|
||||||
|
return members!.get(memberUinOrUidStr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let member = getMember();
|
let member = getMember();
|
||||||
|
// 如果缓存中不存在该成员,尝试刷新缓存
|
||||||
if (!member) {
|
if (!member) {
|
||||||
members = await this.getGroupMembers(groupCodeStr);
|
members = (await this.refreshGroupMemberCache(groupCodeStr)).get(groupCodeStr);
|
||||||
member = getMember();
|
member = getMember();
|
||||||
}
|
}
|
||||||
return member;
|
return member;
|
||||||
@@ -174,26 +167,26 @@ export class NTQQGroupApi {
|
|||||||
return this.context.session.getGroupService().getGroupRecommendContactArkJson(groupCode);
|
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);
|
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);
|
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);
|
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({
|
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||||
chatType: 2,
|
chatType: 2,
|
||||||
guildId: '',
|
guildId: '',
|
||||||
peerUid: GroupCode,
|
peerUid: groupCode,
|
||||||
}, msgId, 1, false);
|
}, msgId, 1, false);
|
||||||
const param = {
|
const param = {
|
||||||
groupCode: GroupCode,
|
groupCode: groupCode,
|
||||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||||
};
|
};
|
||||||
@@ -204,9 +197,9 @@ export class NTQQGroupApi {
|
|||||||
return this.context.session.getGroupService().kickMemberV2(param);
|
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')!;
|
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) {
|
async quitGroupV2(GroupCode: string, needDeleteLocalMsg: boolean) {
|
||||||
@@ -217,65 +210,42 @@ export class NTQQGroupApi {
|
|||||||
return this.context.session.getGroupService().quitGroupV2(param);
|
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 = {
|
const param = {
|
||||||
groupCode: GroupCode,
|
groupCode: groupCode,
|
||||||
msgRandom: parseInt(msgRandom),
|
msgRandom: parseInt(msgRandom),
|
||||||
msgSeq: parseInt(msgSeq),
|
msgSeq: parseInt(msgSeq),
|
||||||
};
|
};
|
||||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
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({
|
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
|
||||||
chatType: 2,
|
chatType: 2,
|
||||||
guildId: '',
|
guildId: '',
|
||||||
peerUid: GroupCode,
|
peerUid: groupCode,
|
||||||
}, msgId, 1, false);
|
}, msgId, 1, false);
|
||||||
const param = {
|
const param = {
|
||||||
groupCode: GroupCode,
|
groupCode: groupCode,
|
||||||
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
msgRandom: parseInt(MsgData.msgList[0].msgRandom),
|
||||||
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
msgSeq: parseInt(MsgData.msgList[0].msgSeq),
|
||||||
};
|
};
|
||||||
return this.context.session.getGroupService().removeGroupEssence(param);
|
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(
|
const [, , , notifies] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
'NodeIKernelGroupService/getSingleScreenNotifies',
|
'NodeIKernelGroupService/getSingleScreenNotifies',
|
||||||
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
|
'NodeIKernelGroupListener/onGroupSingleScreenNotifies',
|
||||||
[
|
[
|
||||||
doubt,
|
doubt,
|
||||||
'',
|
'',
|
||||||
num,
|
count,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return notifies;
|
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) {
|
async searchGroup(groupCode: string) {
|
||||||
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
const [, ret] = await this.core.eventWrapper.callNormalEventV2(
|
||||||
'NodeIKernelSearchService/searchGroup',
|
'NodeIKernelSearchService/searchGroup',
|
||||||
@@ -294,178 +264,89 @@ export class NTQQGroupApi {
|
|||||||
return ret.groupInfos.find(g => g.groupCode === groupCode);
|
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) => {
|
const data = await solveAsyncProblem((eventWrapper: NTEventWrapper, GroupCode: string, uid: string, forced = false) => {
|
||||||
return eventWrapper.callNormalEventV2(
|
return eventWrapper.callNormalEventV2(
|
||||||
'NodeIKernelGroupService/getMemberInfo',
|
'NodeIKernelGroupService/getMemberInfo',
|
||||||
'NodeIKernelGroupListener/onMemberInfoChange',
|
'NodeIKernelGroupListener/onMemberInfoChange',
|
||||||
[GroupCode, [uid], forced],
|
[groupCode, [uid], forced],
|
||||||
(ret) => ret.result === 0,
|
(ret) => ret.result === 0,
|
||||||
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
(params, _, members) => params === GroupCode && members.size > 0 && members.has(uid),
|
||||||
1,
|
1,
|
||||||
forced ? 2500 : 250
|
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)) {
|
if (data && data[3] instanceof Map && data[3].has(uid)) {
|
||||||
return data[3].get(uid);
|
return data[3].get(uid);
|
||||||
}
|
}
|
||||||
if (retry > 0) {
|
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;
|
if (trydata) return trydata;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
|
async getGroupFileCount(groupCodes: Array<string>) {
|
||||||
infos: Map<string, GroupMember>;
|
return this.context.session.getRichMediaService().batchGetGroupFileCount(groupCodes);
|
||||||
finish: boolean;
|
|
||||||
hasNext: boolean | undefined;
|
|
||||||
}> {
|
|
||||||
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
|
|
||||||
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
|
|
||||||
.catch(() => { });
|
|
||||||
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
|
|
||||||
if (result.errCode !== 0) {
|
|
||||||
throw new Error('获取群成员列表出错,' + result.errMsg);
|
|
||||||
}
|
|
||||||
let resMode2;
|
|
||||||
if (modeListener) {
|
|
||||||
const ret = (await once)?.[0];
|
|
||||||
if (ret) {
|
|
||||||
resMode2 = ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.context.session.getGroupService().destroyMemberListScene(sceneId);
|
|
||||||
return {
|
|
||||||
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
|
|
||||||
finish: result.result.finish,
|
|
||||||
hasNext: resMode2?.hasNext,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
|
async getArkJsonGroupShare(groupCode: string) {
|
||||||
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) {
|
|
||||||
const ret = await this.core.eventWrapper.callNoListenerEvent(
|
const ret = await this.core.eventWrapper.callNoListenerEvent(
|
||||||
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
|
'NodeIKernelGroupService/getGroupRecommendContactArkJson',
|
||||||
GroupCode,
|
groupCode,
|
||||||
) as GeneralCallResult & { arkJson: string };
|
) as GeneralCallResult & { arkJson: string };
|
||||||
return ret.arkJson;
|
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')!;
|
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) {
|
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) {
|
||||||
const flagitem = flag.split('|');
|
|
||||||
const groupCode = flagitem[0];
|
|
||||||
const seq = flagitem[1];
|
|
||||||
const type = parseInt(flagitem[2]);
|
|
||||||
|
|
||||||
return this.context.session.getGroupService().operateSysNotify(
|
return this.context.session.getGroupService().operateSysNotify(
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
operateType: operateType,
|
operateType: operateType,
|
||||||
targetMsg: {
|
targetMsg: {
|
||||||
seq: seq, // 通知序列号
|
seq: notify.seq, // 通知序列号
|
||||||
type: type,
|
type: notify.type,
|
||||||
groupCode: groupCode,
|
groupCode: notify.group.groupCode,
|
||||||
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
postscript: reason ?? ' ', // 仅传空值可能导致处理失败,故默认给个空格
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async quitGroup(groupQQ: string) {
|
async quitGroup(groupCode: string) {
|
||||||
return this.context.session.getGroupService().quitGroup(groupQQ);
|
return this.context.session.getGroupService().quitGroup(groupCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async kickMember(groupQQ: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
|
async kickMember(groupCode: string, kickUids: string[], refuseForever: boolean = false, kickReason: string = '') {
|
||||||
return this.context.session.getGroupService().kickMember(groupQQ, kickUids, refuseForever, kickReason);
|
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为解除禁言
|
// timeStamp为秒数, 0为解除禁言
|
||||||
return this.context.session.getGroupService().setMemberShutUp(groupQQ, memList);
|
return this.context.session.getGroupService().setMemberShutUp(groupCode, memList);
|
||||||
}
|
}
|
||||||
|
|
||||||
async banGroup(groupQQ: string, shutUp: boolean) {
|
async banGroup(groupCode: string, shutUp: boolean) {
|
||||||
return this.context.session.getGroupService().setGroupShutUp(groupQQ, shutUp);
|
return this.context.session.getGroupService().setGroupShutUp(groupCode, shutUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMemberCard(groupQQ: string, memberUid: string, cardName: string) {
|
async setMemberCard(groupCode: string, memberUid: string, cardName: string) {
|
||||||
return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
|
return this.context.session.getGroupService().modifyMemberCardName(groupCode, memberUid, cardName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
|
async setMemberRole(groupCode: string, memberUid: string, role: NTGroupMemberRole) {
|
||||||
return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
|
return this.context.session.getGroupService().modifyMemberRole(groupCode, memberUid, role);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setGroupName(groupQQ: string, groupName: string) {
|
async setGroupName(groupCode: string, groupName: string) {
|
||||||
return this.context.session.getGroupService().modifyGroupName(groupQQ, groupName, false);
|
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,
|
id: string,
|
||||||
width: number,
|
width: number,
|
||||||
height: number
|
height: number
|
||||||
@@ -479,11 +360,11 @@ export class NTQQGroupApi {
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
confirmRequired: confirmRequired,
|
confirmRequired: confirmRequired,
|
||||||
};
|
};
|
||||||
return this.context.session.getGroupService().publishGroupBulletin(groupQQ, psKey!, data);
|
return this.context.session.getGroupService().publishGroupBulletin(groupCode, psKey!, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGroupRemainAtTimes(GroupCode: string) {
|
async getGroupRemainAtTimes(groupCode: string) {
|
||||||
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
|
return this.context.session.getGroupService().getGroupRemainAtTimes(groupCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemberExtInfo(groupCode: string, uin: string) {
|
async getMemberExtInfo(groupCode: string, uin: string) {
|
||||||
|
@@ -31,7 +31,7 @@ export class NTQQPacketApi {
|
|||||||
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||||
.then()
|
.then()
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.logger.logError.bind(this.core.context.logger);
|
this.logger.logError(err);
|
||||||
this.errStack.push(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,8 @@ import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
|
|||||||
import { RequestUtil } from '@/common/request';
|
import { RequestUtil } from '@/common/request';
|
||||||
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
|
||||||
import { solveAsyncProblem } from '@/common/helper';
|
import { solveAsyncProblem } from '@/common/helper';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import { LRUCache } from '@/common/lru-cache';
|
||||||
|
|
||||||
export class NTQQUserApi {
|
export class NTQQUserApi {
|
||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
@@ -11,37 +13,28 @@ export class NTQQUserApi {
|
|||||||
this.context = context;
|
this.context = context;
|
||||||
this.core = core;
|
this.core = core;
|
||||||
}
|
}
|
||||||
//self_tind格式
|
|
||||||
async createUidFromTinyId(tinyId: string) {
|
async getCoreAndBaseInfo(uids: string[]) {
|
||||||
return this.context.session.getMsgService().createUidFromTinyId(this.core.selfInfo.uin, tinyId);
|
return await this.core.eventWrapper.callNoListenerEvent(
|
||||||
|
'NodeIKernelProfileService/getCoreAndBaseInfo',
|
||||||
|
'nodeStore',
|
||||||
|
uids,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async getStatusByUid(uid: string) {
|
|
||||||
return this.context.session.getProfileService().getStatus(uid);
|
// 默认获取自己的 type = 2 获取别人 type = 1
|
||||||
}
|
async getProfileLike(uid: string, start: number, count: number, type: number = 2) {
|
||||||
async getProfileLike(uid: string, start: number, count: number) {
|
|
||||||
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
return this.context.session.getProfileLikeService().getBuddyProfileLike({
|
||||||
friendUids: [uid],
|
friendUids: [uid],
|
||||||
basic: 1,
|
basic: 1,
|
||||||
vote: 1,
|
vote: 1,
|
||||||
favorite: 0,
|
favorite: 0,
|
||||||
userProfile: 1,
|
userProfile: 1,
|
||||||
type: 2,
|
type: type,
|
||||||
start: start,
|
start: start,
|
||||||
limit: count,
|
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) {
|
async setLongNick(longNick: string) {
|
||||||
return this.context.session.getProfileService().setLongNick(longNick);
|
return this.context.session.getProfileService().setLongNick(longNick);
|
||||||
}
|
}
|
||||||
@@ -172,35 +165,51 @@ export class NTQQUserApi {
|
|||||||
if (!skey) {
|
if (!skey) {
|
||||||
throw new Error('SKey is Empty');
|
throw new Error('SKey is Empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
return skey;
|
return skey;
|
||||||
}
|
}
|
||||||
|
|
||||||
//后期改成流水线处理
|
|
||||||
async getUidByUinV2(Uin: string) {
|
async getUidByUinV2(Uin: string) {
|
||||||
let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin);
|
if (!Uin) {
|
||||||
if (uid) return uid;
|
return '';
|
||||||
uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
|
}
|
||||||
if (uid) return uid;
|
const services = [
|
||||||
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin);
|
() => this.context.session.getUixConvertService().getUid([Uin]).then((data) => data.uidInfo.get(Uin)).catch(() => undefined),
|
||||||
if (uid) return uid;
|
() => promisify<string, string[], Map<string, string>>
|
||||||
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换
|
(this.context.session.getProfileService().getUidByUin)('FriendsServiceImpl', [Uin]).then((data) => data.get(Uin)).catch(() => undefined),
|
||||||
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid;
|
() => this.context.session.getGroupService().getUidByUins([Uin]).then((data) => data.uids.get(Uin)).catch(() => undefined),
|
||||||
//if (uid) return uid;
|
() => this.getUserDetailInfoByUin(Uin).then((data) => data.detail.uid).catch(() => undefined),
|
||||||
return uid;
|
];
|
||||||
|
let uid: string | undefined = undefined;
|
||||||
|
for (const service of services) {
|
||||||
|
uid = await service();
|
||||||
|
if (uid && uid.indexOf('*') == -1 && uid !== '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uid ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
//后期改成流水线处理
|
|
||||||
async getUinByUidV2(Uid: string) {
|
async getUinByUidV2(Uid: string) {
|
||||||
let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid);
|
if (!Uid) {
|
||||||
if (uin) return uin;
|
return '0';
|
||||||
uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
|
}
|
||||||
if (uin) return uin;
|
const services = [
|
||||||
uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid);
|
() => this.context.session.getUixConvertService().getUin([Uid]).then((data) => data.uinInfo.get(Uid)).catch(() => undefined),
|
||||||
if (uin) return uin;
|
() => this.context.session.getGroupService().getUinByUids([Uid]).then((data) => data.uins.get(Uid)).catch(() => undefined),
|
||||||
uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid);
|
() => promisify<string, string[], Map<string, string>>
|
||||||
if (uin) return uin;
|
(this.context.session.getProfileService().getUinByUid)('FriendsServiceImpl', [Uid]).then((data) => data.get(Uid)).catch(() => undefined),
|
||||||
uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
|
() => this.core.apis.FriendApi.getBuddyIdMap(true).then((data) => data.getKey(Uid)).catch(() => undefined),
|
||||||
return uin;
|
() => this.getUserDetailInfo(Uid).then((data) => data.uin).catch(() => undefined),
|
||||||
|
];
|
||||||
|
let uin: string | undefined = undefined;
|
||||||
|
for (const service of services) {
|
||||||
|
uin = await service();
|
||||||
|
if (uin && uin !== '0' && uin !== '') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uin ?? '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentContactListSnapShot(count: number) {
|
async getRecentContactListSnapShot(count: number) {
|
||||||
|
@@ -366,50 +366,4 @@ export class NTQQWebApi {
|
|||||||
|
|
||||||
return post;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
74
src/core/external/appid.json
vendored
74
src/core/external/appid.json
vendored
@@ -86,5 +86,77 @@
|
|||||||
"6.9.59-29456": {
|
"6.9.59-29456": {
|
||||||
"appid": 537249961,
|
"appid": 537249961,
|
||||||
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
122
src/core/external/offset.json
vendored
122
src/core/external/offset.json
vendored
@@ -82,5 +82,125 @@
|
|||||||
"6.9.59-29456-arm64": {
|
"6.9.59-29456-arm64": {
|
||||||
"send": "4005FE8",
|
"send": "4005FE8",
|
||||||
"recv": "4008800"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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;
|
||||||
|
}
|
@@ -27,7 +27,6 @@ export class RkeyManager {
|
|||||||
await this.refreshRkey();
|
await this.refreshRkey();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
throw new Error(`获取rkey失败: ${e}`);//外抛
|
||||||
//this.logger.logError.bind(this.logger)('获取rkey失败', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.rkeyData;
|
return this.rkeyData;
|
||||||
@@ -50,7 +49,7 @@ export class RkeyManager {
|
|||||||
expired_time: temp.expired_time
|
expired_time: temp.expired_time
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.logError.bind(this.logger)(`[Rkey] Get Rkey ${url} Error `, e);
|
this.logger.logError(`[Rkey] Get Rkey ${url} Error `, e);
|
||||||
//是否为最后一个url
|
//是否为最后一个url
|
||||||
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
if (url === this.serverUrl[this.serverUrl.length - 1]) {
|
||||||
throw new Error(`获取rkey失败: ${e}`);//外抛
|
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,12 +24,13 @@ import path from 'node:path';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { hostname, systemName, systemVersion } from '@/common/system';
|
import { hostname, systemName, systemVersion } from '@/common/system';
|
||||||
import { NTEventWrapper } from '@/common/event';
|
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 { NapCatConfigLoader } from '@/core/helper/config';
|
||||||
import os from 'node:os';
|
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 { proxiedListenerOf } from '@/common/proxy-handler';
|
||||||
import { NTQQPacketApi } from './apis/packet';
|
import { NTQQPacketApi } from './apis/packet';
|
||||||
|
import { UmamiTrace } from '@/common/umami';
|
||||||
export * from './wrapper';
|
export * from './wrapper';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './services';
|
export * from './services';
|
||||||
@@ -127,7 +128,7 @@ export class NapCatCore {
|
|||||||
await api.initApi();
|
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.context.logger.setFileLogEnabled(
|
||||||
this.configLoader.configData.fileLog,
|
this.configLoader.configData.fileLog,
|
||||||
@@ -152,9 +153,15 @@ export class NapCatCore {
|
|||||||
// Renamed from 'InitDataListener'
|
// Renamed from 'InitDataListener'
|
||||||
async initNapCatCoreListeners() {
|
async initNapCatCoreListeners() {
|
||||||
const msgListener = new NodeIKernelMsgListener();
|
const msgListener = new NodeIKernelMsgListener();
|
||||||
|
|
||||||
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
|
||||||
|
if (this.context.workingEnv === NapCatCoreWorkingEnv.Framework) {
|
||||||
|
UmamiTrace.sendEvent('framework/kickoff');
|
||||||
|
} else {
|
||||||
|
UmamiTrace.sendEvent('shell/kickoff');
|
||||||
|
}
|
||||||
// 下线通知
|
// 下线通知
|
||||||
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;
|
this.selfInfo.online = false;
|
||||||
};
|
};
|
||||||
msgListener.onRecvMsg = (msgs) => {
|
msgListener.onRecvMsg = (msgs) => {
|
||||||
@@ -163,7 +170,6 @@ export class NapCatCore {
|
|||||||
msgListener.onAddSendMsg = (msg) => {
|
msgListener.onAddSendMsg = (msg) => {
|
||||||
this.context.logger.logMessage(msg, this.selfInfo);
|
this.context.logger.logMessage(msg, this.selfInfo);
|
||||||
};
|
};
|
||||||
//await sleep(2500);
|
|
||||||
this.context.session.getMsgService().addKernelMsgListener(
|
this.context.session.getMsgService().addKernelMsgListener(
|
||||||
proxiedListenerOf(msgListener, this.context.logger),
|
proxiedListenerOf(msgListener, this.context.logger),
|
||||||
);
|
);
|
||||||
@@ -185,92 +191,6 @@ export class NapCatCore {
|
|||||||
this.context.session.getProfileService().addKernelProfileListener(
|
this.context.session.getProfileService().addKernelProfileListener(
|
||||||
proxiedListenerOf(profileListener, this.context.logger),
|
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 {
|
export class NodeIKernelGroupListener {
|
||||||
onGroupListInited(listEmpty: boolean): any { }
|
onGroupListInited(listEmpty: boolean): any { }
|
||||||
@@ -28,7 +28,7 @@ export class NodeIKernelGroupListener {
|
|||||||
onGroupConfMemberChange(...args: unknown[]): any {
|
onGroupConfMemberChange(...args: unknown[]): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupDetailInfoChange(...args: unknown[]): any {
|
onGroupDetailInfoChange(detailInfo: GroupDetailInfo): any {
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupExtListUpdate(...args: unknown[]): 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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,7 +23,9 @@ export class PacketClientSession {
|
|||||||
get operation() {
|
get operation() {
|
||||||
return this.context.operation;
|
return this.context.operation;
|
||||||
}
|
}
|
||||||
|
get client() {
|
||||||
|
return this.context.client;
|
||||||
|
}
|
||||||
// TODO: global message element adapter (?
|
// TODO: global message element adapter (?
|
||||||
get msgConverter() {
|
get msgConverter() {
|
||||||
return this.context.msgConverter;
|
return this.context.msgConverter;
|
||||||
|
@@ -3,6 +3,7 @@ export interface MiniAppReqCustomParams {
|
|||||||
desc: string;
|
desc: string;
|
||||||
picUrl: string;
|
picUrl: string;
|
||||||
jumpUrl: string;
|
jumpUrl: string;
|
||||||
|
webUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MiniAppReqTemplateParams {
|
export interface MiniAppReqTemplateParams {
|
||||||
|
@@ -14,6 +14,8 @@ import {
|
|||||||
GroupFileExtra
|
GroupFileExtra
|
||||||
} from "@/core/packet/transformer/proto";
|
} from "@/core/packet/transformer/proto";
|
||||||
import {
|
import {
|
||||||
|
BaseEmojiType,
|
||||||
|
FaceType,
|
||||||
NTMsgAtType,
|
NTMsgAtType,
|
||||||
PicType,
|
PicType,
|
||||||
SendArkElement,
|
SendArkElement,
|
||||||
@@ -122,7 +124,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isGroupReply(): boolean {
|
get isGroupReply(): boolean {
|
||||||
return this.messageClientSeq !== 0;
|
return this.messageClientSeq === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||||
@@ -162,7 +164,7 @@ export class PacketMsgFaceElement extends IPacketMsgElement<SendFaceElement> {
|
|||||||
constructor(element: SendFaceElement) {
|
constructor(element: SendFaceElement) {
|
||||||
super(element);
|
super(element);
|
||||||
this.faceId = element.faceElement.faceIndex;
|
this.faceId = element.faceElement.faceIndex;
|
||||||
this.isLargeFace = element.faceElement.faceType === 3;
|
this.isLargeFace = element.faceElement.faceType === FaceType.AniSticke;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
|
||||||
|
@@ -30,7 +30,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
|
|||||||
shareType: req.shareType,
|
shareType: req.shareType,
|
||||||
versionId: req.versionId,
|
versionId: req.versionId,
|
||||||
withShareTicket: req.withShareTicket,
|
withShareTicket: req.withShareTicket,
|
||||||
webURL: "",
|
webURL: req.webUrl ?? "",
|
||||||
appidRich: Buffer.alloc(0),
|
appidRich: Buffer.alloc(0),
|
||||||
template: {
|
template: {
|
||||||
templateId: "",
|
templateId: "",
|
||||||
|
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),
|
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 = {
|
export const PushMsgBody = {
|
||||||
responseHead: ProtoField(1, () => ResponseHead),
|
responseHead: ProtoField(1, () => ResponseHead),
|
||||||
contentHead: ProtoField(2, () => ContentHead),
|
contentHead: ProtoField(2, () => ContentHead),
|
||||||
|
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 { GeneralCallResult } from '@/core/services/common';
|
||||||
import { NodeIKernelBuddyListener } from '@/core/listeners';
|
import { NodeIKernelBuddyListener } from '@/core/listeners';
|
||||||
import { BuddyListReqType } from '../types/user';
|
import { BuddyListReqType } from '@/core/types/user';
|
||||||
|
|
||||||
export interface NodeIKernelBuddyService {
|
export interface NodeIKernelBuddyService {
|
||||||
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
|
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {
|
||||||
|
@@ -149,7 +149,7 @@ export interface NodeIKernelGroupService {
|
|||||||
|
|
||||||
getGroupExtList(force: boolean): Promise<GeneralCallResult>;
|
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
|
getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export interface NodeIKernelGroupService {
|
|||||||
|
|
||||||
getGroupPortrait(): void;
|
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;
|
modifyGroupRemark(groupCode: string, remark: string): void;
|
||||||
|
|
||||||
@@ -187,13 +187,13 @@ export interface NodeIKernelGroupService {
|
|||||||
|
|
||||||
destroyGroup(groupCode: string): void;
|
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;
|
clearGroupNotifies(groupCode: string): void;
|
||||||
|
|
||||||
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>;
|
getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>;
|
||||||
|
|
||||||
clearGroupNotifiesUnreadCount(unknown: boolean): void;
|
clearGroupNotifiesUnreadCount(doubt: boolean): void;
|
||||||
|
|
||||||
operateSysNotify(
|
operateSysNotify(
|
||||||
doubt: boolean,
|
doubt: boolean,
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/types';
|
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/types';
|
||||||
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
|
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
|
||||||
import { GeneralCallResult } from '@/core/services/common';
|
import { GeneralCallResult } from '@/core/services/common';
|
||||||
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '../types/msg';
|
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg';
|
||||||
|
|
||||||
export interface NodeIKernelMsgService {
|
export interface NodeIKernelMsgService {
|
||||||
|
buildMultiForwardMsg(req: { srcMsgIds: Array<string>, srcContact: Peer }): Promise<GeneralCallResult & { rspInfo: { elements: unknown } }>;
|
||||||
|
|
||||||
generateMsgUniqueId(chatType: number, time: string): string;
|
generateMsgUniqueId(chatType: number, time: string): string;
|
||||||
|
|
||||||
|
@@ -4,14 +4,14 @@ import { GeneralCallResult } from '@/core/services/common';
|
|||||||
|
|
||||||
export interface NodeIKernelProfileService {
|
export interface NodeIKernelProfileService {
|
||||||
getOtherFlag(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
getOtherFlag(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
||||||
|
|
||||||
getVasInfo(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
getVasInfo(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
||||||
|
|
||||||
getRelationFlag(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
getRelationFlag(callfrom: string, uids: string[]): Promise<Map<string, any>>;
|
||||||
|
|
||||||
getUidByUin(callfrom: string, uin: Array<string>): Promise<Map<string, string>>;
|
getUidByUin(callfrom: string, uin: Array<string>): Map<string, string>;
|
||||||
|
|
||||||
getUinByUid(callfrom: string, uid: Array<string>): Promise<Map<string, string>>;
|
getUinByUid(callfrom: string, uid: Array<string>): Map<string, string>;
|
||||||
|
|
||||||
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;
|
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise<Map<string, SimpleInfo>>;
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChatType, Peer } from '@/core/types';
|
import { ChatType, Peer } from '@/core/types';
|
||||||
import { NodeIKernelRecentContactListener } from '../listeners/NodeIKernelRecentContactListener';
|
import { NodeIKernelRecentContactListener } from '@/core/listeners/NodeIKernelRecentContactListener';
|
||||||
import { GeneralCallResult } from './common';
|
import { GeneralCallResult } from '@/core/services/common';
|
||||||
import { FSABRecentContactParams } from '../types/contact';
|
import { FSABRecentContactParams } from '@/core/types/contact';
|
||||||
|
|
||||||
export interface NodeIKernelRecentContactService {
|
export interface NodeIKernelRecentContactService {
|
||||||
setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments
|
setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments
|
||||||
|
@@ -16,7 +16,7 @@ export interface NodeIKernelSearchService {
|
|||||||
penetrate: string
|
penetrate: string
|
||||||
}): Promise<GeneralCallResult>;// needs 1 arguments
|
}): Promise<GeneralCallResult>;// needs 1 arguments
|
||||||
|
|
||||||
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown;
|
searchLocalInfo(keywords: string, type: number/*4*/): unknown;
|
||||||
|
|
||||||
cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments
|
cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { NodeIO3MiscListener } from "../listeners/NodeIO3MiscListener";
|
import { NodeIO3MiscListener } from "@/core/listeners/NodeIO3MiscListener";
|
||||||
|
|
||||||
export interface NodeIO3MiscService {
|
export interface NodeIO3MiscService {
|
||||||
get(): NodeIO3MiscService;
|
get(): NodeIO3MiscService;
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
export enum MsfStatusType {
|
export enum MsfStatusType {
|
||||||
KUNKNOWN,
|
KUNKNOWN = 0,
|
||||||
KDISCONNECTED,
|
KDISCONNECTED = 1,
|
||||||
KCONNECTED
|
KCONNECTED = 2
|
||||||
}
|
}
|
||||||
export enum MsfChangeReasonType {
|
export enum MsfChangeReasonType {
|
||||||
KUNKNOWN,
|
KUNKNOWN = 0,
|
||||||
KUSERLOGININ,
|
KUSERLOGININ = 1,
|
||||||
KUSERLOGINOUT,
|
KUSERLOGINOUT = 2,
|
||||||
KAUTO
|
KAUTO = 3
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { ElementType, FaceType, MessageElement, NTGrayTipElementSubTypeV2, PicSubType, PicType, TipAioOpGrayTipElement, TipGroupElement, NTVideoType } from "./msg";
|
import { ElementType, MessageElement, NTGrayTipElementSubTypeV2, PicSubType, PicType, TipAioOpGrayTipElement, TipGroupElement, NTVideoType, FaceType } from "./msg";
|
||||||
|
|
||||||
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
|
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export interface TextElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FaceElement {
|
export interface FaceElement {
|
||||||
|
pokeType?: number;
|
||||||
faceIndex: number;
|
faceIndex: number;
|
||||||
faceType: FaceType;
|
faceType: FaceType;
|
||||||
faceText?: string;
|
faceText?: string;
|
||||||
@@ -40,20 +41,22 @@ export interface FaceElement {
|
|||||||
surpriseId?: string;
|
surpriseId?: string;
|
||||||
randomType?: number;
|
randomType?: number;
|
||||||
}
|
}
|
||||||
|
export interface GrayTipRovokeElement {
|
||||||
|
operatorRole: string;
|
||||||
|
operatorUid: string;
|
||||||
|
operatorNick: string;
|
||||||
|
operatorRemark: string;
|
||||||
|
operatorMemRemark?: string;
|
||||||
|
wording: string; // 自定义的撤回提示语
|
||||||
|
}
|
||||||
|
|
||||||
export interface GrayTipElement {
|
export interface GrayTipElement {
|
||||||
subElementType: NTGrayTipElementSubTypeV2;
|
subElementType: NTGrayTipElementSubTypeV2;
|
||||||
revokeElement: {
|
revokeElement: GrayTipRovokeElement;
|
||||||
operatorRole: string;
|
|
||||||
operatorUid: string;
|
|
||||||
operatorNick: string;
|
|
||||||
operatorRemark: string;
|
|
||||||
operatorMemRemark?: string;
|
|
||||||
wording: string; // 自定义的撤回提示语
|
|
||||||
};
|
|
||||||
aioOpGrayTipElement: TipAioOpGrayTipElement;
|
aioOpGrayTipElement: TipAioOpGrayTipElement;
|
||||||
groupElement: TipGroupElement;
|
groupElement: TipGroupElement;
|
||||||
xmlElement: {
|
xmlElement: {
|
||||||
|
busiId: string;
|
||||||
content: string;
|
content: string;
|
||||||
templId: string;
|
templId: string;
|
||||||
};
|
};
|
||||||
@@ -253,7 +256,7 @@ export interface FaceBubbleElement {
|
|||||||
faceFlag: number;
|
faceFlag: number;
|
||||||
content: string;
|
content: string;
|
||||||
oldVersionStr: string;
|
oldVersionStr: string;
|
||||||
faceType: number;
|
faceType: FaceType;
|
||||||
others: string;
|
others: string;
|
||||||
yellowFaceInfo: {
|
yellowFaceInfo: {
|
||||||
index: number;
|
index: number;
|
||||||
|
54
src/core/types/emoji.ts
Normal file
54
src/core/types/emoji.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export enum PullMomentType {
|
||||||
|
REINSTALL = 0,
|
||||||
|
RESTART_FIRST_AIO = 1,
|
||||||
|
LOGIN_APP = 2,
|
||||||
|
SINGEL_PULL_NOTIFY = 3,
|
||||||
|
TRIGGER_SPECIFIC_EMOJI_RANDOM_RESULT = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PullSysEmojisReq {
|
||||||
|
fetchAdvaceSource: boolean;
|
||||||
|
fetchBaseSource: boolean;
|
||||||
|
pullMoment: PullMomentType;
|
||||||
|
pullType: number;
|
||||||
|
refresh: boolean;
|
||||||
|
thresholdValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BaseEmojiType {
|
||||||
|
NORMAL_EMOJI = 0,
|
||||||
|
SUPER_EMOJI = 1,
|
||||||
|
RANDOM_SUPER_EMOJI = 2,
|
||||||
|
CHAIN_SUPER_EMOJI = 3,
|
||||||
|
EMOJI_EMOJI = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetBaseEmojiPathReq {
|
||||||
|
emojiId: string;
|
||||||
|
type: BaseEmojiType;
|
||||||
|
}
|
||||||
|
export enum EmojiPanelCategory {
|
||||||
|
OTHER_PANEL = 0,
|
||||||
|
NORMAL_PANEL = 1,
|
||||||
|
SUPER_PANEL = 2,
|
||||||
|
RED_HEART_PANEL = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadBaseEmojiInfo {
|
||||||
|
baseResDownloadUrl: string;
|
||||||
|
advancedResDownloadUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadBaseEmojiByUrlReq {
|
||||||
|
emojiId: string;
|
||||||
|
groupName: string;
|
||||||
|
panelCategory: EmojiPanelCategory;
|
||||||
|
downloadInfo: DownloadBaseEmojiInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadBaseEmojiByIdReq {
|
||||||
|
emojiId: string;
|
||||||
|
groupName: string;
|
||||||
|
panelCategory: EmojiPanelCategory;
|
||||||
|
qzoneCode: string;
|
||||||
|
}
|
74
src/core/types/graytip.ts
Normal file
74
src/core/types/graytip.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
export enum JsonGrayBusiId {
|
||||||
|
AIO_AV_C2C_NOTICE = 2021,
|
||||||
|
AIO_AV_GROUP_NOTICE = 2022,
|
||||||
|
AIO_C2C_DONT_DISTURB = 2100,
|
||||||
|
AIO_CRM_FLAGS_TIPS = 2050,
|
||||||
|
AIO_GROUP_ESSENCE_MSG_TIP = 2401,
|
||||||
|
AIO_NUDGE_CUSTOM_GUIDE = 2041,
|
||||||
|
AIO_PUSH_GUIDE_GRAY_TIPS = 2701,
|
||||||
|
AIO_RECALL_MSGCUSTOM_WORDINGGUIDE = 2000,
|
||||||
|
AIO_ROBOT_SAFETY_TIP = 2201,
|
||||||
|
AIO_ZPLAN_EMOTICON_GUIDE = 2301,
|
||||||
|
AIO_ZPLAN_SCENE_LINKAGE = 2302,
|
||||||
|
AIO_ZPLAN_SEND_MEME = 2300,
|
||||||
|
DISBAND_DISCUSSION_GRAY_TIP_ID = 2603,
|
||||||
|
FILE_SENDING_SIZE_4GB_LIMIT = 3003,
|
||||||
|
GROUP_AIO_CONFIGURABLE_GRAY_TIPS = 2407,
|
||||||
|
GROUP_AIO_HOME_SCHOOL_WELCOME_GRAY_TIP_ID = 2404,
|
||||||
|
GROUP_AIO_MSG_FREQUENCY_GRAY_TIP_ID = 2406,
|
||||||
|
GROUP_AIO_SHUTUP_GRAY_TIP_ID = 2402,
|
||||||
|
GROUP_AIO_TEMPORARY_GRAY_TIP_ID = 2405,
|
||||||
|
GROUP_AIO_UNREAD_MSG_AI_SUMMARY = 2408,
|
||||||
|
GROUP_AIO_UPLOAD_PERMISSIONS_GRAY_TIP_ID = 2403,
|
||||||
|
LITE_ACTION = 86,
|
||||||
|
ONLINE_FILE_CANCEL_RECV_ON_RECVING = 4,
|
||||||
|
ONLINE_FILE_GO_OFFLINE = 11,
|
||||||
|
ONLINE_FILE_GO_OFFLINE_ALL = 12,
|
||||||
|
ONLINE_FILE_RECV_BY_MOBILE = 13,
|
||||||
|
ONLINE_FILE_RECV_ERROR = 10,
|
||||||
|
ONLINE_FILE_REFUSE_ALL_RECV = 7,
|
||||||
|
ONLINE_FILE_REFUSE_ALL_RECV_ON_RECVING = 8,
|
||||||
|
ONLINE_FILE_REFUSE_RECV = 3,
|
||||||
|
ONLINE_FILE_SEND_ERROR = 9,
|
||||||
|
ONLINE_FILE_STOP_ALL_SEND = 5,
|
||||||
|
ONLINE_FILE_STOP_ALL_SEND_ON_SENDING = 6,
|
||||||
|
ONLINE_FILE_STOP_SEND = 1,
|
||||||
|
ONLINE_FILE_STOP_SEND_ON_SENDING = 2,
|
||||||
|
ONLINE_GROUP_HOME_WORK = 51,
|
||||||
|
PTT_AUTO_CHANGE_GUIDE = 2060,
|
||||||
|
QCIRCLE_SHOW_FULE_TIPS = 2601,
|
||||||
|
QWALLET_GRAY_TIP_ID = 2602,
|
||||||
|
RED_BAG = 81,
|
||||||
|
RELATION_C2C_GROUP_AIO_SETUP_GROUP_AND_REMARK = 1005,
|
||||||
|
RELATION_C2C_LOVER_BONUS = 1003,
|
||||||
|
RELATION_C2C_MEMBER_ADD = 1017,
|
||||||
|
RELATION_C2C_REACTIVE_DEGRADE_MSG = 1019,
|
||||||
|
RELATION_C2C_REACTIVE_UPGRADE_MSG = 1018,
|
||||||
|
RELATION_C2C_SAY_HELLO = 1004,
|
||||||
|
RELATION_CHAIN_BLACKED = 1000,
|
||||||
|
RELATION_CHAIN_MATCH_FRIEND = 1007,
|
||||||
|
RELATION_CREATE_GROUP_GRAY_TIP_ID = 1009,
|
||||||
|
RELATION_EMOJIEGG_SHOW = 1001,
|
||||||
|
RELATION_EMOJIEGG_WILL_DEGRADE = 1002,
|
||||||
|
RELATION_FRIEND_CLONE_INFO = 1006,
|
||||||
|
RELATION_GROUP_BATCH_ADD_FRIEND = 1020,
|
||||||
|
RELATION_GROUP_MEMBER_ADD = 1022,
|
||||||
|
RELATION_GROUP_MEMBER_ADD_WITH_MODIFY_NAME = 1015,
|
||||||
|
RELATION_GROUP_MEMBER_ADD_WITH_WELCOME = 1016,
|
||||||
|
RELATION_GROUP_MEMBER_RECOMMEND = 1021,
|
||||||
|
RELATION_GROUP_SHUT_UP = 1014,
|
||||||
|
RELATION_LIMIT_TMP_CONVERSATION_SET = 1011,
|
||||||
|
RELATION_NEARBY_GOTO_VERIFY = 1008,
|
||||||
|
RELATION_ONEWAY_FRIEND_GRAY_TIP_ID = 1012,
|
||||||
|
RELATION_ONEWAY_FRIEND_NEW_GRAY_TIP_ID = 1013,
|
||||||
|
RELATION_YQT = 1010,
|
||||||
|
TROOP_ADD_FRIEND_ACTIVE = 19264,
|
||||||
|
TROOP_ADD_FRIEND_HOT_CHAT = 19265,
|
||||||
|
TROOP_ADD_FRIEND_NEW_MEMBER = 19267,
|
||||||
|
TROOP_ADD_FRIEND_REPLY_OR_AT = 19266,
|
||||||
|
TROOP_BREAK_ICE = 10405,
|
||||||
|
TROOP_FLAME_IGNITED = 19273,
|
||||||
|
UI_RESERVE_100000_110000 = 100000,
|
||||||
|
VAS_FILE_UPLOAD_OVER_1G = 3002,
|
||||||
|
VAS_FILE_UPLOAD_OVER_LIMIT = 3001,
|
||||||
|
}
|
@@ -17,7 +17,160 @@ export enum GroupInfoSource {
|
|||||||
KRECENTCONTACT,
|
KRECENTCONTACT,
|
||||||
KMOREPANEL
|
KMOREPANEL
|
||||||
}
|
}
|
||||||
|
export interface GroupDetailInfo {
|
||||||
|
groupCode: string;
|
||||||
|
groupUin: string;
|
||||||
|
ownerUid: string;
|
||||||
|
ownerUin: string;
|
||||||
|
groupFlag: number;
|
||||||
|
groupFlagExt: number;
|
||||||
|
maxMemberNum: number;
|
||||||
|
memberNum: number;
|
||||||
|
groupOption: number;
|
||||||
|
classExt: number;
|
||||||
|
groupName: string;
|
||||||
|
fingerMemo: string;
|
||||||
|
groupQuestion: string;
|
||||||
|
certType: number;
|
||||||
|
richFingerMemo: string;
|
||||||
|
tagRecord: any[];
|
||||||
|
shutUpAllTimestamp: number;
|
||||||
|
shutUpMeTimestamp: number;
|
||||||
|
groupTypeFlag: number;
|
||||||
|
privilegeFlag: number;
|
||||||
|
groupSecLevel: number;
|
||||||
|
groupFlagExt3: number;
|
||||||
|
isConfGroup: number;
|
||||||
|
isModifyConfGroupFace: number;
|
||||||
|
isModifyConfGroupName: number;
|
||||||
|
groupFlagExt4: number;
|
||||||
|
groupMemo: string;
|
||||||
|
cmdUinMsgSeq: number;
|
||||||
|
cmdUinJoinTime: number;
|
||||||
|
cmdUinUinFlag: number;
|
||||||
|
cmdUinMsgMask: number;
|
||||||
|
groupSecLevelInfo: number;
|
||||||
|
cmdUinPrivilege: number;
|
||||||
|
cmdUinFlagEx2: number;
|
||||||
|
appealDeadline: number;
|
||||||
|
remarkName: string;
|
||||||
|
isTop: boolean;
|
||||||
|
groupFace: number;
|
||||||
|
groupGeoInfo: {
|
||||||
|
ownerUid: string;
|
||||||
|
SetTime: number;
|
||||||
|
CityId: number;
|
||||||
|
Longitude: string;
|
||||||
|
Latitude: string;
|
||||||
|
GeoContent: string;
|
||||||
|
poiId: string;
|
||||||
|
};
|
||||||
|
certificationText: string;
|
||||||
|
cmdUinRingtoneId: number;
|
||||||
|
longGroupName: string;
|
||||||
|
autoAgreeJoinGroupUserNumForConfGroup: number;
|
||||||
|
autoAgreeJoinGroupUserNumForNormalGroup: number;
|
||||||
|
cmdUinFlagExt3Grocery: number;
|
||||||
|
groupCardPrefix: {
|
||||||
|
introduction: string;
|
||||||
|
rptPrefix: any[];
|
||||||
|
};
|
||||||
|
groupExt: {
|
||||||
|
groupInfoExtSeq: number;
|
||||||
|
reserve: number;
|
||||||
|
luckyWordId: string;
|
||||||
|
lightCharNum: number;
|
||||||
|
luckyWord: string;
|
||||||
|
starId: number;
|
||||||
|
essentialMsgSwitch: number;
|
||||||
|
todoSeq: number;
|
||||||
|
blacklistExpireTime: number;
|
||||||
|
isLimitGroupRtc: number;
|
||||||
|
companyId: number;
|
||||||
|
hasGroupCustomPortrait: number;
|
||||||
|
bindGuildId: string;
|
||||||
|
groupOwnerId: {
|
||||||
|
memberUin: string;
|
||||||
|
memberUid: string;
|
||||||
|
memberQid: string;
|
||||||
|
};
|
||||||
|
essentialMsgPrivilege: number;
|
||||||
|
msgEventSeq: string;
|
||||||
|
inviteRobotSwitch: number;
|
||||||
|
gangUpId: string;
|
||||||
|
qqMusicMedalSwitch: number;
|
||||||
|
showPlayTogetherSwitch: number;
|
||||||
|
groupFlagPro1: string;
|
||||||
|
groupBindGuildIds: {
|
||||||
|
guildIds: any[];
|
||||||
|
};
|
||||||
|
viewedMsgDisappearTime: string;
|
||||||
|
groupExtFlameData: {
|
||||||
|
switchState: number;
|
||||||
|
state: number;
|
||||||
|
dayNums: any[];
|
||||||
|
version: number;
|
||||||
|
updateTime: string;
|
||||||
|
isDisplayDayNum: boolean;
|
||||||
|
};
|
||||||
|
groupBindGuildSwitch: number;
|
||||||
|
groupAioBindGuildId: string;
|
||||||
|
groupExcludeGuildIds: {
|
||||||
|
guildIds: any[];
|
||||||
|
};
|
||||||
|
fullGroupExpansionSwitch: number;
|
||||||
|
fullGroupExpansionSeq: string;
|
||||||
|
inviteRobotMemberSwitch: number;
|
||||||
|
inviteRobotMemberExamine: number;
|
||||||
|
groupSquareSwitch: number;
|
||||||
|
};
|
||||||
|
msgLimitFrequency: number;
|
||||||
|
hlGuildAppid: number;
|
||||||
|
hlGuildSubType: number;
|
||||||
|
isAllowRecallMsg: number;
|
||||||
|
confUin: string;
|
||||||
|
confMaxMsgSeq: number;
|
||||||
|
confToGroupTime: number;
|
||||||
|
groupSchoolInfo: {
|
||||||
|
location: string;
|
||||||
|
grade: number;
|
||||||
|
school: string;
|
||||||
|
};
|
||||||
|
activeMemberNum: number;
|
||||||
|
groupGrade: number;
|
||||||
|
groupCreateTime: number;
|
||||||
|
subscriptionUin: string;
|
||||||
|
subscriptionUid: string;
|
||||||
|
noFingerOpenFlag: number;
|
||||||
|
noCodeFingerOpenFlag: number;
|
||||||
|
isGroupFreeze: number;
|
||||||
|
allianceId: string;
|
||||||
|
groupExtOnly: {
|
||||||
|
tribeId: number;
|
||||||
|
moneyForAddGroup: number;
|
||||||
|
};
|
||||||
|
isAllowConfGroupMemberModifyGroupName: number;
|
||||||
|
isAllowConfGroupMemberNick: number;
|
||||||
|
isAllowConfGroupMemberAtAll: number;
|
||||||
|
groupClassText: string;
|
||||||
|
groupFreezeReason: number;
|
||||||
|
headPortraitSeq: number;
|
||||||
|
groupHeadPortrait: {
|
||||||
|
portraitCnt: number;
|
||||||
|
portraitInfo: any[];
|
||||||
|
defaultId: number;
|
||||||
|
verifyingPortraitCnt: number;
|
||||||
|
verifyingPortraitInfo: any[];
|
||||||
|
};
|
||||||
|
cmdUinJoinMsgSeq: number;
|
||||||
|
cmdUinJoinRealMsgSeq: number;
|
||||||
|
groupAnswer: string;
|
||||||
|
groupAdminMaxNum: number;
|
||||||
|
inviteNoAuthNumLimit: string;
|
||||||
|
hlGuildOrgId: number;
|
||||||
|
isAllowHlGuildBinary: number;
|
||||||
|
localExitGroupReason: number;
|
||||||
|
}
|
||||||
export interface GroupExt0xEF0InfoFilter {
|
export interface GroupExt0xEF0InfoFilter {
|
||||||
bindGuildId: number;
|
bindGuildId: number;
|
||||||
blacklistExpireTime: number;
|
blacklistExpireTime: number;
|
||||||
@@ -63,16 +216,16 @@ export interface KickMemberV2Req {
|
|||||||
|
|
||||||
// 数据来源类型
|
// 数据来源类型
|
||||||
export enum DataSource {
|
export enum DataSource {
|
||||||
LOCAL,
|
LOCAL = 0,
|
||||||
REMOTE
|
REMOTE = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 群列表更新类型
|
// 群列表更新类型
|
||||||
export enum GroupListUpdateType {
|
export enum GroupListUpdateType {
|
||||||
REFRESHALL,
|
REFRESHALL = 0,
|
||||||
GETALL,
|
GETALL = 1,
|
||||||
MODIFIED,
|
MODIFIED = 2,
|
||||||
REMOVE
|
REMOVE = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupMemberCache {
|
export interface GroupMemberCache {
|
||||||
|
@@ -7,4 +7,6 @@ export * from './system';
|
|||||||
export * from './webapi';
|
export * from './webapi';
|
||||||
export * from './sign';
|
export * from './sign';
|
||||||
export * from './element';
|
export * from './element';
|
||||||
export * from './constant';
|
export * from './constant';
|
||||||
|
export * from './graytip';
|
||||||
|
export * from './emoji';
|
@@ -1,6 +1,10 @@
|
|||||||
import { NTGroupMemberRole } from '@/core';
|
import { NTGroupMemberRole } from '@/core';
|
||||||
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
|
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 2024/11/22 Refactor Mlikiowa
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表示对等方的信息
|
* 表示对等方的信息
|
||||||
*/
|
*/
|
||||||
@@ -127,7 +131,7 @@ export enum PicSubType {
|
|||||||
KRELATED = 7
|
KRELATED = 7
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 消息@类型枚举
|
* 消息AT类型枚举
|
||||||
*/
|
*/
|
||||||
export enum NTMsgAtType {
|
export enum NTMsgAtType {
|
||||||
ATTYPEALL = 1,
|
ATTYPEALL = 1,
|
||||||
@@ -260,16 +264,6 @@ export enum NTGrayTipElementSubTypeV2 {
|
|||||||
GRAYTIP_ELEMENT_SUBTYPE_XMLMSG = 12,
|
GRAYTIP_ELEMENT_SUBTYPE_XMLMSG = 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 表情类型枚举
|
|
||||||
*/
|
|
||||||
export enum FaceType {
|
|
||||||
normal = 1, // 小黄脸
|
|
||||||
normal2 = 2, // 新小黄脸
|
|
||||||
dice = 3, // 骰子
|
|
||||||
poke = 5 // 拍一拍
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poke 类型枚举
|
* Poke 类型枚举
|
||||||
*/
|
*/
|
||||||
@@ -288,8 +282,8 @@ export enum PokeType {
|
|||||||
* 表情索引枚举
|
* 表情索引枚举
|
||||||
*/
|
*/
|
||||||
export enum FaceIndex {
|
export enum FaceIndex {
|
||||||
dice = 358,
|
DICE = 358,
|
||||||
rps = 359
|
RPS = 359
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -389,12 +383,39 @@ export enum MemberAddShowType {
|
|||||||
K_YOU_INVITE_OTHER = 7,
|
K_YOU_INVITE_OTHER = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群提示元素成员角色枚举
|
||||||
|
*/
|
||||||
|
export enum NTGroupGrayElementRole {
|
||||||
|
KOTHER = 0,
|
||||||
|
KMEMBER = 1,
|
||||||
|
KADMIN = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群灰色提示成员接口
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface NTGroupGrayMember {
|
||||||
|
serialVersionUID: string;
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 群灰色提示邀请者和被邀请者接口
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
export interface NTGroupGrayInviterAndInvite {
|
||||||
|
invited: NTGroupGrayMember;
|
||||||
|
inviter: NTGroupGrayMember;
|
||||||
|
serialVersionUID: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 群提示元素接口
|
* 群提示元素接口
|
||||||
*/
|
*/
|
||||||
export interface TipGroupElement {
|
export interface TipGroupElement {
|
||||||
type: TipGroupElementType;
|
type: TipGroupElementType;
|
||||||
role: 0;
|
role: NTGroupGrayElementRole;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
memberUid: string;
|
memberUid: string;
|
||||||
memberNick: string;
|
memberNick: string;
|
||||||
@@ -405,13 +426,13 @@ export interface TipGroupElement {
|
|||||||
createGroup: null;
|
createGroup: null;
|
||||||
memberAdd?: {
|
memberAdd?: {
|
||||||
showType: MemberAddShowType;
|
showType: MemberAddShowType;
|
||||||
otherAdd: null;
|
otherAdd: NTGroupGrayMember;
|
||||||
otherAddByOtherQRCode: null;
|
otherAddByOtherQRCode: NTGroupGrayInviterAndInvite;
|
||||||
otherAddByYourQRCode: null;
|
otherAddByYourQRCode: NTGroupGrayMember;
|
||||||
youAddByOtherQRCode: null;
|
youAddByOtherQRCode: NTGroupGrayMember;
|
||||||
otherInviteOther: null;
|
otherInviteOther: NTGroupGrayInviterAndInvite;
|
||||||
otherInviteYou: null;
|
otherInviteYou: NTGroupGrayMember;
|
||||||
youInviteOther: null
|
youInviteOther: NTGroupGrayMember;
|
||||||
};
|
};
|
||||||
shutUp?: {
|
shutUp?: {
|
||||||
curTime: string;
|
curTime: string;
|
||||||
@@ -487,7 +508,7 @@ export interface RawMessage {
|
|||||||
*/
|
*/
|
||||||
export interface QueryMsgsParams {
|
export interface QueryMsgsParams {
|
||||||
chatInfo: Peer;
|
chatInfo: Peer;
|
||||||
filterMsgType: [];
|
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
||||||
filterSendersUid: string[];
|
filterSendersUid: string[];
|
||||||
filterMsgFromTime: string;
|
filterMsgFromTime: string;
|
||||||
filterMsgToTime: string;
|
filterMsgToTime: string;
|
||||||
@@ -532,4 +553,16 @@ export interface MsgReqType {
|
|||||||
includeSelf: boolean,
|
includeSelf: boolean,
|
||||||
includeDeleteMsg: boolean,
|
includeDeleteMsg: boolean,
|
||||||
extraCnt: number
|
extraCnt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表情类型枚举
|
||||||
|
*/
|
||||||
|
export enum FaceType {
|
||||||
|
Unknown = 0,
|
||||||
|
OldFace = 1, // 老表情
|
||||||
|
Normal = 2, // 常规表情
|
||||||
|
AniSticke = 3, // 动画贴纸
|
||||||
|
Lottie = 4,// 新格式表情
|
||||||
|
Poke = 5 // 可变Poke
|
||||||
}
|
}
|
@@ -1,20 +1,20 @@
|
|||||||
export enum GroupNotifyMsgType {
|
export enum GroupNotifyMsgType {
|
||||||
UN_SPECIFIED,
|
UN_SPECIFIED = 0,
|
||||||
INVITED_BY_MEMBER,
|
INVITED_BY_MEMBER = 1,
|
||||||
REFUSE_INVITED,
|
REFUSE_INVITED = 2,
|
||||||
REFUSED_BY_ADMINI_STRATOR,
|
REFUSED_BY_ADMINI_STRATOR = 3,
|
||||||
AGREED_TOJOIN_DIRECT,// 有人接受了邀请入群
|
AGREED_TOJOIN_DIRECT = 4,// 有人接受了邀请入群
|
||||||
INVITED_NEED_ADMINI_STRATOR_PASS,
|
INVITED_NEED_ADMINI_STRATOR_PASS = 5,
|
||||||
AGREED_TO_JOIN_BY_ADMINI_STRATOR,
|
AGREED_TO_JOIN_BY_ADMINI_STRATOR = 6,
|
||||||
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS,
|
REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS = 7,
|
||||||
SET_ADMIN,
|
SET_ADMIN = 8,
|
||||||
KICK_MEMBER_NOTIFY_ADMIN,
|
KICK_MEMBER_NOTIFY_ADMIN = 9,
|
||||||
KICK_MEMBER_NOTIFY_KICKED,
|
KICK_MEMBER_NOTIFY_KICKED = 10,
|
||||||
MEMBER_LEAVE_NOTIFY_ADMIN,// 主动退出
|
MEMBER_LEAVE_NOTIFY_ADMIN = 11,// 主动退出
|
||||||
CANCEL_ADMIN_NOTIFY_CANCELED,
|
CANCEL_ADMIN_NOTIFY_CANCELED = 12,
|
||||||
CANCEL_ADMIN_NOTIFY_ADMIN,// 其他人取消管理员
|
CANCEL_ADMIN_NOTIFY_ADMIN = 13,// 其他人取消管理员
|
||||||
TRANSFER_GROUP_NOTIFY_OLDOWNER,
|
TRANSFER_GROUP_NOTIFY_OLDOWNER = 14,
|
||||||
TRANSFER_GROUP_NOTIFY_ADMIN
|
TRANSFER_GROUP_NOTIFY_ADMIN = 15
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupNotifies {
|
export interface GroupNotifies {
|
||||||
@@ -24,24 +24,24 @@ export interface GroupNotifies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupNotifyMsgStatus {
|
export enum GroupNotifyMsgStatus {
|
||||||
KINIT,//初始化
|
KINIT = 0,//初始化
|
||||||
KUNHANDLE,//未处理
|
KUNHANDLE = 1,//未处理
|
||||||
KAGREED,//同意
|
KAGREED = 2,//同意
|
||||||
KREFUSED,//拒绝
|
KREFUSED = 3,//拒绝
|
||||||
KIGNORED//忽略
|
KIGNORED = 4//忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupInviteStatus {
|
export enum GroupInviteStatus {
|
||||||
INIT,
|
INIT = 0,
|
||||||
WAIT_TO_APPROVE,
|
WAIT_TO_APPROVE = 1,
|
||||||
JOINED,
|
JOINED = 2,
|
||||||
REFUSED_BY_ADMINI_STRATOR
|
REFUSED_BY_ADMINI_STRATOR = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupInviteType {
|
export enum GroupInviteType {
|
||||||
BYBUDDY,
|
BYBUDDY = 0,
|
||||||
BYGROUPMEMBER,
|
BYGROUPMEMBER = 1,
|
||||||
BYDISCUSSMEMBER
|
BYDISCUSSMEMBER = 2
|
||||||
}
|
}
|
||||||
export interface ShutUpGroupHonor {
|
export interface ShutUpGroupHonor {
|
||||||
[key: string]: number;
|
[key: string]: number;
|
||||||
@@ -116,20 +116,20 @@ export enum NTGroupRequestOperateTypes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum BuddyReqType {
|
export enum BuddyReqType {
|
||||||
KMEINITIATOR,
|
KMEINITIATOR = 0,
|
||||||
KPEERINITIATOR,
|
KPEERINITIATOR = 1,
|
||||||
KMEAGREED,
|
KMEAGREED = 2,
|
||||||
KMEAGREEDANDADDED,
|
KMEAGREEDANDADDED = 3,
|
||||||
KPEERAGREED,
|
KPEERAGREED = 4,
|
||||||
KPEERAGREEDANDADDED,
|
KPEERAGREEDANDADDED = 5,
|
||||||
KPEERREFUSED,
|
KPEERREFUSED = 6,
|
||||||
KMEREFUSED,
|
KMEREFUSED = 7,
|
||||||
KMEIGNORED,
|
KMEIGNORED = 8,
|
||||||
KMEAGREEANYONE,
|
KMEAGREEANYONE = 9,
|
||||||
KMESETQUESTION,
|
KMESETQUESTION = 10,
|
||||||
KMEAGREEANDADDFAILED,
|
KMEAGREEANDADDFAILED = 11,
|
||||||
KMSGINFO,
|
KMSGINFO = 12,
|
||||||
KMEINITIATORWAITPEERCONFIRM
|
KMEINITIATORWAITPEERCONFIRM = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FriendRequest {
|
export interface FriendRequest {
|
||||||
|
@@ -322,8 +322,8 @@ export type Friend = User;
|
|||||||
|
|
||||||
// 业务键枚举
|
// 业务键枚举
|
||||||
export enum BizKey {
|
export enum BizKey {
|
||||||
KPRIVILEGEICON,
|
KPRIVILEGEICON = 0,
|
||||||
KPHOTOWALL
|
KPHOTOWALL = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据UIN获取用户详细信息
|
// 根据UIN获取用户详细信息
|
||||||
@@ -347,9 +347,9 @@ export enum UserDetailSource {
|
|||||||
|
|
||||||
// 个人资料业务类型枚举
|
// 个人资料业务类型枚举
|
||||||
export enum ProfileBizType {
|
export enum ProfileBizType {
|
||||||
KALL,
|
KALL = 0,
|
||||||
KBASEEXTEND,
|
KBASEEXTEND = 1,
|
||||||
KVAS,
|
KVAS = 2,
|
||||||
KQZONE,
|
KQZONE = 3,
|
||||||
KOTHER
|
KOTHER = 4
|
||||||
}
|
}
|
@@ -1,6 +1,26 @@
|
|||||||
//LiteLoader需要提供部分IPC接口,以便于其他插件调用
|
//LiteLoader需要提供部分IPC接口,以便于其他插件调用
|
||||||
const { ipcMain } = require('electron');
|
const { ipcMain, BrowserWindow } = require('electron');
|
||||||
const napcat = require('./napcat.cjs');
|
const napcat = require('./napcat.cjs');
|
||||||
|
const { shell } = require('electron');
|
||||||
ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
|
ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
|
||||||
return napcat.NCgetWebUiUrl();
|
return napcat.NCgetWebUiUrl();
|
||||||
});
|
});
|
||||||
|
ipcMain.on('open_external_url', (event, url) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
});
|
||||||
|
ipcMain.handle('napcat_get_reactweb', async (event, arg) => {
|
||||||
|
let url = new URL(await napcat.NCgetWebUiUrl());
|
||||||
|
let port = url.port;
|
||||||
|
let token = url.searchParams.get('token');
|
||||||
|
return `https://napcat.152710.xyz/web_login?back=http://127.0.0.1:${port}&token=${token}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on('napcat_open_inner_url', (event, url) => {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
});
|
||||||
|
win.loadURL(url);
|
||||||
|
win.webContents.setWindowOpenHandler(details => {
|
||||||
|
win.loadURL(details.url)
|
||||||
|
})
|
||||||
|
});
|
@@ -9,6 +9,7 @@ import { NodeIKernelLoginService } from '@/core/services';
|
|||||||
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
|
||||||
import { InitWebUi, WebUiConfig } from '@/webui';
|
import { InitWebUi, WebUiConfig } from '@/webui';
|
||||||
import { NapCatOneBot11Adapter } from '@/onebot';
|
import { NapCatOneBot11Adapter } from '@/onebot';
|
||||||
|
import { UmamiTrace } from '@/common/umami';
|
||||||
|
|
||||||
//Framework ES入口文件
|
//Framework ES入口文件
|
||||||
export async function getWebUiUrl() {
|
export async function getWebUiUrl() {
|
||||||
@@ -25,8 +26,10 @@ export async function NCoreInitFramework(
|
|||||||
console.log('NapCat Framework App Loading...');
|
console.log('NapCat Framework App Loading...');
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
|
UmamiTrace.sendEvent('framework/error', { name: err.name });
|
||||||
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
|
console.log('[NapCat] [Error] Unhandled Exception:', err.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
console.log('[NapCat] [Error] unhandledRejection:', reason);
|
console.log('[NapCat] [Error] unhandledRejection:', reason);
|
||||||
});
|
});
|
||||||
@@ -35,6 +38,10 @@ export async function NCoreInitFramework(
|
|||||||
const logger = new LogWrapper(pathWrapper.logsPath);
|
const logger = new LogWrapper(pathWrapper.logsPath);
|
||||||
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
|
||||||
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
|
||||||
|
let guid = loginService.getMachineGuid();
|
||||||
|
UmamiTrace.init(basicInfoWrapper.getFullQQVesion(), guid);
|
||||||
|
UmamiTrace.sendTrace('framework/boot');
|
||||||
|
UmamiTrace.sendEvent('framework/login');
|
||||||
//直到登录成功后,执行下一步
|
//直到登录成功后,执行下一步
|
||||||
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
const selfInfo = await new Promise<SelfInfo>((resolveSelfInfo) => {
|
||||||
const loginListener = new NodeIKernelLoginListener();
|
const loginListener = new NodeIKernelLoginListener();
|
||||||
@@ -58,7 +65,7 @@ export async function NCoreInitFramework(
|
|||||||
await loaderObject.core.initCore();
|
await loaderObject.core.initCore();
|
||||||
|
|
||||||
//启动WebUi
|
//启动WebUi
|
||||||
InitWebUi(logger, pathWrapper).then().catch(logger.logError.bind(logger));
|
InitWebUi(logger, pathWrapper).then().catch(e => logger.logError(e));
|
||||||
//初始化LLNC的Onebot实现
|
//初始化LLNC的Onebot实现
|
||||||
await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();
|
await new NapCatOneBot11Adapter(loaderObject.core, loaderObject.context, pathWrapper).InitOneBot();
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,17 @@
|
|||||||
const { contextBridge } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
const { ipcRenderer } = require('electron');
|
|
||||||
|
|
||||||
const napcat = {
|
const napcat = {
|
||||||
getWebUiUrl: async () => {
|
getWebUiUrl: async () => {
|
||||||
return ipcRenderer.invoke('napcat_get_webtoken');
|
return ipcRenderer.invoke('napcat_get_webtoken');
|
||||||
},
|
},
|
||||||
|
openExternalUrl: async (url) => {
|
||||||
|
ipcRenderer.send('open_external_url', url);
|
||||||
|
},
|
||||||
|
openInnerUrl: async (url) => {
|
||||||
|
ipcRenderer.send('napcat_open_inner_url', url);
|
||||||
|
},
|
||||||
|
getWebUiUrlReact: async () => {
|
||||||
|
return ipcRenderer.invoke('napcat_get_reactweb');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// 在window对象下导出只读对象
|
// 在window对象下导出只读对象
|
||||||
contextBridge.exposeInMainWorld('napcat', napcat);
|
contextBridge.exposeInMainWorld('napcat', napcat);
|
@@ -1,27 +1,20 @@
|
|||||||
export const onSettingWindowCreated = async (view) => {
|
export const onSettingWindowCreated = async (view) => {
|
||||||
|
|
||||||
// view.style.width = "100%";
|
|
||||||
// view.style.height = "100%";
|
|
||||||
// //添加iframe
|
|
||||||
// const iframe = document.createElement("iframe");
|
|
||||||
// iframe.src = await window.napcat.getWebUiUrl();
|
|
||||||
// iframe.width = "100%";
|
|
||||||
// iframe.height = "100%";
|
|
||||||
// iframe.style.border = "none";
|
|
||||||
// //去掉iframe滚动条
|
|
||||||
// //iframe.scrolling = "no";
|
|
||||||
// //有滚动条何尝不是一种美
|
|
||||||
// view.appendChild(iframe);
|
|
||||||
let webui = await window.napcat.getWebUiUrl();
|
let webui = await window.napcat.getWebUiUrl();
|
||||||
|
let webuiReact = await window.napcat.getWebUiUrlReact();
|
||||||
view.innerHTML = `
|
view.innerHTML = `
|
||||||
<setting-section data-title="">
|
<setting-section data-title="">
|
||||||
<setting-panel>
|
<setting-panel>
|
||||||
<setting-list data-direction="column">
|
<setting-list data-direction="column">
|
||||||
<setting-item>
|
<setting-item>
|
||||||
<setting-button data-type="primary" class="nc_openwebui">打开配置页面</setting-button>
|
<setting-button data-type="primary" class="nc_openwebui">在QQ内打开配置页面(VUE)</setting-button>
|
||||||
|
<setting-button data-type="primary" class="nc_openwebui_ex">在默认浏览器打开配置页面(VUE)</setting-button>
|
||||||
|
</setting-item>
|
||||||
|
<setting-item>
|
||||||
|
<setting-button data-type="primary" class="nc_openwebui_ex_react">在默认浏览器打开配置页面(React)</setting-button>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
<setting-item>
|
<setting-item>
|
||||||
<div>
|
<div>
|
||||||
|
<setting-text>WebUi远程地址可以点击下方复制哦~</setting-text>
|
||||||
<setting-text class="nc_webui">WebUi</setting-text>
|
<setting-text class="nc_webui">WebUi</setting-text>
|
||||||
</div>
|
</div>
|
||||||
</setting-item>
|
</setting-item>
|
||||||
@@ -29,8 +22,27 @@ export const onSettingWindowCreated = async (view) => {
|
|||||||
</setting-panel>
|
</setting-panel>
|
||||||
</setting-section>
|
</setting-section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
view.querySelector('.nc_openwebui').addEventListener('click', () => {
|
view.querySelector('.nc_openwebui').addEventListener('click', () => {
|
||||||
window.open(webui, '_blank');
|
window.napcat.openInnerUrl(webui);
|
||||||
});
|
});
|
||||||
|
view.querySelector('.nc_openwebui_ex').addEventListener('click', () => {
|
||||||
|
window.napcat.openExternalUrl(webui);
|
||||||
|
});
|
||||||
|
|
||||||
|
view.querySelector('.nc_openwebui_ex_react').addEventListener('click', () => {
|
||||||
|
window.napcat.openExternalUrl(webuiReact);
|
||||||
|
});
|
||||||
|
|
||||||
view.querySelector('.nc_webui').innerText = webui;
|
view.querySelector('.nc_webui').innerText = webui;
|
||||||
};
|
|
||||||
|
// 添加点击复制功能
|
||||||
|
view.querySelector('.nc_webui').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(webui);
|
||||||
|
alert('WebUi URL 已复制到剪贴板');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('复制到剪贴板失败: ', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,7 +1,6 @@
|
|||||||
import { ActionName, BaseCheckResult } from './router';
|
import { ActionName, BaseCheckResult } from './router';
|
||||||
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
|
import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
|
||||||
import { NapCatCore } from '@/core';
|
import { NapCatCore } from '@/core';
|
||||||
import { isNull } from '@/common/helper';
|
|
||||||
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
|
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
|
||||||
|
|
||||||
export class OB11Response {
|
export class OB11Response {
|
||||||
@@ -30,7 +29,7 @@ export class OB11Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
||||||
actionName: ActionName = ActionName.Unknown;
|
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
private validate: ValidateFunction<any> | undefined = undefined;
|
private validate: ValidateFunction<any> | undefined = undefined;
|
||||||
payloadSchema: any = undefined;
|
payloadSchema: any = undefined;
|
||||||
@@ -43,7 +42,7 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
|||||||
|
|
||||||
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
|
protected async check(payload: PayloadType): Promise<BaseCheckResult> {
|
||||||
if (this.payloadSchema) {
|
if (this.payloadSchema) {
|
||||||
this.validate = new Ajv({ allowUnionTypes: true }).compile(this.payloadSchema);
|
this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true }).compile(this.payloadSchema);
|
||||||
}
|
}
|
||||||
if (this.validate && !this.validate(payload)) {
|
if (this.validate && !this.validate(payload)) {
|
||||||
const errors = this.validate.errors as ErrorObject[];
|
const errors = this.validate.errors as ErrorObject[];
|
||||||
@@ -66,7 +65,7 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
|||||||
return OB11Response.ok(resData);
|
return OB11Response.ok(resData);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.core.context.logger.logError('发生错误', e);
|
this.core.context.logger.logError('发生错误', e);
|
||||||
return OB11Response.error(e?.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200);
|
return OB11Response.error((e as Error).message.toString() || e?.stack?.toString() || '未知错误,可能操作超时', 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +79,9 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
|
|||||||
return OB11Response.ok(resData, echo);
|
return OB11Response.ok(resData, echo);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.core.context.logger.logError('发生错误', e);
|
this.core.context.logger.logError('发生错误', e);
|
||||||
return OB11Response.error(e.toString() || e.stack?.toString(), 1200, echo);
|
return OB11Response.error((e as Error).message.toString() || e.stack?.toString(), 1200, echo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
|
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>;
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,13 @@
|
|||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = {
|
const SchemaData = Type.Object({
|
||||||
type: 'object',
|
rawData: Type.String(),
|
||||||
properties: {
|
brief: Type.String(),
|
||||||
rawData: { type: 'string' },
|
});
|
||||||
brief: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['brief', 'rawData'],
|
|
||||||
} as const satisfies JSONSchema;
|
|
||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
export class CreateCollection extends OneBotAction<Payload, any> {
|
export class CreateCollection extends OneBotAction<Payload, any> {
|
||||||
actionName = ActionName.CreateCollection;
|
actionName = ActionName.CreateCollection;
|
||||||
@@ -25,4 +21,4 @@ export class CreateCollection extends OneBotAction<Payload, any> {
|
|||||||
payload.brief, payload.rawData,
|
payload.brief, payload.rawData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,23 +1,19 @@
|
|||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
|
||||||
const SchemaData = {
|
const SchemaData = Type.Object({
|
||||||
type: 'object',
|
count: Type.Union([Type.Number(), Type.String()], { default: 48 }),
|
||||||
properties: {
|
});
|
||||||
count: { type: ['number', 'string'] },
|
|
||||||
},
|
|
||||||
} as const satisfies JSONSchema;
|
|
||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
|
export class FetchCustomFace extends OneBotAction<Payload, string[]> {
|
||||||
actionName = ActionName.FetchCustomFace;
|
actionName = ActionName.FetchCustomFace;
|
||||||
payloadSchema = SchemaData;
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
//48 可能正好是QQ需要的一个页面的数量 Tagged Mlikiowa
|
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+payload.count);
|
||||||
const ret = await this.core.apis.MsgApi.fetchFavEmojiList(+(payload.count ?? 48));
|
|
||||||
return ret.emojiInfoList.map(e => e.url);
|
return ret.emojiInfoList.map(e => e.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,32 +1,27 @@
|
|||||||
//getMsgEmojiLikesList
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
|
||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { MessageUnique } from '@/common/message-unique';
|
import { MessageUnique } from '@/common/message-unique';
|
||||||
|
|
||||||
const SchemaData = {
|
const SchemaData = Type.Object({
|
||||||
type: 'object',
|
message_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
properties: {
|
emojiId: Type.Union([Type.Number(), Type.String()]),
|
||||||
user_id: { type: 'string' },
|
emojiType: Type.Union([Type.Number(), Type.String()]),
|
||||||
group_id: { type: 'string' },
|
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
|
||||||
emojiId: { type: 'string' },
|
});
|
||||||
emojiType: { type: 'string' },
|
|
||||||
message_id: { type: ['string', 'number'] },
|
|
||||||
count: { type: ['string', 'number'] },
|
|
||||||
},
|
|
||||||
required: ['emojiId', 'emojiType', 'message_id'],
|
|
||||||
} as const satisfies JSONSchema;
|
|
||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
export class FetchEmojiLike extends OneBotAction<Payload, any> {
|
export class FetchEmojiLike extends OneBotAction<Payload, any> {
|
||||||
actionName = ActionName.FetchEmojiLike;
|
actionName = ActionName.FetchEmojiLike;
|
||||||
payloadSchema = SchemaData;
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(parseInt(payload.message_id.toString()));
|
const msgIdPeer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
|
||||||
if (!msgIdPeer) throw new Error('消息不存在');
|
if (!msgIdPeer) throw new Error('消息不存在');
|
||||||
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
|
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(msgIdPeer.Peer, [msgIdPeer.MsgId])).msgList[0];
|
||||||
return await this.core.apis.MsgApi.getMsgEmojiLikesList(msgIdPeer.Peer, msg.msgSeq, payload.emojiId, payload.emojiType, +(payload.count ?? 20));
|
return await this.core.apis.MsgApi.getMsgEmojiLikesList(
|
||||||
|
msgIdPeer.Peer, msg.msgSeq, payload.emojiId.toString(), payload.emojiType.toString(), +payload.count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,17 @@
|
|||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
export class FetchUserProfileLike extends OneBotAction<{ qq: number }, any> {
|
const SchemaData = Type.Object({
|
||||||
|
user_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
|
export class FetchUserProfileLike extends OneBotAction<Payload, any> {
|
||||||
actionName = ActionName.FetchUserProfileLike;
|
actionName = ActionName.FetchUserProfileLike;
|
||||||
|
|
||||||
async _handle(payload: { qq: number }) {
|
async _handle(payload: Payload) {
|
||||||
if (!payload.qq) throw new Error('qq is required');
|
return await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
|
||||||
return await this.core.apis.UserApi.getUidByUinV2(payload.qq.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,14 @@
|
|||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
|
||||||
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
|
||||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = {
|
const SchemaData = Type.Object({
|
||||||
type: 'object',
|
group_id: Type.Union([Type.Number(), Type.String()]),
|
||||||
properties: {
|
chat_type: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
|
||||||
group_id: { type: ['number', 'string'] },
|
});
|
||||||
chat_type: { type: ['number', 'string'] },
|
|
||||||
},
|
|
||||||
required: ['group_id'],
|
|
||||||
} as const satisfies JSONSchema;
|
|
||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
interface GetAiCharactersResponse {
|
interface GetAiCharactersResponse {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -28,7 +24,7 @@ export class GetAiCharacters extends GetPacketStatusDepends<Payload, GetAiCharac
|
|||||||
payloadSchema = SchemaData;
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +(payload.chat_type ?? 1) as AIVoiceChatType);
|
const rawList = await this.core.apis.PacketApi.pkt.operation.FetchAiVoiceList(+payload.group_id, +payload.chat_type as AIVoiceChatType);
|
||||||
return rawList?.map((item) => ({
|
return rawList?.map((item) => ({
|
||||||
type: item.category,
|
type: item.category,
|
||||||
characters: item.voices.map((voice) => ({
|
characters: item.voices.map((voice) => ({
|
||||||
|
14
src/onebot/action/extends/GetClientkey.ts
Normal file
14
src/onebot/action/extends/GetClientkey.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { OneBotAction } from '../OneBotAction';
|
||||||
|
|
||||||
|
interface GetClientkeyResponse {
|
||||||
|
clientkey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
|
||||||
|
actionName = ActionName.GetClientkey;
|
||||||
|
|
||||||
|
async _handle() {
|
||||||
|
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
|
||||||
|
}
|
||||||
|
}
|
@@ -1,23 +1,19 @@
|
|||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
|
||||||
const SchemaData = {
|
const SchemaData = Type.Object({
|
||||||
type: 'object',
|
category: Type.Union([Type.Number(), Type.String()]),
|
||||||
properties: {
|
count: Type.Union([Type.Union([Type.Number(), Type.String()])], { default: 1 }),
|
||||||
category: { type: ['number', 'string'] },
|
});
|
||||||
count: { type: ['number', 'string'] },
|
|
||||||
},
|
|
||||||
required: ['category', 'count'],
|
|
||||||
} as const satisfies JSONSchema;
|
|
||||||
|
|
||||||
type Payload = FromSchema<typeof SchemaData>;
|
type Payload = Static<typeof SchemaData>;
|
||||||
|
|
||||||
export class GetCollectionList extends OneBotAction<Payload, any> {
|
export class GetCollectionList extends OneBotAction<Payload, any> {
|
||||||
actionName = ActionName.GetCollectionList;
|
actionName = ActionName.GetCollectionList;
|
||||||
payloadSchema = SchemaData;
|
payloadSchema = SchemaData;
|
||||||
|
|
||||||
async _handle(payload: Payload) {
|
async _handle(payload: Payload) {
|
||||||
return await this.core.apis.CollectionApi.getAllCollection(parseInt(payload.category.toString()), +(payload.count ?? 1));
|
return await this.core.apis.CollectionApi.getAllCollection(+payload.category, +payload.count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,33 +1,37 @@
|
|||||||
import { GroupNotifyMsgStatus } from '@/core';
|
import { GroupNotifyMsgStatus } from '@/core';
|
||||||
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
import { OneBotAction } from '@/onebot/action/OneBotAction';
|
||||||
import { ActionName } from '@/onebot/action/router';
|
import { ActionName } from '@/onebot/action/router';
|
||||||
|
import { Notify } from '@/onebot/types';
|
||||||
|
|
||||||
interface OB11GroupRequestNotify {
|
export default class GetGroupAddRequest extends OneBotAction<null, Notify[] | null> {
|
||||||
group_id: number,
|
|
||||||
user_id: number,
|
|
||||||
flag: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GetGroupAddRequest extends OneBotAction<null, OB11GroupRequestNotify[] | null> {
|
|
||||||
actionName = ActionName.GetGroupIgnoreAddRequest;
|
actionName = ActionName.GetGroupIgnoreAddRequest;
|
||||||
|
|
||||||
async _handle(payload: null): Promise<OB11GroupRequestNotify[] | null> {
|
async _handle(payload: null): Promise<Notify[] | null> {
|
||||||
const ignoredNotifies = await this.core.apis.GroupApi.getSingleScreenNotifies(true, 10);
|
const NTQQUserApi = this.core.apis.UserApi;
|
||||||
const retData: any = {
|
const NTQQGroupApi = this.core.apis.GroupApi;
|
||||||
join_requests: await Promise.all(
|
const ignoredNotifies = await NTQQGroupApi.getSingleScreenNotifies(true, 10);
|
||||||
ignoredNotifies
|
const retData: Notify[] = [];
|
||||||
.filter(notify => notify.type === 7)
|
|
||||||
.map(async SSNotify => ({
|
const notifyPromises = ignoredNotifies
|
||||||
request_id: SSNotify.seq,
|
.filter(notify => notify.type === 7)
|
||||||
requester_uin: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user1?.uid),
|
.map(async SSNotify => {
|
||||||
requester_nick: SSNotify.user1?.nickName,
|
const invitorUin = SSNotify.user1?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user1.uid) : 0;
|
||||||
group_id: SSNotify.group?.groupCode,
|
const actorUin = SSNotify.user2?.uid ? +await NTQQUserApi.getUinByUidV2(SSNotify.user2.uid) : 0;
|
||||||
group_name: SSNotify.group?.groupName,
|
retData.push({
|
||||||
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
request_id: +SSNotify.seq,
|
||||||
actor: await this.core.apis.UserApi.getUinByUidV2(SSNotify.user2?.uid) || 0,
|
invitor_uin: invitorUin,
|
||||||
}))),
|
invitor_nick: SSNotify.user1?.nickName,
|
||||||
};
|
group_id: +SSNotify.group?.groupCode,
|
||||||
|
message: SSNotify?.postscript,
|
||||||
|
group_name: SSNotify.group?.groupName,
|
||||||
|
checked: SSNotify.status !== GroupNotifyMsgStatus.KUNHANDLE,
|
||||||
|
actor: actorUin,
|
||||||
|
requester_nick: SSNotify.user1?.nickName,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(notifyPromises);
|
||||||
|
|
||||||
return retData;
|
return retData;
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user