Compare commits

..

146 Commits

Author SHA1 Message Date
手瓜一十雪
57112c21a2 refactor: flag handle&onebot标准化 2024-12-05 14:17:09 +08:00
手瓜一十雪
0e8ceeb6c9 refactor: CardChangedEvent 2024-12-05 11:36:06 +08:00
手瓜一十雪
f52b8d1f04 feat: 30366 全平台通用性适配 2024-12-05 11:15:10 +08:00
手瓜一十雪
f374cc77ae chore: readme 2024-12-04 23:04:37 +08:00
手瓜一十雪
7c694e7fae chore: 跑路的规范/困难的实现 2024-12-04 23:03:46 +08:00
Mlikiowa
932ffc2673 release: v4.2.23 2024-12-04 14:18:02 +00:00
手瓜一十雪
3de5438139 fix: poke report 2024-12-04 22:16:59 +08:00
手瓜一十雪
c4b5f34271 fix: 兜底 防止进入影响速度 2024-12-04 22:02:11 +08:00
手瓜一十雪
22d3ac33a2 Refactor: 更新群组通知处理逻辑,优化数据结构和异步处理 2024-12-04 21:59:53 +08:00
Mlikiowa
2e5dd6535a release: v4.2.22 2024-12-04 13:18:58 +00:00
手瓜一十雪
eac58a2a50 fix: 9.9.17-30366 2024-12-04 21:04:24 +08:00
手瓜一十雪
e939ec0e52 fix: #597 2024-12-04 20:37:08 +08:00
Mlikiowa
5b17a14a2a release: v4.2.21 2024-12-04 11:49:26 +00:00
手瓜一十雪
8fb8c888f5 refactor: 移除未使用的uidCache和uinCache逻辑 2024-12-04 19:48:59 +08:00
Mlikiowa
4a2884509e release: v4.2.20 2024-12-04 11:46:12 +00:00
手瓜一十雪
e295235a89 fix: #596 2024-12-04 19:45:46 +08:00
手瓜一十雪
ef515a38d0 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 18:50:55 +08:00
手瓜一十雪
02cff040e3 refactor: 移除未使用的createUidFromTinyId和getStatusByUid方法 2024-12-04 18:50:51 +08:00
Mlikiowa
bb0f65a52d release: v4.2.19 2024-12-04 10:42:32 +00:00
手瓜一十雪
d51d6a5cc1 refactor: getUidByUinV2/getUinByUidV2 2024-12-04 18:41:28 +08:00
手瓜一十雪
eb99379a79 fix: 性能优化 2024-12-04 18:29:33 +08:00
Mlikiowa
388eb57d0d release: v4.2.18 2024-12-04 03:40:59 +00:00
手瓜一十雪
0b8131392a chore: 移出调试 2024-12-04 11:40:36 +08:00
手瓜一十雪
229efbd006 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-04 11:39:09 +08:00
手瓜一十雪
a482fa3a8d refactor: 优化调整文件处理 2024-12-04 11:38:59 +08:00
Mlikiowa
6cf047af39 release: v4.2.17 2024-12-04 02:57:21 +00:00
手瓜一十雪
41748c0b3f refactor: 数据清理与刷新 2024-12-04 10:55:56 +08:00
手瓜一十雪
1ce8be3c7e fix: 精简GroupApi实现 2024-12-04 10:47:21 +08:00
手瓜一十雪
32778acf57 refactor: NTQQGroupApi 2024-12-04 10:43:48 +08:00
Mlikiowa
a3c71473ae release: v4.2.16 2024-12-03 14:15:16 +00:00
手瓜一十雪
aceece7e90 fix: code 2024-12-03 22:12:57 +08:00
Mlikiowa
52efb4f9ef release: v4.2.15 2024-12-03 14:07:35 +00:00
手瓜一十雪
6b0d96fe8d fix: 移除复杂信息 2024-12-03 22:07:11 +08:00
Mlikiowa
ad052821b0 release: v4.2.14 2024-12-03 13:50:37 +00:00
手瓜一十雪
da7636e60c fix: #592 2024-12-03 21:50:12 +08:00
Mlikiowa
ef01dd0d77 release: v4.2.13 2024-12-03 13:43:37 +00:00
手瓜一十雪
03f7d4673f fix: code 2024-12-03 21:42:08 +08:00
手瓜一十雪
94e9c87978 fix: 优化处理 2024-12-03 21:42:08 +08:00
手瓜一十雪
501bbbe4df fix 2024-12-03 21:42:08 +08:00
手瓜一十雪
c9122a3fee fix: 临时的抽象方案 2024-12-03 21:42:08 +08:00
手瓜一十雪
8a289d014e fix: error 2024-12-03 21:42:08 +08:00
手瓜一十雪
ddadd38151 refactor: GroupAdminChange 2024-12-03 21:42:08 +08:00
手瓜一十雪
0b8d0e3cac feat: 迁移事件解析原理 2024-12-03 21:42:08 +08:00
手瓜一十雪
eeb27d38bc fix: 清理旧的脚本 2024-12-03 17:33:13 +08:00
Mlikiowa
491a79ec96 release: v4.2.12 2024-12-03 09:22:43 +00:00
手瓜一十雪
f429db61af fix: #594 2024-12-03 17:16:58 +08:00
Mlikiowa
2881099602 release: v4.2.11 2024-12-02 14:08:36 +00:00
手瓜一十雪
672ae8decf fix: Once处理空格目录 中文目录 2024-12-02 22:08:08 +08:00
手瓜一十雪
2abc7e541d fix: 空格目录启动问题 2024-12-02 21:23:13 +08:00
手瓜一十雪
45b1f369ac style: 异步实现 2024-12-02 11:44:37 +08:00
手瓜一十雪
3b5d2c8f6f style: 简化写法 2024-12-02 11:16:12 +08:00
Mlikiowa
5376e16c9f release: v4.2.10 2024-12-01 11:12:10 +00:00
bietiaop
af052242fa feat:带等级的实时日志 2024-12-01 15:19:23 +08:00
手瓜一十雪
85e0b71545 chore: daily -> weekly 2024-12-01 13:26:56 +08:00
手瓜一十雪
1206d1fcf6 chore: bug report 2024-12-01 13:25:36 +08:00
手瓜一十雪
f7534dc438 fix: 自动迁移 2024-12-01 13:20:20 +08:00
手瓜一十雪
97f317254e fix: MiniApp type check 2024-12-01 13:11:56 +08:00
手瓜一十雪
9eaf51e15f fix: nullable 2024-12-01 13:04:00 +08:00
手瓜一十雪
7221f4ac02 fix: type-check 2024-12-01 12:50:13 +08:00
手瓜一十雪
1bb6dce239 refactor: type-check (#586)
* refactor: type-check

* fix: default

* refactor: type-check
2024-12-01 12:41:51 +08:00
bietiaop
d13db5e8eb feat: 实时日志 (#584)
* feat: 历史日志

* feat: 实时日志

* fix: EventEmitter实现事件监听
2024-12-01 09:31:47 +08:00
Mlikiowa
040b5535f3 release: v4.2.9 2024-11-30 06:12:44 +00:00
手瓜一十雪
b44e1618fb fix: quick error 2024-11-30 14:12:23 +08:00
手瓜一十雪
1e13483bc3 fix: type 2024-11-30 13:32:21 +08:00
手瓜一十雪
f9519d3923 style: lint 2024-11-30 13:29:10 +08:00
手瓜一十雪
86cdfbb79b feat: 取消上报 pic_type 2024-11-30 12:11:33 +08:00
手瓜一十雪
a70585e854 feat: 处理失败的情况 2024-11-30 12:08:58 +08:00
Mlikiowa
040d0a8635 release: v4.2.8 2024-11-30 01:39:59 +00:00
手瓜一十雪
efa512ab21 fix: #580 2024-11-30 09:34:03 +08:00
bietiaop
9b04aed8b3 feat: 历史日志 2024-11-30 09:30:13 +08:00
手瓜一十雪
7087eafe37 feat: Universal Package (#578)
* feat: 统一包支持

* feat: Universal
2024-11-29 15:11:35 +08:00
Mlikiowa
c81c4af653 release: v4.2.7 2024-11-29 04:48:36 +00:00
手瓜一十雪
c05cc9dd02 feat: 迁移29927 2024-11-29 12:48:03 +08:00
手瓜一十雪
1a0da00f2d refactor: webui log 移除公网输出 2024-11-29 12:41:52 +08:00
手瓜一十雪
31b0c1d3d7 refactor: react webui 2024-11-29 12:22:25 +08:00
手瓜一十雪
53c1d40bcf refactor: logger bind (#577) 2024-11-28 20:55:28 +08:00
手瓜一十雪
97cacb4383 refactor: framework的操作性 2024-11-28 20:00:24 +08:00
Mlikiowa
e03905abaf release: v4.2.6 2024-11-28 07:28:33 +00:00
手瓜一十雪
06eba28b4c fux: #574 2024-11-28 15:28:09 +08:00
手瓜一十雪
bbfeac46dd Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-28 15:27:26 +08:00
手瓜一十雪
2fe4da094a fix 2024-11-28 15:14:01 +08:00
Mlikiowa
b454d8c0f9 release: v4.2.5 2024-11-28 07:07:10 +00:00
手瓜一十雪
1f9b5453cc fix: #573 2024-11-28 15:06:47 +08:00
Mlikiowa
3261791e99 release: v4.2.4 2024-11-28 03:00:35 +00:00
手瓜一十雪
3bb12e3f45 fix: #572 2024-11-28 10:56:57 +08:00
手瓜一十雪
1dc2f7e5a2 style: lint 2024-11-28 10:46:14 +08:00
手瓜一十雪
2531b08538 refactor: 提高解析兼容 2024-11-28 10:41:51 +08:00
手瓜一十雪
9fcfb5493c fix: #571 2024-11-28 10:27:04 +08:00
Mlikiowa
4576354c51 release: v4.2.3 2024-11-28 01:54:43 +00:00
手瓜一十雪
1dcf2ef0c6 fix: error handle 2024-11-28 09:53:50 +08:00
Mlikiowa
3642c65e8c release: v4.2.2 2024-11-27 12:39:03 +00:00
手瓜一十雪
40e105994a fix: pic size 2024-11-27 20:38:39 +08:00
Mlikiowa
f2ee973882 release: v4.2.1 2024-11-27 11:07:43 +00:00
手瓜一十雪
3aa30792bf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-27 19:07:11 +08:00
手瓜一十雪
6e336fa78e fix: 合并丢失 2024-11-27 19:07:01 +08:00
Mlikiowa
900027a6b7 release: v4.2.0 2024-11-27 10:55:36 +00:00
手瓜一十雪
38bdca2409 fix: qrcode login 2024-11-27 18:51:54 +08:00
手瓜一十雪
7196e476bf Merge pull request #567 from bietiaop/webapi-bietiaop
refactor:优化WebUI后端代码格式(无新功能添加)
2024-11-27 18:39:19 +08:00
手瓜一十雪
e0fd3785d9 Merge pull request #565 from Ander-pixe/webui-new
修改webui
2024-11-27 18:35:13 +08:00
手瓜一十雪
b53ebb6c2a refactor: parse local path 2024-11-27 18:34:33 +08:00
纸凤孤凰
1ea80f4447 fix:修复webui已知bug 2024-11-27 18:16:16 +08:00
手瓜一十雪
627d3c0a7a Merge pull request #570 from NapNeko/dependabot/npm_and_yarn/vite-6.0.1
chore(deps-dev): bump vite from 5.4.11 to 6.0.1
2024-11-27 16:51:20 +08:00
dependabot[bot]
182cccfc71 chore(deps-dev): bump vite from 5.4.11 to 6.0.1
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.11 to 6.0.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.0.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 08:32:10 +00:00
手瓜一十雪
6a3713e86c fix: webui reload 2024-11-27 12:35:10 +08:00
手瓜一十雪
788da4e4f1 style 2024-11-27 12:25:37 +08:00
手瓜一十雪
fd26d34e19 fix 2024-11-27 12:20:30 +08:00
手瓜一十雪
e9fcdc7d2e feat: 简化代码 2024-11-27 11:35:51 +08:00
手瓜一十雪
0fe4911d01 fix: 优化类型 2024-11-27 11:29:03 +08:00
手瓜一十雪
d4fb09fa80 fix: 简化类型 2024-11-27 10:57:40 +08:00
手瓜一十雪
e6d5a37236 fix: menuRef 2024-11-27 10:53:50 +08:00
手瓜一十雪
79fd10ac10 Merge branch 'main' into pr/567 2024-11-26 20:06:18 +08:00
手瓜一十雪
a2e6095e44 chore: 注释不必要的代码 2024-11-26 19:44:37 +08:00
手瓜一十雪
64530471a0 fix: GroupChange 2024-11-26 19:42:35 +08:00
stapxs
e31e831309 feat: 优化网络配置卡片样式 2024-11-26 17:12:55 +08:00
手瓜一十雪
cf6871df9b Merge branch 'main' into pr/565 2024-11-26 12:56:57 +08:00
手瓜一十雪
482e7f1c75 Merge pull request #566 from NapNeko/refactor-group-event
refactor: parseGroupEvent
2024-11-26 12:51:48 +08:00
手瓜一十雪
aab501e31e Merge branch 'refactor-group-event' into pr/565 2024-11-26 12:40:19 +08:00
手瓜一十雪
ceec9e5e1b refactor: self report 2024-11-26 11:26:34 +08:00
手瓜一十雪
aadebb3cc5 refactor: event emit 2024-11-26 11:10:59 +08:00
手瓜一十雪
657ddd3341 refactor: emitRecallMsg 2024-11-26 11:08:12 +08:00
手瓜一十雪
62127b6d48 refactor: onMsgRecall 2024-11-25 22:36:07 +08:00
bietiaop
f5f405796f fix:vite配置别名framwork缺少@webapi 2024-11-25 22:29:18 +08:00
bietiaop
39873947a3 chore:移除多余依赖 2024-11-25 22:11:57 +08:00
手瓜一十雪
a1079dd948 fix 2024-11-25 21:58:35 +08:00
bietiaop
4eeabcc9e0 refactor:优化WebUI后端代码格式(无新功能添加) 2024-11-25 21:56:57 +08:00
手瓜一十雪
c3568d07e8 fix: #563 2024-11-25 21:56:34 +08:00
手瓜一十雪
1adb4a4ba8 refactor: parsePrivateMsgEvent 2024-11-25 21:53:35 +08:00
手瓜一十雪
6d0020533c chore: 可读性提高 优化代码 2024-11-25 21:44:22 +08:00
手瓜一十雪
4e6af0a655 feat: 性能优化 2024-11-25 21:28:04 +08:00
手瓜一十雪
00f726b515 fix: event parse 2024-11-25 21:20:32 +08:00
手瓜一十雪
035aa32305 fix: parseGroupUploadFileEvene 2024-11-25 21:04:33 +08:00
手瓜一十雪
62ea4b98e1 refactor: parseGroupEvent 2024-11-25 20:39:44 +08:00
pk5ls20
4be821137d feat: eslint 2024-11-25 20:02:50 +08:00
pk5ls20
7fba9960bf Merge branch 'main' into webui-new 2024-11-25 19:57:41 +08:00
手瓜一十雪
876bfbd3cb feat: tipgroup type 2024-11-25 19:32:30 +08:00
手瓜一十雪
edde2c210b feat: type 2024-11-25 19:24:51 +08:00
手瓜一十雪
f956d96d94 feat: 上报文件picType 2024-11-25 18:27:36 +08:00
手瓜一十雪
c2296fd900 fix 2024-11-25 18:24:13 +08:00
Mlikiowa
0feed5b640 release: v4.1.21 2024-11-25 07:48:50 +00:00
手瓜一十雪
93904dcb1b fix: 删除的移除 2024-11-25 15:45:07 +08:00
手瓜一十雪
86cbdf793a Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-25 15:36:01 +08:00
手瓜一十雪
56b1b9b598 chore: 移除开发debug 2024-11-25 15:35:48 +08:00
Mlikiowa
f7ec3ae131 release: v4.1.20 2024-11-25 07:16:41 +00:00
手瓜一十雪
01d11d6213 refactor: 热重载 2024-11-25 15:16:12 +08:00
Mlikiowa
74a316e758 release: v4.1.19 2024-11-25 05:27:11 +00:00
纸凤孤凰
3fbed815a5 修改webui 2024-11-25 02:17:48 +08:00
183 changed files with 3658 additions and 3558 deletions

2
.env.universal Normal file
View File

@@ -0,0 +1,2 @@
VITE_BUILD_TYPE = Production
VITE_BUILD_PLATFORM = Universal

View File

@@ -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

View File

@@ -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"

View File

@@ -34,7 +34,14 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 回家旅途 ## 回家旅途
[QQ Group](https://qm.qq.com/q/NWP25OeV0c) [QQ Group](https://qm.qq.com/q/I6LU87a0Yq)
## 性能设计/协议标准
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) 对本项目的大力支持 参考部分代码 已获授权

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{ {
"name": "qq-chat", "name": "qq-chat",
"version": "9.9.16-29456", "version": "9.9.16-29927",
"verHash": "dd395162", "verHash": "3e273e30",
"linuxVersion": "3.2.13-29456", "linuxVersion": "3.2.13-29927",
"linuxVerHash": "e379390a", "linuxVerHash": "833d113c",
"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": "29927",
"isPureShell": true, "isPureShell": true,
"isByteCodeShell": true, "isByteCodeShell": true,
"platform": "win32", "platform": "win32",

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "4.1.18", "version": "4.2.23",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {

View File

@@ -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>

View File

@@ -14,7 +14,7 @@
"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": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -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 scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -74,7 +74,7 @@ export class QQLoginManager {
} }
return false; return false;
} }
public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string, isLogin: string } | undefined> { public async checkQQLoginStatusWithQrcode(): Promise<{ qrcodeurl: string; isLogin: string } | undefined> {
try { try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, { const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST', method: 'POST',

View File

@@ -1,16 +1,18 @@
<template> <template>
<div class="dashboard-container"> <t-layout class="dashboard-container">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" /> <div ref="menuRef">
<div class="content"> <SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<router-view />
</div> </div>
</div> <t-layout>
<router-view />
</t-layout>
</t-layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { onMounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue'; import SidebarMenu from './webui/Nav.vue';
import emitter from '@/ts/event-bus';
interface MenuItem { interface MenuItem {
value: string; value: string;
icon: string; icon: string;
@@ -25,6 +27,14 @@ 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' },
]); ]);
const menuRef = ref<HTMLDivElement | null>(null);
emitter.on('sendMenu', (event) => {
emitter.emit('sendWidth', menuRef.value?.offsetWidth);
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0');
});
</script> </script>
<style scoped> <style scoped>
@@ -32,6 +42,7 @@ const menuItems = ref<MenuItem[]>([
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100vh; height: 100vh;
width: 100%;
} }
.sidebar-menu { .sidebar-menu {
@@ -39,14 +50,6 @@ const menuItems = ref<MenuItem[]>([
z-index: 2; z-index: 2;
} }
.content {
flex: 1;
/* padding: 20px; */
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.content { .content {
padding: 10px; padding: 10px;

View File

@@ -1,22 +1,43 @@
<template> <template>
<div class="login-container"> <t-card class="layout">
<h2 class="sotheby-font">QQ Login</h2> <div class="login-container">
<div class="login-methods"> <h2 class="sotheby-font">QQ Login</h2>
<t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }" <div class="login-methods">
@click="loginMethod = 'quick'">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="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
</t-tooltip>
<t-tooltip content="二维码登录">
<t-button
id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</t-tooltip>
</div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select
id="quick-login-select"
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div> </div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form"> <t-footer class="footer">Power By NapCat.WebUi</t-footer>
<t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account" </t-card>
@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">
@@ -37,6 +58,9 @@ 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 {
@@ -64,10 +88,11 @@ const HeartBeat = async (): Promise<void> => {
if (heartBeatTimer) { if (heartBeatTimer) {
clearInterval(heartBeatTimer); clearInterval(heartBeatTimer);
} }
//判断是否已经调转 // //判断是否已经调转
if (router.currentRoute.value.path !== '/dashboard/basic-info') { // if (router.currentRoute.value.path !== '/dashboard/basic-info') {
return; // return;
} // }
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' }); await router.push({ path: '/dashboard/basic-info' });
} else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) { } else if (isLogined?.qrcodeurl && qrcodeUrl !== isLogined.qrcodeurl) {
qrcodeUrl = isLogined.qrcodeurl; qrcodeUrl = isLogined.qrcodeurl;
@@ -88,14 +113,16 @@ onMounted(() => {
</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) {
@@ -154,7 +181,5 @@ onMounted(() => {
bottom: 20px; bottom: 20px;
left: 0; left: 0;
right: 0; right: 0;
width: 100%;
background-color: white;
} }
</style> </style>

View File

@@ -1,20 +1,22 @@
<template> <template>
<div class="login-container"> <t-card class="layout">
<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>

View File

@@ -1,16 +1,31 @@
<template> <template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu"> <t-menu theme="light" default-value="2-1" :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, defineProps, onMounted, watch } from 'vue';
import emitter from '@/ts/event-bus';
type MenuItem = { type MenuItem = {
value: string; value: string;
@@ -31,15 +47,39 @@ type MenuItem = {
defineProps<{ defineProps<{
menuItems: MenuItem[]; menuItems: MenuItem[];
}>(); }>();
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) => {
setTimeout(() => {
emitter.emit('sendMenu', collapsed.value);
}, 300);
});
onMounted(() => {
const mediaQuery = window.matchMedia('(max-width: 800px)');
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 +97,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 {

View File

@@ -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,18 @@ 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,
} 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 +63,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 +75,13 @@ 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.mount('#app'); app.mount('#app');

View File

@@ -1,122 +1,300 @@
<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 <t-divider align="right">
v-model="activeTab" <t-button @click="addConfig()">
:addable="true" <template #icon><add-icon /></template>
theme="card" 添加配置</t-button>
@add="showAddTabDialog" </t-divider>
@remove="removeTab" </div>
class="full-tabs" <div v-if="loadPage" ref="setting" class="setting">
> <t-tabs ref="tabsRef" :style="{ width: tabsWidth + 'px' }" default-value="all" @change="selectType">
<t-tab-panel <t-tab-panel value="all" label="全部"></t-tab-panel>
v-for="(config, idx) in clientPanelData" <t-tab-panel value="httpServers" label="HTTP 服务器"></t-tab-panel>
:key="idx" <t-tab-panel value="httpClients" label="HTTP 客户端"></t-tab-panel>
:label="config.name" <t-tab-panel value="websocketServers" label="WebSocket 服务器"></t-tab-panel>
:removable="true" <t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
:value="idx" </t-tabs>
class="full-tab-panel" </div>
> <div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" /> <div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
<div class="button-container"> <div v-for="(item, index) in cardConfig" :key="index">
<t-button @click="saveConfig" style="width: 100px; height: 40px">保存</t-button> <t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }"
:header-bordered="true" class="setting-card">
<template #actions>
<t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm theme="danger" 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"></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"></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="tag-item" :theme="item.debug ? 'success' : 'danger'">
{{ item.debug ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')"
label="Websocket 功能">
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'">
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行">
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="上报自身消息">
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'">
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag>
</t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')"
label="强制推送事件">
<t-tag class="tag-item"
:theme="item.enableForcePushEvent ? 'success' : 'danger'">
{{ 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 v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true"
@close="isDialogVisible = false" :show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul">
@confirm="addTab" <div slot="body" class="dialog-body" >
> <t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
<t-form ref="form" :model="newTab"> <t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name"> 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 style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
<t-select v-model="newTab.type"> 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 { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next';
import { MessagePlugin } from 'tdesign-vue-next'; import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue';
import emitter from '@/ts/event-bus';
import { import {
httpServerDefaultConfigs, mergeNetworkDefaultConfig,
httpClientDefaultConfigs, mergeOneBotConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
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 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) => {
console.log(key);
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 +315,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 +343,198 @@ 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 input = document.createElement('input');
const userConfig = await getOB11Config(); input.value = text;
if (!userConfig) { document.body.appendChild(input);
await MessagePlugin.error('无法获取配置!'); input.select();
return; await navigator.clipboard.writeText(text);
} document.body.removeChild(input);
userConfig.network = config; MessagePlugin.success('复制成功');
const success = await setOB11Config(userConfig); };
if (success) {
await MessagePlugin.success('配置保存成功'); const handleResize = () => {
// 得根据卡片宽度改,懒得改了;先不管了
// if(window.innerWidth < 540) {
// infoOneCol.value= true
// } else {
// infoOneCol.value= false
// }
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 === 'number' && !isNaN(width)) {
newTab.value = { name: '', type: 'httpServers' }; menuWidth.value = width;
isDialogVisible.value = true; handleResize();
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.value.some((panel) => panel.name === name)) {
await MessagePlugin.error('选项卡名称已存在');
return;
} }
const defaultConfig = structuredClone(defaultConfigMap[type]); });
defaultConfig.name = name;
clientPanelData.value.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.value.length - 1;
await MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.value.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig();
const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth);
setTimeout(() => {
handleResize();
}, 300);
}
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
}); });
</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;
}
.browse-icon{
flex: 2;
}
:global(.t-dialog__ctx .t-dialog--defaul) {
margin: 0 20px;
}
@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: 60vh;
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;
}
.card-address .t-card__body {
display: flex;
flex-direction: row;
align-items: center;
}
.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>

View File

@@ -1,25 +1,27 @@
<template> <template>
<div> <div class="title">
<t-divider content="其余配置" align="left" /> <t-divider content="其余配置" align="left" />
</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-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item"> </t-form-item>
<t-switch v-model="otherConfig.parseMultMsg" /> <t-form-item label="启用上报解析合并消息" name="parseMultMsg" class="form-item">
</t-form-item> <t-switch v-model="otherConfig.parseMultMsg" />
</t-form> </t-form-item>
<div class="button-container"> </t-form>
<t-button @click="saveConfig">保存</t-button> <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">
@@ -34,6 +36,7 @@ const otherConfig = ref<Partial<OneBotConfig>>({
parseMultMsg: true 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) {
@@ -86,55 +89,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>

View File

@@ -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>

View File

@@ -1,28 +1,25 @@
<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-checkbox 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-checkbox 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-checkbox 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>
@@ -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>

View File

@@ -1,34 +1,31 @@
<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-checkbox 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-checkbox 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-checkbox 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-checkbox 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>
@@ -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>

View File

@@ -1,31 +1,28 @@
<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-checkbox 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-checkbox 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-checkbox 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>
@@ -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>

View File

@@ -1,37 +1,34 @@
<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-checkbox 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-checkbox 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-checkbox 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-checkbox 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>
@@ -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>

View File

@@ -0,0 +1,3 @@
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

View File

@@ -2,11 +2,13 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.1.18", "version": "4.2.23",
"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",
@@ -41,20 +43,20 @@
"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",
"@sinclair/typebox": "^0.34.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"
} }
} }

View File

@@ -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 {};
} }
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -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,7 +13,6 @@ type Uri2LocalRes = {
success: boolean, success: boolean,
errMsg: string, errMsg: string,
fileName: string, fileName: string,
ext: string,
path: string path: string
} }
@@ -73,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) => {
// 创建一个流式读取器 // 创建一个流式读取器
@@ -160,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,
@@ -182,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 };
@@ -191,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:')) {
@@ -220,60 +175,32 @@ 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): 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 filename = randomUUID();
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' }; const filePath = path.join(dir, filename);
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
const fileExt = path.extname(HandledUri);
let filename = path.basename(HandledUri, fileExt);
filename += fileExt;
//复制文件到临时文件并保持后缀
const filenameTemp = tempName + fileExt;
const filePath = path.join(dir, filenameTemp);
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//接下来都要有文件名
if (UriType == FileUriType.Remote) { switch (UriType) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname)); case FileUriType.Local:
if (pathInfo.name) { const fileExt = path.extname(HandledUri);
const pathlen = 200 - dir.length - pathInfo.name.length; const localFileName = path.basename(HandledUri, fileExt) + fileExt;
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断 const tempFilePath = path.join(dir, filename + fileExt);
if (pathInfo.ext) { fs.copyFileSync(HandledUri, tempFilePath);
filename += pathInfo.ext; return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
}
} case FileUriType.Remote:
filename = filename.replace(/[/\\:*?"<>|]/g, '_'); const buffer = await httpDownload(HandledUri);
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10); fs.writeFileSync(filePath, buffer, { flag: 'wx' });
const filePath = path.join(dir, tempName + fileExt); return { success: true, errMsg: '', fileName: filename, path: filePath };
const buffer = await httpDownload(HandledUri);
//没有文件就创建 case FileUriType.Base64:
fs.writeFileSync(filePath, buffer, { flag: 'wx' }); const base64 = HandledUri.replace(/^base64:\/\//, '');
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; const base64Buffer = Buffer.from(base64, 'base64');
fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, path: filePath };
default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
} }
//解析Base64 }
if (UriType == FileUriType.Base64) {
const base64 = HandledUri.replace(/^base64:\/\//, '');
const buffer = Buffer.from(base64, 'base64');
let filePath = path.join(dir, filename);
let fileExt = '';
fs.writeFileSync(filePath, buffer);
const { success, ext, path: fileTypePath } = await checkFileV2(filePath);
if (success) {
filePath = fileTypePath;
fileExt = ext;
filename = filename + '.' + ext;
}
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}

View File

@@ -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})`
}]`;
}

View File

@@ -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'),
]);
}
} }

View File

@@ -1 +1 @@
export const napCatVersion = '4.1.18'; export const napCatVersion = '4.2.23';

View File

@@ -6,7 +6,6 @@ import {
Peer, Peer,
PicElement, PicElement,
PicSubType, PicSubType,
PicType,
RawMessage, RawMessage,
SendFileElement, SendFileElement,
SendPicElement, SendPicElement,
@@ -17,7 +16,7 @@ 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 '@/core/helper/rkey'; import { RkeyManager } from '@/core/helper/rkey';
@@ -26,7 +25,7 @@ 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'; import { getFileTypeForSendType } from '../helper/msg';
export class NTQQFileApi { export class NTQQFileApi {
@@ -62,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; let 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) {
@@ -91,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,
@@ -113,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');
@@ -141,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,
@@ -152,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);
@@ -183,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);
@@ -193,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({
@@ -230,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('语音转换失败, 请检查语音文件是否正常');
@@ -239,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 {
@@ -454,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) {
@@ -464,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);
} }
} }

View File

@@ -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,
}); });
} }

View File

@@ -1,6 +1,5 @@
import { import {
GeneralCallResult, GeneralCallResult,
Group,
GroupMember, GroupMember,
NTGroupMemberRole, NTGroupMemberRole,
NTGroupRequestOperateTypes, NTGroupRequestOperateTypes,
@@ -8,6 +7,7 @@ import {
KickMemberV2Req, KickMemberV2Req,
MemberExtSourceType, MemberExtSourceType,
NapCatCore, NapCatCore,
GroupNotify,
} 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 +16,22 @@ 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() { async initApi() {
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger)); this.initCache().then().catch(e => this.context.logger.logError(e));
}
async initCache() {
this.groups = await this.getGroups();
for (const group of this.groups) {
this.groupCache.set(group.groupCode, group);
}
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`);
// process.pid 调试点
} }
async getCoreAndBaseInfo(uids: string[]) { async initCache() {
return await this.core.eventWrapper.callNoListenerEvent( for (const group of await this.getGroups(true)) {
'NodeIKernelProfileService/getCoreAndBaseInfo', this.refreshGroupMemberCache(group.groupCode).then().catch();
'nodeStore', }
uids,
);
} }
async fetchGroupEssenceList(groupCode: string) { async fetchGroupEssenceList(groupCode: string) {
@@ -54,20 +42,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 +66,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 +108,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 +153,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 +183,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 +196,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 +250,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 +346,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) {

View File

@@ -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);
}); });
} }

View File

@@ -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,13 +13,15 @@ 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',
async getStatusByUid(uid: string) { 'nodeStore',
return this.context.session.getProfileService().getStatus(uid); uids,
);
} }
// 默认获取自己的 type = 2 获取别人 type = 1 // 默认获取自己的 type = 2 获取别人 type = 1
async getProfileLike(uid: string, start: number, count: number, type: number = 2) { async getProfileLike(uid: string, start: number, count: number, type: number = 2) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({ return this.context.session.getProfileLikeService().getBuddyProfileLike({
@@ -161,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) {

View File

@@ -98,5 +98,17 @@
"6.9.61-29927": { "6.9.61-29927": {
"appid": 537255836, "appid": 537255836,
"qua": "V1_MAC_NQ_6.9.61_29927_GW_B" "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"
} }
} }

View File

@@ -102,5 +102,25 @@
"6.9.61-29927-arm64": { "6.9.61-29927-arm64": {
"send": "4038740", "send": "4038740",
"recv": "403AF58" "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"
} }
} }

View File

@@ -1,14 +1,14 @@
import * as fileType from 'file-type'; import { fileTypeFromFile } from 'file-type';
import { PicType } from '../types'; import { PicType } from '../types';
export async function getFileTypeForSendType(picPath: string): Promise<PicType> { export async function getFileTypeForSendType(picPath: string): Promise<PicType> {
const fileTypeResult = (await fileType.fileTypeFromFile(picPath))?.ext ?? 'jpg'; const fileTypeResult = (await fileTypeFromFile(picPath))?.ext ?? 'jpg';
const picTypeMap: { [key: string]: PicType } = { const picTypeMap: { [key: string]: PicType } = {
'webp': PicType.NEWPIC_WEBP, //'webp': PicType.NEWPIC_WEBP,
'gif': PicType.NEWPIC_GIF, 'gif': PicType.NEWPIC_GIF,
'png': PicType.NEWPIC_APNG, // 'png': PicType.NEWPIC_APNG,
'jpg': PicType.NEWPIC_JPEG, // 'jpg': PicType.NEWPIC_JPEG,
'jpeg': PicType.NEWPIC_JPEG, // 'jpeg': PicType.NEWPIC_JPEG,
'bmp': PicType.NEWPIC_BMP, // 'bmp': PicType.NEWPIC_BMP,
}; };
return picTypeMap[fileTypeResult] ?? PicType.NEWPIC_JPEG; return picTypeMap[fileTypeResult] ?? PicType.NEWPIC_JPEG;
} }

View File

@@ -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}`);//外抛

View File

@@ -24,10 +24,10 @@ 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';
export * from './wrapper'; export * from './wrapper';
@@ -127,7 +127,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,
@@ -154,7 +154,7 @@ export class NapCatCore {
const msgListener = new NodeIKernelMsgListener(); const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => { msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知 // 下线通知
this.context.logger.logError.bind(this.context.logger)('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc); this.context.logger.logError('[KickedOffLine] [' + Info.tipsTitle + '] ' + Info.tipsDesc);
this.selfInfo.online = false; this.selfInfo.online = false;
}; };
msgListener.onRecvMsg = (msgs) => { msgListener.onRecvMsg = (msgs) => {
@@ -163,7 +163,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 +184,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;
} }
} }

View File

@@ -255,7 +255,7 @@ export class NodeIKernelMsgListener {
} }
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any { onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
} }

View 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),
}

View File

@@ -54,6 +54,16 @@ export const PushMsg = {
generalFlag: ProtoField(9, ScalarType.INT32, true), generalFlag: ProtoField(9, ScalarType.INT32, 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),
operatorUid: ProtoField(5, ScalarType.STRING, true),
increaseType: ProtoField(6, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.BYTES, true),
};
export const PushMsgBody = { export const PushMsgBody = {
responseHead: ProtoField(1, () => ResponseHead), responseHead: ProtoField(1, () => ResponseHead),
contentHead: ProtoField(2, () => ContentHead), contentHead: ProtoField(2, () => ContentHead),

View File

@@ -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,

View File

@@ -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>>;

View File

@@ -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,17 +41,18 @@ 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: {

View File

@@ -383,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;
@@ -399,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;

View File

@@ -1,6 +1,16 @@
//LiteLoader需要提供部分IPC接口以便于其他插件调用 //LiteLoader需要提供部分IPC接口以便于其他插件调用
const { ipcMain } = require('electron'); const { ipcMain } = 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}`;
});

View File

@@ -58,7 +58,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();
} }

View File

@@ -1,10 +1,14 @@
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);
},
getWebUiUrlReact: async () => {
return ipcRenderer.invoke('napcat_get_reactweb');
}
}; };
// 在window对象下导出只读对象 // 在window对象下导出只读对象
contextBridge.exposeInMainWorld('napcat', napcat); contextBridge.exposeInMainWorld('napcat', napcat);

View File

@@ -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.open(webui, '_blank');
}); });
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);
}
});
};

View File

@@ -42,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[];

View File

@@ -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,
); );
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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
);
} }
} }

View File

@@ -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());
} }
} }

View File

@@ -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) => ({

View File

@@ -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);
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -1,16 +1,11 @@
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 = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
});
const SchemaData = { type Payload = Static<typeof SchemaData>;
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupInfoEx extends OneBotAction<Payload, any> { export class GetGroupInfoEx extends OneBotAction<Payload, any> {
actionName = ActionName.GetGroupInfoEx; actionName = ActionName.GetGroupInfoEx;

View File

@@ -1,47 +1,37 @@
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 { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/utils/helper/miniAppHelper"; import { MiniAppInfo, MiniAppInfoHelper } from "@/core/packet/utils/helper/miniAppHelper";
import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from "@/core/packet/entities/miniApp"; import { MiniAppData, MiniAppRawData, MiniAppReqCustomParams, MiniAppReqParams } from "@/core/packet/entities/miniApp";
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Union([
type: 'object', Type.Object({
properties: { type: Type.Union([Type.Literal('bili'), Type.Literal('weibo')]),
type: { title: Type.String(),
type: 'string', desc: Type.String(),
enum: ['bili', 'weibo'] picUrl: Type.String(),
}, jumpUrl: Type.String(),
title: { type: 'string' }, rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
desc: { type: 'string' }, }),
picUrl: { type: 'string' }, Type.Object({
jumpUrl: { type: 'string' }, title: Type.String(),
iconUrl: { type: 'string' }, desc: Type.String(),
sdkId: { type: 'string' }, picUrl: Type.String(),
appId: { type: 'string' }, jumpUrl: Type.String(),
scene: { type: ['number', 'string'] }, iconUrl: Type.String(),
templateType: { type: ['number', 'string'] }, appId: Type.String(),
businessType: { type: ['number', 'string'] }, scene: Type.Union([Type.Number(), Type.String()]),
verType: { type: ['number', 'string'] }, templateType: Type.Union([Type.Number(), Type.String()]),
shareType: { type: ['number', 'string'] }, businessType: Type.Union([Type.Number(), Type.String()]),
versionId: { type: 'string' }, verType: Type.Union([Type.Number(), Type.String()]),
withShareTicket: { type: ['number', 'string'] }, shareType: Type.Union([Type.Number(), Type.String()]),
rawArkData: { type: ['boolean', 'string'] } versionId: Type.String(),
}, sdkId: Type.String(),
oneOf: [ withShareTicket: Type.Union([Type.Number(), Type.String()]),
{ rawArkData: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
required: ['type', 'title', 'desc', 'picUrl', 'jumpUrl'] })
}, ]);
{ type Payload = Static<typeof SchemaData>;
required: [
'title', 'desc', 'picUrl', 'jumpUrl',
'iconUrl', 'appId', 'scene', 'templateType', 'businessType',
'verType', 'shareType', 'versionId', 'withShareTicket'
]
}
]
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GetMiniAppArk extends GetPacketStatusDepends<Payload, { export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
data: MiniAppData | MiniAppRawData data: MiniAppData | MiniAppRawData
@@ -57,7 +47,7 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
picUrl: payload.picUrl, picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl jumpUrl: payload.jumpUrl
} as MiniAppReqCustomParams; } as MiniAppReqCustomParams;
if (payload.type) { if ('type' in payload) {
reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template); reqParam = MiniAppInfoHelper.generateReq(customParams, MiniAppInfo.get(payload.type)!.template);
} else { } else {
const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload; const { appId, scene, iconUrl, templateType, businessType, verType, shareType, versionId, withShareTicket } = payload;

View File

@@ -1,34 +1,28 @@
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', user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
properties: { start: Type.Union([Type.Number(), Type.String()], { default: 0 }),
user_id: { type: ['number', 'string'] }, count: Type.Union([Type.Number(), Type.String()], { default: 10 }),
start: { type: ['number', 'string'] }, type: Type.Union([Type.Number(), Type.String()], { default: 2 }),
count: { type: ['number', 'string'] }, });
type: { type: ['number', 'string'] },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class GetProfileLike extends OneBotAction<Payload, any> { export class GetProfileLike extends OneBotAction<Payload, any> {
actionName = ActionName.GetProfileLike; actionName = ActionName.GetProfileLike;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
const start = payload.start ? Number(payload.start) : 0;
const count = payload.count ? Number(payload.count) : 10;
const type = payload.count ? Number(payload.count) : 2;
const user_uid = const user_uid =
this.core.selfInfo.uin === payload.user_id || !payload.user_id ? this.core.selfInfo.uin === payload.user_id || !payload.user_id ?
this.core.selfInfo.uid : this.core.selfInfo.uid :
await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const ret = await this.core.apis.UserApi.getProfileLike(user_uid ?? this.core.selfInfo.uid, start, count, type); const ret = await this.core.apis.UserApi.getProfileLike(user_uid ?? this.core.selfInfo.uid, +payload.start, +payload.count, +payload.type);
const listdata = ret.info.userLikeInfos[0].voteInfo.userInfos; const listdata = ret.info.userLikeInfos[0].voteInfo.userInfos;
for (const item of listdata) { for (const item of listdata) {
item.uin = parseInt((await this.core.apis.UserApi.getUinByUidV2(item.uid)) || ''); item.uin = +((await this.core.apis.UserApi.getUinByUidV2(item.uid)) ?? '');
} }
return ret.info.userLikeInfos[0].voteInfo; return ret.info.userLikeInfos[0].voteInfo;
} }

View File

@@ -1,8 +1,7 @@
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
export class GetRkey extends GetPacketStatusDepends<void, Array<any>> {
export class GetRkey extends GetPacketStatusDepends<null, Array<any>> {
actionName = ActionName.GetRkey; actionName = ActionName.GetRkey;
async _handle() { async _handle() {

View File

@@ -4,7 +4,7 @@ import { ActionName } from '@/onebot/action/router';
export class GetRobotUinRange extends OneBotAction<void, Array<any>> { export class GetRobotUinRange extends OneBotAction<void, Array<any>> {
actionName = ActionName.GetRobotUinRange; actionName = ActionName.GetRobotUinRange;
async _handle(payload: void) { async _handle() {
return await this.core.apis.UserApi.getRobotUinRange(); return await this.core.apis.UserApi.getRobotUinRange();
} }
} }

View File

@@ -1,16 +1,12 @@
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";
// no_cache get时传字符串 import { Static, Type } from '@sinclair/typebox';
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
});
type Payload = Static<typeof SchemaData>;
export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> { export class GetUserStatus extends GetPacketStatusDepends<Payload, { status: number; ext_status: number; } | undefined> {
actionName = ActionName.GetUserStatus; actionName = ActionName.GetUserStatus;

View File

@@ -1,25 +1,21 @@
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 { checkFileExist, uriToLocalFile } from '@/common/file';
import { checkFileExist, uri2local } from '@/common/file';
import fs from 'fs'; import fs from 'fs';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', image: Type.String(),
properties: { });
image: { type: 'string' },
},
required: ['image'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class OCRImage extends OneBotAction<Payload, any> { export class OCRImage extends OneBotAction<Payload, any> {
actionName = ActionName.OCRImage; actionName = ActionName.OCRImage;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.image)); const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`); throw new Error(`OCR ${payload.image}失败,image字段可能格式不正确`);
} }
@@ -29,12 +25,12 @@ export class OCRImage extends OneBotAction<Payload, any> {
fs.unlink(path, () => { }); fs.unlink(path, () => { });
if (!ret) { if (!ret) {
throw new Error(`OCR ${payload.file}失败`); throw new Error(`OCR ${payload.image}失败`);
} }
return ret.result; return ret.result;
} }
fs.unlink(path, () => { }); fs.unlink(path, () => { });
throw new Error(`OCR ${payload.file}失败,文件可能不存在`); throw new Error(`OCR ${payload.image}失败,文件可能不存在`);
} }
} }

View File

@@ -1,16 +1,12 @@
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus'; import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { });
group_id: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class SetGroupSign extends GetPacketStatusDepends<Payload, any> { export class SetGroupSign extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetGroupSign; actionName = ActionName.SetGroupSign;

View File

@@ -1,18 +1,14 @@
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 { ChatType } from '@/core'; import { ChatType } from '@/core';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', user_id: Type.Union([Type.Number(), Type.String()]),
properties: { event_type: Type.Number(),
event_type: { type: 'number' }, });
user_id: { type: ['number', 'string'] },
},
required: ['event_type', 'user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class SetInputStatus extends OneBotAction<Payload, any> { export class SetInputStatus extends OneBotAction<Payload, any> {
actionName = ActionName.SetInputStatus; actionName = ActionName.SetInputStatus;

View File

@@ -1,16 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', longNick: Type.String(),
properties: { });
longNick: { type: 'string' },
},
required: ['longNick'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class SetLongNick extends OneBotAction<Payload, any> { export class SetLongNick extends OneBotAction<Payload, any> {
actionName = ActionName.SetLongNick; actionName = ActionName.SetLongNick;

View File

@@ -1,19 +1,14 @@
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 { Static, Type } from '@sinclair/typebox';
// 设置在线状态
const SchemaData = { const SchemaData = Type.Object({
type: 'object', status: Type.Union([Type.Number(), Type.String()]),
properties: { ext_status: Type.Union([Type.Number(), Type.String()]),
status: { type: ['number', 'string'] }, battery_status: Type.Union([Type.Number(), Type.String()]),
ext_status: { type: ['number', 'string'] }, });
battery_status: { type: ['number', 'string'] },
},
required: ['status', 'ext_status', 'battery_status'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class SetOnlineStatus extends OneBotAction<Payload, null> { export class SetOnlineStatus extends OneBotAction<Payload, null> {
actionName = ActionName.SetOnlineStatus; actionName = ActionName.SetOnlineStatus;
@@ -21,9 +16,9 @@ export class SetOnlineStatus extends OneBotAction<Payload, null> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const ret = await this.core.apis.UserApi.setSelfOnlineStatus( const ret = await this.core.apis.UserApi.setSelfOnlineStatus(
parseInt(payload.status.toString()), +payload.status,
parseInt(payload.ext_status.toString()), +payload.ext_status,
parseInt(payload.battery_status.toString()), +payload.battery_status,
); );
if (ret.result !== 0) { if (ret.result !== 0) {
throw new Error('设置在线状态失败'); throw new Error('设置在线状态失败');

View File

@@ -1,39 +1,27 @@
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName, BaseCheckResult } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import * as fs from 'node:fs'; import fs from 'node:fs/promises';
import { checkFileExist, uri2local } from '@/common/file'; import { checkFileExist, uriToLocalFile } from '@/common/file';
import { Static, Type } from '@sinclair/typebox';
interface Payload { const SchemaData = Type.Object({
file: string; file: Type.String(),
} });
type Payload = Static<typeof SchemaData>;
export default class SetAvatar extends OneBotAction<Payload, null> { export default class SetAvatar extends OneBotAction<Payload, null> {
actionName = ActionName.SetQQAvatar; actionName = ActionName.SetQQAvatar;
payloadSchema = SchemaData;
// 用不着复杂检测
protected async check(payload: Payload): Promise<BaseCheckResult> {
if (!payload.file || typeof payload.file != 'string') {
return {
valid: false,
message: 'file字段不能为空或者类型错误',
};
}
return {
valid: true,
};
}
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const { path, success } = (await uri2local(this.core.NapCatTempPath, payload.file)); const { path, success } = (await uriToLocalFile(this.core.NapCatTempPath, payload.file));
if (!success) { if (!success) {
throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`); throw new Error(`头像${payload.file}设置失败,file字段可能格式不正确`);
} }
if (path) { if (path) {
await checkFileExist(path, 5000);// 避免崩溃 await checkFileExist(path, 5000);// 避免崩溃
const ret = await this.core.apis.UserApi.setQQAvatar(path); const ret = await this.core.apis.UserApi.setQQAvatar(path);
fs.unlink(path, () => { fs.unlink(path).catch(() => { });
});
if (!ret) { if (!ret) {
throw new Error(`头像${payload.file}设置失败,api无返回`); throw new Error(`头像${payload.file}设置失败,api无返回`);
} }
@@ -44,8 +32,7 @@ export default class SetAvatar extends OneBotAction<Payload, null> {
throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`); throw new Error(`头像${payload.file}设置失败,未知的错误,${ret.result}:${ret.errMsg}`);
} }
} else { } else {
fs.unlink(path, () => { }); fs.unlink(path).catch(() => { });
throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`); throw new Error(`头像${payload.file}设置失败,无法获取头像,文件可能不存在`);
} }
return null; return null;

View File

@@ -1,17 +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";
const SchemaData = { import { Static, Type } from '@sinclair/typebox';
type: 'object',
properties: {
group_id: { type: ['number', 'string'] },
user_id: { type: ['number', 'string'] },
special_title: { type: 'string' },
},
required: ['group_id', 'user_id', 'special_title'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
user_id: Type.Union([Type.Number(), Type.String()]),
special_title: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export class SetSpecialTittle extends GetPacketStatusDepends<Payload, any> { export class SetSpecialTittle extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetSpecialTittle; actionName = ActionName.SetSpecialTittle;

View File

@@ -1,18 +1,14 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', user_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
properties: { group_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
user_id: { type: 'string' }, phoneNumber: Type.String({ default: '' }),
group_id: { type: 'string' }, });
phoneNumber: { type: 'string' },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
type Payload = Static<typeof SchemaData>;
export class SharePeer extends OneBotAction<Payload, any> { export class SharePeer extends OneBotAction<Payload, any> {
actionName = ActionName.SharePeer; actionName = ActionName.SharePeer;
@@ -20,28 +16,24 @@ export class SharePeer extends OneBotAction<Payload, any> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
if (payload.group_id) { if (payload.group_id) {
return await this.core.apis.GroupApi.getGroupRecommendContactArkJson(payload.group_id); return await this.core.apis.GroupApi.getGroupRecommendContactArkJson(payload.group_id.toString());
} else if (payload.user_id) { } else if (payload.user_id) {
return await this.core.apis.UserApi.getBuddyRecommendContactArkJson(payload.user_id, payload.phoneNumber || ''); return await this.core.apis.UserApi.getBuddyRecommendContactArkJson(payload.user_id.toString(), payload.phoneNumber);
} }
} }
} }
const SchemaDataGroupEx = { const SchemaDataGroupEx = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { });
group_id: { type: 'string' },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type PayloadGroupEx = FromSchema<typeof SchemaDataGroupEx>; type PayloadGroupEx = Static<typeof SchemaDataGroupEx>;
export class ShareGroupEx extends OneBotAction<PayloadGroupEx, any> { export class ShareGroupEx extends OneBotAction<PayloadGroupEx, any> {
actionName = ActionName.ShareGroupEx; actionName = ActionName.ShareGroupEx;
payloadSchema = SchemaDataGroupEx; payloadSchema = SchemaDataGroupEx;
async _handle(payload: PayloadGroupEx) { async _handle(payload: PayloadGroupEx) {
return await this.core.apis.GroupApi.getArkJsonGroupShare(payload.group_id); return await this.core.apis.GroupApi.getArkJsonGroupShare(payload.group_id.toString());
} }
} }

View File

@@ -1,19 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', words: Type.Array(Type.String()),
properties: { });
words: {
type: 'array',
items: { type: 'string' },
},
},
required: ['words'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class TranslateEnWordToZn extends OneBotAction<Payload, Array<any> | null> { export class TranslateEnWordToZn extends OneBotAction<Payload, Array<any> | null> {
actionName = ActionName.TranslateEnWordToZn; actionName = ActionName.TranslateEnWordToZn;

View File

@@ -2,12 +2,8 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { FileNapCatOneBotUUID } from '@/common/helper'; import { FileNapCatOneBotUUID } from '@/common/helper';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types'; import { OB11MessageImage, OB11MessageVideo } from '@/onebot/types';
import { Static, Type } from '@sinclair/typebox';
// interface GetFilePayload {
// file: string; // 文件名或者fileUuid
// }
export interface GetFileResponse { export interface GetFileResponse {
file?: string; // path file?: string; // path
@@ -16,19 +12,14 @@ export interface GetFileResponse {
file_name?: string; file_name?: string;
base64?: string; base64?: string;
} }
const GetFileBase_PayloadSchema = {
type: 'object',
properties: {
file: { type: 'string' },
file_id: { type: 'string' }
},
oneOf: [
{ required: ['file'] },
{ required: ['file_id'] }
]
} as const satisfies JSONSchema;
export type GetFilePayload = FromSchema<typeof GetFileBase_PayloadSchema>; const GetFileBase_PayloadSchema = Type.Object({
file: Type.Optional(Type.String()),
file_id: Type.Optional(Type.String())
});
export type GetFilePayload = Static<typeof GetFileBase_PayloadSchema>;
export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> { export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
payloadSchema = GetFileBase_PayloadSchema; payloadSchema = GetFileBase_PayloadSchema;
@@ -50,12 +41,12 @@ export class GetFileBase extends OneBotAction<GetFilePayload, GetFileResponse> {
let url = ''; let url = '';
if (mixElement?.picElement && rawMessage) { if (mixElement?.picElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement) as OB11MessageImage | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
if (mixElement?.videoElement && rawMessage) { if (mixElement?.videoElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement) as OB11MessageVideo | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
const res: GetFileResponse = { const res: GetFileResponse = {

View File

@@ -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 { FileNapCatOneBotUUID } from "@/common/helper"; import { FileNapCatOneBotUUID } from "@/common/helper";
import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus"; import { GetPacketStatusDepends } from "@/onebot/action/packet/GetPacketStatus";
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { file_id: Type.String(),
group_id: { type: ['number', 'string'] }, });
file_id: { type: ['string'] },
},
required: ['group_id', 'file_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
interface GetGroupFileUrlResponse { interface GetGroupFileUrlResponse {
url?: string; url?: string;

View File

@@ -1,22 +1,18 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { folder_name: Type.String(),
group_id: { type: ['string', 'number'] }, });
folder_name: { type: 'string' },
},
required: ['group_id', 'folder_name'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class CreateGroupFileFolder extends OneBotAction<Payload, any> { export class CreateGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CreateGroupFileFolder; actionName = ActionName.GoCQHTTP_CreateGroupFileFolder;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.CreatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem; return (await this.core.apis.GroupApi.creatGroupFileFolder(payload.group_id.toString(), payload.folder_name)).resultWithGroupItem;
} }
} }

View File

@@ -1,18 +1,15 @@
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 { FileNapCatOneBotUUID } from '@/common/helper'; import { FileNapCatOneBotUUID } from '@/common/helper';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { file_id: Type.String(),
group_id: { type: ['string', 'number'] }, });
file_id: { type: 'string' },
},
required: ['group_id', 'file_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class DeleteGroupFile extends OneBotAction<Payload, any> { export class DeleteGroupFile extends OneBotAction<Payload, any> {
actionName = ActionName.GOCQHTTP_DeleteGroupFile; actionName = ActionName.GOCQHTTP_DeleteGroupFile;
@@ -20,6 +17,6 @@ export class DeleteGroupFile extends OneBotAction<Payload, any> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id); const data = FileNapCatOneBotUUID.decodeModelId(payload.file_id);
if (!data) throw new Error('Invalid file_id'); if (!data) throw new Error('Invalid file_id');
return await this.core.apis.GroupApi.DelGroupFile(payload.group_id.toString(), [data.fileId]); return await this.core.apis.GroupApi.delGroupFile(payload.group_id.toString(), [data.fileId]);
} }
} }

View File

@@ -1,24 +1,20 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { folder_id: Type.Optional(Type.String()),
group_id: { type: ['string', 'number'] }, folder: Type.Optional(Type.String()),
folder_id: { type: 'string' }, });
folder: { type: 'string' }
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class DeleteGroupFileFolder extends OneBotAction<Payload, any> { export class DeleteGroupFileFolder extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder; actionName = ActionName.GoCQHTTP_DeleteGroupFileFolder;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return (await this.core.apis.GroupApi.DelGroupFileFolder( return (await this.core.apis.GroupApi.delGroupFileFolder(
payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult; payload.group_id.toString(), payload.folder ?? payload.folder_id ?? '')).groupFileCommonResult;
} }
} }

View File

@@ -4,29 +4,20 @@ import fs from 'fs';
import { join as joinPath } from 'node:path'; import { join as joinPath } from 'node:path';
import { calculateFileMD5, httpDownload } from '@/common/file'; import { calculateFileMD5, httpDownload } from '@/common/file';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { Static, Type } from '@sinclair/typebox';
interface FileResponse { interface FileResponse {
file: string; file: string;
} }
const SchemaData = { const SchemaData = Type.Object({
type: 'object', url: Type.Optional(Type.String()),
properties: { base64: Type.Optional(Type.String()),
thread_count: { type: ['number', 'string'] }, name: Type.Optional(Type.String()),
url: { type: 'string' }, headers: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
base64: { type: 'string' }, });
name: { type: 'string' },
headers: {
type: ['string', 'array'],
items: {
type: 'string',
},
},
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResponse> { export default class GoCQHTTPDownloadFile extends OneBotAction<Payload, FileResponse> {
actionName = ActionName.GoCQHTTP_DownloadFile; actionName = ActionName.GoCQHTTP_DownloadFile;

View File

@@ -1,20 +1,15 @@
import { OneBotAction } from '@/onebot/action/OneBotAction'; import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode } from '@/onebot'; import { OB11Message, OB11MessageData, OB11MessageDataType, OB11MessageForward, OB11MessageNodePlain as OB11MessageNode } from '@/onebot';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
});
type Payload = Static<typeof SchemaData>;
const SchemaData = {
type: 'object',
properties: {
message_id: { type: 'string' },
id: { type: 'string' },
},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, any> { export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetForwardMsg; actionName = ActionName.GoCQHTTP_GetForwardMsg;
@@ -60,7 +55,7 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction<Payload, any> {
throw new Error('message_id is required'); throw new Error('message_id is required');
} }
const rootMsgId = MessageUnique.getShortIdByMsgId(msgId); const rootMsgId = MessageUnique.getShortIdByMsgId(msgId.toString());
const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId); const rootMsg = MessageUnique.getMsgIdAndPeerByShortId(rootMsgId ?? +msgId);
if (!rootMsg) { if (!rootMsg) {
throw new Error('msg not found'); throw new Error('msg not found');

View File

@@ -2,26 +2,22 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message } from '@/onebot'; import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { ChatType } from '@/core/types'; import { ChatType } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config'; import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
} }
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
});
const SchemaData = {
type: 'object',
properties: {
user_id: { type: ['number', 'string'] },
message_seq: { type: ['number', 'string'] },
count: { type: ['number', 'string'] },
reverseOrder: { type: ['boolean', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export default class GetFriendMsgHistory extends OneBotAction<Payload, Response> { export default class GetFriendMsgHistory extends OneBotAction<Payload, Response> {
actionName = ActionName.GetFriendMsgHistory; actionName = ActionName.GetFriendMsgHistory;
@@ -30,7 +26,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
async _handle(payload: Payload, adapter: string): Promise<Response> { async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数 //处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
const MsgCount = +(payload.count ?? 20);
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder; const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
if (!uid) throw new Error(`记录${payload.user_id}不存在`); if (!uid) throw new Error(`记录${payload.user_id}不存在`);
const friend = await this.core.apis.FriendApi.isBuddy(uid); const friend = await this.core.apis.FriendApi.isBuddy(uid);
@@ -38,7 +34,7 @@ export default class GetFriendMsgHistory extends OneBotAction<Payload, Response>
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0'); const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0'; const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ? const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, MsgCount)).msgList; (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`); if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息 //翻转消息
if (isReverseOrder) msgList.reverse(); if (isReverseOrder) msgList.reverse();

View File

@@ -1,15 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()])
properties: { });
group_id: { type: ['number', 'string'] }
}, type Payload = Static<typeof SchemaData>;
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPGetGroupAtAllRemain extends OneBotAction<Payload, any> { export class GoCQHTTPGetGroupAtAllRemain extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain; actionName = ActionName.GoCQHTTP_GetGroupAtAllRemain;

View File

@@ -1,16 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()])
properties: { });
group_id: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class GetGroupFileSystemInfo extends OneBotAction<Payload, { export class GetGroupFileSystemInfo extends OneBotAction<Payload, {
file_count: number, file_count: number,

View File

@@ -1,20 +1,17 @@
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 { OB11Construct } from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { folder_id: Type.Optional(Type.String()),
group_id: { type: ['string', 'number'] }, folder: Type.Optional(Type.String()),
folder_id: { type: 'string' }, file_count: Type.Union([Type.Number(), Type.String()], { default: 50 }),
folder: { type: 'string' }, });
file_count: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class GetGroupFilesByFolder extends OneBotAction<any, any> { export class GetGroupFilesByFolder extends OneBotAction<any, any> {
actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder; actionName = ActionName.GoCQHTTP_GetGroupFilesByFolder;
@@ -23,7 +20,7 @@ export class GetGroupFilesByFolder extends OneBotAction<any, any> {
const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), { const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1, sortType: 1,
fileCount: +(payload.file_count ?? 50), fileCount: +payload.file_count,
startIndex: 0, startIndex: 0,
sortOrder: 2, sortOrder: 2,
showOnlinedocFolder: 0, showOnlinedocFolder: 0,

View File

@@ -1,18 +1,14 @@
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 { WebHonorType } from '@/core/types'; import { WebHonorType } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { type: Type.Optional(Type.Enum(WebHonorType))
group_id: { type: ['number', 'string'] }, });
type: { enum: [WebHonorType.ALL, WebHonorType.EMOTION, WebHonorType.LEGEND, WebHonorType.PERFORMER, WebHonorType.STRONG_NEWBIE, WebHonorType.TALKATIVE] },
}, type Payload = Static<typeof SchemaData>;
required: ['group_id'],
} as const satisfies JSONSchema;
// enum是不是有点抽象
type Payload = FromSchema<typeof SchemaData>;
export class GetGroupHonorInfo extends OneBotAction<Payload, Array<any>> { export class GetGroupHonorInfo extends OneBotAction<Payload, Array<any>> {
actionName = ActionName.GetGroupHonorInfo; actionName = ActionName.GetGroupHonorInfo;

View File

@@ -2,26 +2,24 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11Message } from '@/onebot'; import { OB11Message } from '@/onebot';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { ChatType, Peer } from '@/core/types'; import { ChatType, Peer } from '@/core/types';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { MessageUnique } from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { AdapterConfigWrap } from '@/onebot/config/config'; import { AdapterConfigWrap } from '@/onebot/config/config';
import { Static, Type } from '@sinclair/typebox';
interface Response { interface Response {
messages: OB11Message[]; messages: OB11Message[];
} }
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
group_id: { type: ['number', 'string'] }, count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
message_seq: { type: ['number', 'string'] }, reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
count: { type: ['number', 'string'] }, });
reverseOrder: { type: ['boolean', 'string'] },
},
required: ['group_id'], type Payload = Static<typeof SchemaData>;
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Response> { export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Response> {
actionName = ActionName.GoCQHTTP_GetGroupMsgHistory; actionName = ActionName.GoCQHTTP_GetGroupMsgHistory;
@@ -30,13 +28,12 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction<Payload, Re
async _handle(payload: Payload, adapter: string): Promise<Response> { async _handle(payload: Payload, adapter: string): Promise<Response> {
//处理参数 //处理参数
const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder; const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const MsgCount = +(payload.count ?? 20);
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() }; const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0'); const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
//拉取消息 //拉取消息
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0'; const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ? const msgList = hasMessageSeq ?
(await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, MsgCount)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, MsgCount)).msgList; (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`); if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
//翻转消息 //翻转消息
if (isReverseOrder) msgList.reverse(); if (isReverseOrder) msgList.reverse();

View File

@@ -1,19 +1,16 @@
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 { OB11GroupFile, OB11GroupFileFolder } from '@/onebot'; import { OB11GroupFile, OB11GroupFileFolder } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { file_count: Type.Union([Type.Number(), Type.String()], { default: 50 }),
group_id: { type: ['string', 'number'] }, });
file_count: { type: ['string', 'number'] },
},
required: ['group_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class GetGroupRootFiles extends OneBotAction<Payload, { export class GetGroupRootFiles extends OneBotAction<Payload, {
files: OB11GroupFile[], files: OB11GroupFile[],
@@ -24,7 +21,7 @@ export class GetGroupRootFiles extends OneBotAction<Payload, {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), { const ret = await this.core.apis.MsgApi.getGroupFileList(payload.group_id.toString(), {
sortType: 1, sortType: 1,
fileCount: +(payload.file_count ?? 50), fileCount: +payload.file_count,
startIndex: 0, startIndex: 0,
sortOrder: 2, sortOrder: 2,
showOnlinedocFolder: 0, showOnlinedocFolder: 0,

View File

@@ -1,15 +1,7 @@
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 { JSONSchema } from 'json-schema-to-ts';
import { sleep } from '@/common/helper'; import { sleep } from '@/common/helper';
const SchemaData = {
type: 'object',
properties: {
no_cache: { type: 'boolean' },
},
} as const satisfies JSONSchema;
export class GetOnlineClient extends OneBotAction<void, Array<any>> { export class GetOnlineClient extends OneBotAction<void, Array<any>> {
actionName = ActionName.GetOnlineClient; actionName = ActionName.GetOnlineClient;

View File

@@ -2,18 +2,14 @@ import { OneBotAction } from '@/onebot/action/OneBotAction';
import { OB11User, OB11UserSex } from '@/onebot'; import { OB11User, OB11UserSex } from '@/onebot';
import { OB11Construct } from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import { ActionName } from '@/onebot/action/router'; import { ActionName } from '@/onebot/action/router';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { calcQQLevel } from '@/common/helper'; import { calcQQLevel } from '@/common/helper';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', user_id: Type.Union([Type.Number(), Type.String()]),
properties: { });
user_id: { type: ['number', 'string'] },
},
required: ['user_id'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> { export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11User> {
actionName = ActionName.GoCQHTTP_GetStrangerInfo; actionName = ActionName.GoCQHTTP_GetStrangerInfo;

View File

@@ -1,15 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', url: Type.String(),
properties: { });
url: { type: 'string' },
}, type Payload = Static<typeof SchemaData>;
required: ['url'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPCheckUrlSafely extends OneBotAction<Payload, any> { export class GoCQHTTPCheckUrlSafely extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_CheckUrlSafely; actionName = ActionName.GoCQHTTP_CheckUrlSafely;

View File

@@ -1,22 +1,15 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', friend_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
properties: { user_id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
friend_id: { type: ['string', 'number'] }, temp_block: Type.Optional(Type.Boolean()),
user_id: { type: ['string', 'number'] }, temp_both_del: Type.Optional(Type.Boolean()),
temp_block: { type: 'boolean' }, });
temp_both_del: { type: 'boolean' },
},
oneOf: [
{ required: ['friend_id'] },
{ required: ['user_id'] },
],
} as const satisfies JSONSchema; type Payload = Static<typeof SchemaData>;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPDeleteFriend extends OneBotAction<Payload, any> { export class GoCQHTTPDeleteFriend extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_DeleteFriend; actionName = ActionName.GoCQHTTP_DeleteFriend;

View File

@@ -1,14 +1,12 @@
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 { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', model: Type.String(),
properties: { });
model: { type: 'string' },
} type Payload = Static<typeof SchemaData>;
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
export class GoCQHTTPGetModelShow extends OneBotAction<Payload, any> { export class GoCQHTTPGetModelShow extends OneBotAction<Payload, any> {
actionName = ActionName.GoCQHTTP_GetModelShow; actionName = ActionName.GoCQHTTP_GetModelShow;

View File

@@ -1,19 +1,10 @@
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';
const SchemaData = {
type: 'object',
properties: {},
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>;
//兼容性代码 //兼容性代码
export class GoCQHTTPSetModelShow extends OneBotAction<Payload, any> { export class GoCQHTTPSetModelShow extends OneBotAction<void, any> {
actionName = ActionName.GoCQHTTP_SetModelShow; actionName = ActionName.GoCQHTTP_SetModelShow;
payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: void) {
return null; return null;
} }
} }

View File

@@ -13,7 +13,7 @@ export class GoCQHTTPHandleQuickAction extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
this.obContext.apis.QuickActionApi this.obContext.apis.QuickActionApi
.handleQuickOperation(payload.context, payload.operation) .handleQuickOperation(payload.context, payload.operation)
.catch(this.core.context.logger.logError.bind(this.core.context.logger)); .catch(e => this.core.context.logger.logError(e));
return null; return null;
} }
} }

View File

@@ -1,25 +1,21 @@
import { checkFileExist, uri2local } from '@/common/file'; import { checkFileExist, uriToLocalFile } from '@/common/file';
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 { unlink } from 'node:fs'; import { unlink } from 'node:fs/promises';
import { FromSchema, JSONSchema } from 'json-schema-to-ts'; import { Static, Type } from '@sinclair/typebox';
const SchemaData = { const SchemaData = Type.Object({
type: 'object', group_id: Type.Union([Type.Number(), Type.String()]),
properties: { content: Type.String(),
group_id: { type: ['number', 'string'] }, image: Type.Optional(Type.String()),
content: { type: 'string' }, pinned: Type.Union([Type.Number(), Type.String()], { default: 0 }),
image: { type: 'string' }, type: Type.Union([Type.Number(), Type.String()], { default: 1 }),
pinned: { type: ['number', 'string'] }, confirm_required: Type.Union([Type.Number(), Type.String()], { default: 1 }),
type: { type: ['number', 'string'] }, is_show_edit_card: Type.Union([Type.Number(), Type.String()], { default: 0 }),
confirm_required: { type: ['number', 'string'] }, tip_window_type: Type.Union([Type.Number(), Type.String()], { default: 0 })
is_show_edit_card: { type: ['number', 'string'] }, });
tip_window_type: { type: ['number', 'string'] },
},
required: ['group_id', 'content'],
} as const satisfies JSONSchema;
type Payload = FromSchema<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
export class SendGroupNotice extends OneBotAction<Payload, null> { export class SendGroupNotice extends OneBotAction<Payload, null> {
actionName = ActionName.GoCQHTTP_SendGroupNotice; actionName = ActionName.GoCQHTTP_SendGroupNotice;
@@ -32,7 +28,7 @@ export class SendGroupNotice extends OneBotAction<Payload, null> {
const { const {
path, path,
success, success,
} = (await uri2local(this.core.NapCatTempPath, payload.image)); } = (await uriToLocalFile(this.core.NapCatTempPath, payload.image));
if (!success) { if (!success) {
throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`); throw new Error(`群公告${payload.image}设置失败,image字段可能格式不正确`);
} }
@@ -45,26 +41,18 @@ export class SendGroupNotice extends OneBotAction<Payload, null> {
throw new Error(`群公告${payload.image}设置失败,图片上传失败`); throw new Error(`群公告${payload.image}设置失败,图片上传失败`);
} }
unlink(path, () => { unlink(path).catch(() => { });
});
UploadImage = ImageUploadResult.picInfo; UploadImage = ImageUploadResult.picInfo;
} }
const noticeType = +(payload.type ?? 1);
const noticePinned = +(payload.pinned ?? 0);
const noticeShowEditCard = +(payload.is_show_edit_card ?? 0);
const noticeTipWindowType = +(payload.tip_window_type ?? 0);
const noticeConfirmRequired = +(payload.confirm_required ?? 1);
const publishGroupBulletinResult = await this.core.apis.WebApi.setGroupNotice( const publishGroupBulletinResult = await this.core.apis.WebApi.setGroupNotice(
payload.group_id.toString(), payload.group_id.toString(),
payload.content, payload.content,
noticePinned, +payload.pinned,
noticeType, +payload.type,
noticeShowEditCard, +payload.is_show_edit_card,
noticeTipWindowType, +payload.tip_window_type,
noticeConfirmRequired, +payload.confirm_required,
UploadImage?.id, UploadImage?.id,
UploadImage?.width, UploadImage?.width,
UploadImage?.height UploadImage?.height

Some files were not shown because too many files have changed in this diff Show More