Compare commits

..

308 Commits

Author SHA1 Message Date
手瓜一十雪
e3814403e4 fix: #682 2025-01-07 19:28:46 +08:00
手瓜一十雪
3d16d52dd8 fix: #696 fallback 2025-01-07 18:51:04 +08:00
Mlikiowa
1ae47fffb4 release: v4.2.68 2025-01-06 13:08:43 +00:00
手瓜一十雪
4e7096b9e2 feat: offset win 31219 2025-01-06 21:08:18 +08:00
Mlikiowa
8cc9b7f6a7 release: v4.2.67 2025-01-05 02:50:38 +00:00
手瓜一十雪
fb45c1020e revert: 再也不用umami了 2025-01-05 10:49:49 +08:00
Mlikiowa
e9db4ae8f4 release: v4.2.66 2025-01-04 11:52:38 +00:00
手瓜一十雪
c46ec32bd6 fix: 简化代码 2025-01-04 13:12:04 +08:00
手瓜一十雪
c58a26ed99 fix 2025-01-04 13:06:37 +08:00
手瓜一十雪
a66f5e4971 fix 2025-01-04 13:03:29 +08:00
pk5ls20
574c8c6089 fix: SendGroupAiRecord
- smtx
2025-01-04 12:48:53 +08:00
手瓜一十雪
67afd95910 feat: getUserDetailInfoV2 2025-01-03 21:38:57 +08:00
手瓜一十雪
f7d0cb0be7 fix 2025-01-03 21:07:26 +08:00
手瓜一十雪
be9b68a0b1 feat: CancelableTask&Fallback 2025-01-03 20:46:51 +08:00
Mlikiowa
4637414af2 release: v4.2.65 2025-01-03 05:14:05 +00:00
手瓜一十雪
4bd92a72bd fix: ua agent 2025-01-03 13:09:43 +08:00
手瓜一十雪
a3be26f3e4 fix: error 2025-01-03 12:44:55 +08:00
Mlikiowa
675c906cbf release: v4.2.64 2024-12-31 14:42:17 +00:00
手瓜一十雪
6be6023236 Merge pull request #679 from wu-yafeng/main
fix #678
2024-12-31 22:41:34 +08:00
WuYafeng
42cee0d018 在没有写入权限时记录warn日志 2024-12-31 22:38:01 +08:00
WuYafeng
041f725748 Update config.ts 2024-12-31 22:25:05 +08:00
WuYafeng
0594d61631 Update config.ts 2024-12-31 22:20:34 +08:00
Mlikiowa
15cae6b765 release: v4.2.63 2024-12-31 13:52:37 +00:00
pk5ls20
b984116c35 fix: #677 2024-12-31 21:34:45 +08:00
Mlikiowa
13bda6e3f4 release: v4.2.62 2024-12-31 08:35:55 +00:00
手瓜一十雪
c0d18549d1 fix 2024-12-31 15:09:08 +08:00
手瓜一十雪
3caff72fce fix: cache trace 2024-12-31 13:10:58 +08:00
Mlikiowa
1313e9c3f4 release: v4.2.61 2024-12-30 15:59:33 +00:00
手瓜一十雪
0848d5a39e feat: clouflare 2024-12-30 23:59:09 +08:00
手瓜一十雪
7660646059 fix 2024-12-30 22:16:09 +08:00
Mlikiowa
bcd90fc744 release: v4.2.60 2024-12-30 12:48:11 +00:00
手瓜一十雪
638fc22d62 fix:error 2024-12-30 20:47:50 +08:00
手瓜一十雪
c87d365b88 fix 2024-12-30 20:42:23 +08:00
Mlikiowa
aee9602f25 release: v4.2.59 2024-12-30 12:29:33 +00:00
手瓜一十雪
976fbd0220 fix: 修复精确统计 2024-12-30 20:27:28 +08:00
手瓜一十雪
afd955d06f fix: error 2024-12-30 20:14:51 +08:00
手瓜一十雪
4d548da66b fix: console-log 2024-12-30 19:56:06 +08:00
手瓜一十雪
41b70f53d1 fix: LoginErrorCode 2024-12-30 19:29:14 +08:00
Wesley F. Young
a47a618bcd make author NapNeko in manifest.json
毕竟我已经脱离一线开发挺久了(
2024-12-30 18:40:57 +08:00
Mlikiowa
62170a30af release: v4.2.58 2024-12-30 09:56:02 +00:00
手瓜一十雪
780c5ac23c fix 2024-12-30 17:55:40 +08:00
手瓜一十雪
9fba519a5a fix 2024-12-30 17:55:13 +08:00
Mlikiowa
3cd0e7d26b release: v4.2.57 2024-12-30 09:54:40 +00:00
手瓜一十雪
a8fd6af994 Merge pull request #676 from NapNeko/dependabot/npm_and_yarn/commander-13.0.0
chore(deps-dev): bump commander from 12.1.0 to 13.0.0
2024-12-30 17:54:03 +08:00
手瓜一十雪
4000b89644 fix 2024-12-30 17:53:05 +08:00
Mlikiowa
9c00bbc0b7 release: v4.2.56 2024-12-30 08:55:36 +00:00
手瓜一十雪
a2989d3b38 fix:workname 2024-12-30 16:54:40 +08:00
Mlikiowa
fc731b60d5 release: v4.2.54 2024-12-30 08:47:57 +00:00
手瓜一十雪
193980dd4a fix: trace 2024-12-30 16:47:33 +08:00
dependabot[bot]
35427b0768 chore(deps-dev): bump commander from 12.1.0 to 13.0.0
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 13.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v12.1.0...v13.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 08:36:27 +00:00
Mlikiowa
73ea130e40 release: v4.2.53 2024-12-30 08:34:23 +00:00
手瓜一十雪
5667e6aaee fix 2024-12-30 16:34:01 +08:00
Mlikiowa
fbd626131d release: v4.2.52 2024-12-30 08:17:53 +00:00
手瓜一十雪
7b82444338 fix 2024-12-30 16:17:29 +08:00
Mlikiowa
8108b9f565 release: v4.2.51 2024-12-30 08:15:48 +00:00
手瓜一十雪
c6ddd00cd9 Merge pull request #675 from NapNeko/test
fix: 重写远程遥测
2024-12-30 16:14:48 +08:00
手瓜一十雪
20c0c00fa0 fixfix 2024-12-30 16:14:32 +08:00
手瓜一十雪
1f90364ba6 fix: 重写远程遥测 2024-12-30 16:10:38 +08:00
手瓜一十雪
49ea4d31a5 fix 2024-12-30 13:23:33 +08:00
Mlikiowa
dc35f1456a release: v4.2.50 2024-12-30 04:33:08 +00:00
手瓜一十雪
0ebeb90804 fix 2024-12-30 12:31:43 +08:00
手瓜一十雪
3ef5436c98 Merge pull request #673 from NapNeko/fixseq
fix: #670
2024-12-30 12:28:29 +08:00
手瓜一十雪
de7996d789 refactor: 进群入群 2024-12-30 12:27:42 +08:00
手瓜一十雪
ac52d9bae2 fix: 算了能用就行 2024-12-30 11:59:16 +08:00
手瓜一十雪
cb02df3b76 Merge pull request #672 from clansty/feat/download_file-file-proto
feat: download_file 支持 file://
2024-12-29 22:45:27 +08:00
手瓜一十雪
5fc5a6f1a6 fix: #670 2024-12-29 22:44:25 +08:00
手瓜一十雪
726a0d0394 fix 2024-12-29 22:30:33 +08:00
Clansty
6edf5345a3 chore: remove unused import 2024-12-29 22:27:41 +08:00
Clansty
242bbfdb14 chore: uriToLocalFile 2024-12-29 22:03:04 +08:00
Clansty
89e7712676 feat: download_file 支持 file:// 2024-12-29 21:41:44 +08:00
手瓜一十雪
9525786929 fix 2024-12-29 16:20:41 +08:00
Mlikiowa
72088e41a8 release: v4.2.46 2024-12-29 07:47:38 +00:00
手瓜一十雪
a3ed9ff2ef Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-12-29 15:47:18 +08:00
手瓜一十雪
ff16dc73ec fix: typo 2024-12-29 15:47:16 +08:00
Mlikiowa
2da4ef5f0f release: v4.2.45 2024-12-29 07:46:08 +00:00
手瓜一十雪
eaf481799d feat: 追踪退出 2024-12-29 15:44:37 +08:00
手瓜一十雪
1f72863aba fix: 追踪登录失败 2024-12-29 15:25:51 +08:00
Mlikiowa
6b353fd8d8 release: v4.2.44 2024-12-29 06:45:43 +00:00
手瓜一十雪
56cde4ad79 fix: #669 2024-12-29 14:44:47 +08:00
手瓜一十雪
3b86d3c632 feat: umami 统计追踪 2024-12-29 14:39:17 +08:00
Mlikiowa
4ac7a25afb release: v4.2.43 2024-12-29 04:04:55 +00:00
手瓜一十雪
8248011a12 fix: #667 2024-12-29 11:54:09 +08:00
手瓜一十雪
5f454456d2 fix: #655 #663 2024-12-29 11:47:09 +08:00
手瓜一十雪
e99a619c23 Merge pull request #665 from clansty/feat/group_name
fix: wrong notice_type sub_type
2024-12-28 21:42:14 +08:00
Clansty
1fc791bb68 fix: wrong notice_type sub_type 2024-12-28 21:40:32 +08:00
手瓜一十雪
f1d83f7c16 Merge pull request #664 from clansty/feat/group_name
feat: 群名称变更事件
2024-12-28 21:05:50 +08:00
Clansty
527bb72bcf feat: 群名称变更事件 2024-12-28 20:54:41 +08:00
手瓜一十雪
d78409fd07 Merge pull request #662 from Shua-github/main
新增send_poke
2024-12-27 15:16:11 +08:00
手瓜一十雪
d5e7e8944f feat: send_poke 2024-12-27 15:14:57 +08:00
Shua-github
fb405a5c1c all_poke替换成send_poke 2024-12-27 13:15:10 +08:00
Shua-github
a9e471deca 新增all_poke 2024-12-27 00:24:35 +08:00
Mlikiowa
9cd15ae337 release: v4.2.42 2024-12-26 12:37:35 +00:00
手瓜一十雪
8ed4cc4b0a feat: send_packet 2024-12-26 20:36:53 +08:00
Mlikiowa
a62de441cf release: v4.2.41 2024-12-26 05:31:09 +00:00
手瓜一十雪
02a8999410 Merge pull request #652 from JerryZRF/main
feat: add `get_clientkey`
2024-12-25 12:51:44 +08:00
手瓜一十雪
59c7979d69 readme: new 2024-12-25 12:28:53 +08:00
手瓜一十雪
bb7b28cd8f feat: 调整logo 2024-12-25 12:24:03 +08:00
手瓜一十雪
056497b98a Merge pull request #657 from FfmpegZZZ/main
chore:修改文档链接
2024-12-24 20:22:59 +08:00
手瓜一十雪
ac2fb032c4 Merge branch 'main' into main 2024-12-24 20:22:40 +08:00
Ffmpeg
c933bdd5d9 chore:修改链接
## 我是猪咪

文档链接打错了
2024-12-24 20:07:11 +08:00
Ffmpeg
89c71a58fa 添加文档地址 (#656) 2024-12-24 19:45:34 +08:00
Ffmpeg
27ba85b4ff 添加文档地址 2024-12-24 19:41:35 +08:00
手瓜一十雪
79a75fed8e feat: 30899 2024-12-24 15:38:53 +08:00
Mlikiowa
ee7a76b29f release: v4.2.40 2024-12-24 07:29:39 +00:00
手瓜一十雪
c53bdc3ce0 feat: 30899 2024-12-24 15:19:44 +08:00
Mlikiowa
f36e328751 release: v4.2.39 2024-12-22 13:32:51 +00:00
pk5ls20
871b5688c2 feat: webui api (/QQVersion & /GetSysStatusRealTime) 2024-12-22 21:31:14 +08:00
JerryZRF
b96076b297 fix: incorrect import 2024-12-22 13:28:20 +08:00
pk5ls20
d4488e40cf feat: better system status helper
- cpu usage diff
2024-12-22 03:56:24 +08:00
pk5ls20
7e61497243 chore: remove console logs 2024-12-22 02:57:00 +08:00
pk5ls20
e71ccdd12a feat: system status helper
- remove duplicate os import
2024-12-22 02:55:49 +08:00
pk5ls20
202129d491 feat: system status helper
- remove pidusage
2024-12-22 02:24:10 +08:00
JerryZRF
a1700dd503 fix: incorrect import 2024-12-22 01:33:42 +08:00
JerryZRF
2954776539 feat: add get_clientkey 2024-12-21 20:43:15 +08:00
手瓜一十雪
fb1f122ef7 feat: 9.9.17-30851 2024-12-21 14:29:41 +08:00
手瓜一十雪
96c63e4689 refactor: 移除不再使用的代码 2024-12-21 14:19:03 +08:00
手瓜一十雪
c94936d3dc refactor: #637 2024-12-21 14:09:57 +08:00
pk5ls20
8c22f11087 feat: system status helper 2024-12-21 13:11:10 +08:00
Mlikiowa
8a089c84a9 release: v4.2.38 2024-12-20 11:46:02 +00:00
手瓜一十雪
b631e6f8a2 fix: 更精确的筛选 2024-12-20 19:44:57 +08:00
手瓜一十雪
b3b48b032c fix: 锁住esbuild版本 以缓解问题 2024-12-20 19:32:20 +08:00
手瓜一十雪
f3e8230eca fix: 暂时指定esbuild版本以缓解上游破坏
see https://github.com/evanw/esbuild/issues/4010
2024-12-20 19:30:30 +08:00
手瓜一十雪
cc9adf9d40 feat: 过滤空消息 2024-12-20 18:58:59 +08:00
手瓜一十雪
15a640d1dc fix: #637 2024-12-20 18:55:30 +08:00
凌莞~(=^▽^=)
c25b9f86db 优化私聊戳一戳事件上报 (#643) 2024-12-18 21:27:44 +08:00
Mlikiowa
ecfd033afb release: v4.2.37 2024-12-17 07:56:18 +00:00
手瓜一十雪
f3ed8c7dff fix #633 2024-12-17 15:55:18 +08:00
Mlikiowa
6089046721 release: v4.2.36 2024-12-17 03:37:26 +00:00
手瓜一十雪
44ff92ad4b style: lint 2024-12-17 09:25:10 +08:00
手瓜一十雪
892262eb85 Merge pull request #635 from NapNeko/ref/ob-network
refactor: adjust onebot network
2024-12-17 09:07:39 +08:00
pk5ls20
2d9cc4d198 fix: as design 2024-12-17 07:07:04 +08:00
pk5ls20
a0c479485d refactor: adjust onebot network 2024-12-17 05:26:27 +08:00
手瓜一十雪
5650f18e50 Merge pull request #634 from bietiaop/main
fix: handleQuickOperation error
2024-12-17 00:29:16 +08:00
bietiaop
553885d025 fix: handleQuickOperation 2024-12-17 00:27:56 +08:00
Mlikiowa
35de00c4af release: v4.2.35 2024-12-16 14:10:08 +00:00
手瓜一十雪
09583e5de5 fuck javascript 2024-12-16 22:09:37 +08:00
Mlikiowa
38b0b7cd00 release: v4.2.34 2024-12-16 13:17:43 +00:00
手瓜一十雪
8b9c7b0c27 Merge pull request #632 from q8018414/patch-1
Update AboutUs.vue
2024-12-16 21:16:39 +08:00
手瓜一十雪
1005619bf3 Merge pull request #630 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2
chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
2024-12-16 21:16:01 +08:00
手瓜一十雪
3e09cff9cb Merge branch 'main' into dependabot/npm_and_yarn/rollup/plugin-typescript-12.1.2 2024-12-16 21:15:52 +08:00
手瓜一十雪
c24384e454 Merge pull request #629 from NapNeko/dependabot/npm_and_yarn/rollup/plugin-node-resolve-16.0.0
chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
2024-12-16 21:15:23 +08:00
手瓜一十雪
f87a543406 fix: #631 2024-12-16 21:14:14 +08:00
手瓜一十雪
f752136283 fix: #631 2024-12-16 21:06:51 +08:00
my_key
7e71622a44 Update AboutUs.vue
新增用于显示New NapCat的tag,便于区分当前版本和最新版本
2024-12-16 20:09:40 +08:00
dependabot[bot]
da92afb379 chore(deps-dev): bump @rollup/plugin-typescript from 11.1.6 to 12.1.2
Bumps [@rollup/plugin-typescript](https://github.com/rollup/plugins/tree/HEAD/packages/typescript) from 11.1.6 to 12.1.2.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/typescript/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/typescript-v12.1.2/packages/typescript)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-typescript"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:44:42 +00:00
dependabot[bot]
d3062de5f9 chore(deps-dev): bump @rollup/plugin-node-resolve from 15.3.1 to 16.0.0
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 15.3.1 to 16.0.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/commonjs-v16.0.0/packages/node-resolve)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-node-resolve"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 08:43:46 +00:00
Mlikiowa
f1440b03a8 release: v4.2.33 2024-12-16 05:03:59 +00:00
手瓜一十雪
9a8b266cef Merge pull request #627 from bietiaop/main
feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法
2024-12-16 13:03:09 +08:00
手瓜一十雪
2a9bc57120 fix: #628 2024-12-16 13:00:07 +08:00
bietiaop
2ed83a0e30 feat: 查看登录QQ信息&获取快速登录列表详细信息&获取nc的包信息&优化了部分写法 2024-12-16 12:46:27 +08:00
Mlikiowa
116e8fd30a release: v4.2.32 2024-12-14 07:03:02 +00:00
手瓜一十雪
891f11173b fix 2024-12-14 15:01:47 +08:00
手瓜一十雪
dfc7996c17 Merge branch 'maybe-feat/type' 2024-12-14 15:00:18 +08:00
手瓜一十雪
dc0561d34f refactor: OB11ActiveHttpAdapter 2024-12-14 14:26:23 +08:00
手瓜一十雪
4fb0845d79 fix: #619 2024-12-14 13:46:25 +08:00
pk5ls20
0e0d4837b8 feat & fix: GetMiniAppArk 2024-12-14 06:56:23 +08:00
pk5ls20
a6adde7966 feat: attempt to enhance type inference 2024-12-14 05:56:49 +08:00
手瓜一十雪
7b693132f9 fix 2024-12-13 23:27:24 +08:00
手瓜一十雪
3c3114b6ab fix: 支持类型推导 2024-12-13 23:23:58 +08:00
Mlikiowa
5cdbf58f59 release: v4.2.31 2024-12-13 14:04:13 +00:00
手瓜一十雪
6f0a4131a2 fix: 异常 2024-12-13 22:03:51 +08:00
Mlikiowa
aa520e2f5d release: v4.2.30 2024-12-13 11:28:32 +00:00
手瓜一十雪
2c3b7e9ee8 fix: fuck! tencent 2024-12-13 19:28:04 +08:00
手瓜一十雪
b86a28092a fix: Login Check 2024-12-13 17:38:27 +08:00
手瓜一十雪
d59e5f2133 fix 2024-12-13 16:45:35 +08:00
手瓜一十雪
3fdd187102 fix: #610 2024-12-13 14:29:11 +08:00
Mlikiowa
3f085fd8ae release: v4.2.29 2024-12-13 04:47:13 +00:00
手瓜一十雪
a4fc131aec feat: 全平台30594 2024-12-13 12:36:51 +08:00
手瓜一十雪
d7d446c3fc fix 2024-12-12 23:02:10 +08:00
手瓜一十雪
212666e603 fix 2024-12-12 18:41:46 +08:00
手瓜一十雪
b545c28340 feat: 30483全平台兼容 2024-12-12 16:57:50 +08:00
手瓜一十雪
72bc345515 feat: linux 3.2.15-30483 2024-12-12 16:53:31 +08:00
手瓜一十雪
cc5082a9e3 feat: mac 6.9.62-30483 2024-12-12 11:43:31 +08:00
手瓜一十雪
45782a6c6c chore: docs 2024-12-12 10:44:09 +08:00
手瓜一十雪
e86d646cce chore: 万里妹妹干坏事 2024-12-12 10:43:13 +08:00
手瓜一十雪
92cfc6b8c8 feat: 更换演示代码 2024-12-12 10:00:38 +08:00
手瓜一十雪
82289d9f1f chore: debug log remove 2024-12-12 09:57:52 +08:00
手瓜一十雪
4cdbdaaf4e feat: win 30483 2024-12-12 09:47:44 +08:00
手瓜一十雪
ecde2427da fix: path 2024-12-12 09:37:51 +08:00
手瓜一十雪
fed1ec5d83 Merge pull request #620 from NapNeko/plugin-support
Feat: 支持快速基于NapCat进行二次开发 不需要通过传统NetWork
2024-12-12 09:34:27 +08:00
手瓜一十雪
4fbd764ced fix 2024-12-12 09:34:13 +08:00
手瓜一十雪
5361079010 Merge pull request #616 from Ander-pixe/webui-new
fix:路由守卫
2024-12-12 09:33:23 +08:00
手瓜一十雪
002d135ef5 fix 2024-12-11 18:26:26 +08:00
手瓜一十雪
a39b0a4a78 feat: plugin 2024-12-11 17:07:21 +08:00
纸凤孤凰
eb5d68422f fix: 路由守卫 2024-12-10 16:59:57 +08:00
纸凤孤凰
3dc13e5c2e Merge remote-tracking branch 'origin/main' into webui-new 2024-12-10 15:29:27 +08:00
huankong233
16881f057a fix: solve the token error
fix: remove useless defineProps
fix: add the missing dependencies to package.json
fix: add ES2022 into tsconfig.json
2024-12-10 09:44:38 +08:00
手瓜一十雪
1cd7d0577f fix: error 2024-12-10 09:44:23 +08:00
Mlikiowa
3c872df97a release: v4.2.28 2024-12-08 14:37:51 +00:00
pk5ls20
218b7bd2a0 fix: #607 2024-12-08 22:23:01 +08:00
Mlikiowa
4552d6970d release: v4.2.27 2024-12-06 03:40:03 +00:00
手瓜一十雪
4b319d15a7 refactor: GetGroupInfo 2024-12-06 11:39:11 +08:00
Mlikiowa
0ae3a4172c release: v4.2.26 2024-12-06 02:40:53 +00:00
手瓜一十雪
bf0c12f1c4 fix: #605 2024-12-06 10:39:49 +08:00
Mlikiowa
cb5eeecb86 release: v4.2.25 2024-12-05 13:13:34 +00:00
手瓜一十雪
8d857cf2be Revert "refactor: CardChangedEvent"
This reverts commit 0e8ceeb6c9.
2024-12-05 21:13:03 +08:00
手瓜一十雪
6f232c465f Merge pull request #604 from Ander-pixe/webui-new
feat:实时日志、关于and部分样式优化
2024-12-05 21:10:08 +08:00
纸凤孤凰
032d444246 Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:27:08 +08:00
纸凤孤凰
49488dd3fb Merge remote-tracking branch 'origin/webui-new' into webui-new 2024-12-05 20:25:14 +08:00
手瓜一十雪
9aec3865ff fix: historyLog 2024-12-05 20:22:55 +08:00
手瓜一十雪
b6b7f2051b fix 2024-12-05 20:02:24 +08:00
手瓜一十雪
46254a699a fix 2024-12-05 19:56:36 +08:00
手瓜一十雪
7b3c287137 fix: cors 2024-12-05 19:37:56 +08:00
手瓜一十雪
1a533742a5 fix: Log 2024-12-05 19:17:07 +08:00
手瓜一十雪
2027266852 fix 2024-12-05 18:56:55 +08:00
手瓜一十雪
946d8b1a7b fix: dependencies error 2024-12-05 18:49:45 +08:00
手瓜一十雪
6d2fb5de6f Merge branch 'main' into pr/604 2024-12-05 18:40:46 +08:00
pk5ls20
91c4a002dd fix: dependencies 2024-12-05 18:15:00 +08:00
纸凤孤凰
4d8112aae5 feat:实时日志、关于and部分样式优化 2024-12-05 15:31:42 +08:00
手瓜一十雪
bb53f245cf style: lint check 2024-12-05 14:50:27 +08:00
手瓜一十雪
9f31cdbf5b fix: 移除不使用api 2024-12-05 14:46:05 +08:00
手瓜一十雪
9a33039d73 fix: 暂时移除 QunAlbum 2024-12-05 14:45:12 +08:00
手瓜一十雪
7cf3be8333 refactor: predict time 2024-12-05 14:42:45 +08:00
Nanako
82afb88e53 Update README.md 2024-12-05 14:37:10 +08:00
Mlikiowa
4aa24b5d67 release: v4.2.24 2024-12-05 06:17:46 +00:00
手瓜一十雪
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
214 changed files with 5056 additions and 3261 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

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
![Logo](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto) ![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
</div> </div>
@@ -30,11 +30,25 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.Pages](https://napneko.pages.dev/) [Cloudflare.Pages](https://napneko.pages.dev/)
[Server.Other](https://napcat.cyou/) [Server.Other](https://docs.napcat.cyou/)
[Qbot.News](https://neko.qbot.news)
## 回家旅途 ## 回家旅途
[QQ Group](https://qm.qq.com/q/NWP25OeV0c) [QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
[QQ Group#2](https://qm.qq.com/q/uqh4I87KoM)
[Telegram](https://t.me/MelodicMoonlight)
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用上报数据中大量使用Magic生成字段消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
## 感谢他们 ## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权

Binary file not shown.

BIN
external/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{ {
"name": "qq-chat", "name": "qq-chat",
"version": "9.9.16-29456", "version": "9.9.17-30899",
"verHash": "dd395162", "verHash": "ececf273",
"linuxVersion": "3.2.13-29456", "linuxVersion": "3.2.15-30899",
"linuxVerHash": "e379390a", "linuxVerHash": "63c751e8",
"type": "module", "type": "module",
"private": true, "private": true,
"description": "QQ", "description": "QQ",
@@ -18,7 +18,7 @@
"qd": "externals/devtools/cli/index.js" "qd": "externals/devtools/cli/index.js"
}, },
"main": "./loadNapCat.js", "main": "./loadNapCat.js",
"buildVersion": "29456", "buildVersion": "30899",
"isPureShell": true, "isPureShell": true,
"isByteCodeShell": true, "isByteCodeShell": true,
"platform": "win32", "platform": "win32",

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -4,16 +4,12 @@
"name": "NapCatQQ", "name": "NapCatQQ",
"slug": "NapCat.Framework", "slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现", "description": "高性能的 OneBot 11 协议实现",
"version": "4.2.0", "version": "4.2.68",
"icon": "./logo.png", "icon": "./logo.png",
"authors": [ "authors": [
{ {
"name": "MliKiowa", "name": "NapNeko",
"link": "https://github.com/MliKiowa" "link": "https://github.com/NapNeko"
},
{
"name": "Young",
"link": "https://github.com/Wesley-Young"
} }
], ],
"repository": { "repository": {

View File

@@ -5,12 +5,14 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}", "webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
"webui:dev": "vite", "webui:dev": "vite --host",
"webui:build": "vite build", "webui:build": "vite build",
"webui:preview": "vite preview" "webui:preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"event-source-polyfill": "^1.0.31",
"mitt": "^3.0.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3", "tdesign-icons-vue-next": "^0.3.3",
"tdesign-vue-next": "^1.10.3", "tdesign-vue-next": "^1.10.3",
@@ -20,6 +22,7 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@types/event-source-polyfill": "^1.0.5",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@vitejs/plugin-legacy": "^5.4.3", "@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",

View File

@@ -109,4 +109,4 @@ onUnmounted(() => {
window.removeEventListener('resize', haddingFbars); window.removeEventListener('resize', haddingFbars);
}); });
</script> </script>
<style scoped></style> <style></style>

Binary file not shown.

View File

@@ -0,0 +1,66 @@
export class githubApiManager {
public async GetBaseData(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting github data :', error);
}
return null;
}
public async GetReleasesData(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/releases', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting releases data:', error);
}
return null;
}
public async GetPullsData(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/pulls', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting Pulls data:', error);
}
return null;
}
public async GetContributors(): Promise<Response | null> {
try {
const ConfigResponse = await fetch('https://api.github.com/repos/NapNeko/NapCatQQ/contributors', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
return await ConfigResponse.json();
}
} catch (error) {
console.error('Error getting Pulls data:', error);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
import { EventSourcePolyfill } from 'event-source-polyfill';
type LogListItem = string;
type LogListData = LogListItem[];
let eventSourcePoly: EventSourcePolyfill | null = null;
export class LogManager {
private readonly retCredential: string;
private readonly apiPrefix: string;
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiPrefix: string = '../api') {
this.retCredential = retCredential;
this.apiPrefix = apiPrefix;
}
public async GetLogList(): Promise<LogListData> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLogList`, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data as LogListData;
}
}
} catch (error) {
console.error('Error getting LogList:', error);
}
return [] as LogListData;
}
public async GetLog(FileName: string): Promise<string> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/Log/GetLog?id=${FileName}`, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data;
}
}
} catch (error) {
console.error('Error getting LogData:', error);
}
return 'null';
}
public async getRealTimeLogs(): Promise<EventSourcePolyfill | null> {
this.creatEventSource();
return eventSourcePoly;
}
private creatEventSource() {
try {
eventSourcePoly = new EventSourcePolyfill(`${this.apiPrefix}/Log/GetLogRealTime`, {
heartbeatTimeout: 3 * 60 * 1000,
headers: {
Authorization: 'Bearer ' + this.retCredential,
Accept: 'text/event-stream',
},
withCredentials: true,
});
} catch (error) {
console.error('创建SSE连接出错:', error);
}
}
}

View File

@@ -1,5 +1,5 @@
import { OneBotConfig } from '../../../src/onebot/config/config'; import { OneBotConfig } from '../../../src/onebot/config/config';
import { ResponseCode } from '../../../src/webui/src/const/status';
export class QQLoginManager { export class QQLoginManager {
private retCredential: string; private retCredential: string;
private readonly apiPrefix: string; private readonly apiPrefix: string;
@@ -22,8 +22,8 @@ export class QQLoginManager {
}); });
if (ConfigResponse.status == 200) { if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json(); const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) { if (ConfigResponseJson.code == ResponseCode.Success) {
return ConfigResponseJson?.data as OneBotConfig; return ConfigResponseJson.data;
} }
} }
} catch (error) { } catch (error) {

View File

@@ -1,18 +1,28 @@
<template> <template>
<t-layout class="dashboard-container"> <t-layout class="dashboard-container">
<div ref="menuRef"> <div v-if="!mediaQuery.matches">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" /> <SidebarMenu
:menu-items="menuItems"
class="sidebar-menu"
:menu-width="sidebarWidth"
/>
</div> </div>
<t-layout> <t-layout>
<router-view /> <router-view />
</t-layout> </t-layout>
<div v-if="mediaQuery.matches" class="bottom-menu">
<BottomMenu :menu-items="menuItems" />
</div>
</t-layout> </t-layout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import SidebarMenu from './webui/Nav.vue'; import SidebarMenu from './webui/Nav.vue';
import BottomMenu from './webui/NavBottom.vue';
import emitter from '@/ts/event-bus'; import emitter from '@/ts/event-bus';
const mediaQuery = window.matchMedia('(max-width: 768px)');
const sidebarWidth = ['232px', '64px'];
interface MenuItem { interface MenuItem {
value: string; value: string;
icon: string; icon: string;
@@ -27,13 +37,18 @@ 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.on('sendMenu', (event) => {
emitter.emit('sendWidth', menuRef.value?.offsetWidth); const menuWidth = event ? sidebarWidth[1] : sidebarWidth[0];
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0'); emitter.emit('sendWidth', menuWidth);
localStorage.setItem('menuWidth', menuWidth.toString() || '0');
}); });
onMounted(() => { onMounted(() => {
localStorage.setItem('menuWidth', menuRef.value?.offsetWidth?.toString() || '0'); if (mediaQuery.matches){
localStorage.setItem('menuWidth', '0');
}
});
onUnmounted(() => {
}); });
</script> </script>
@@ -49,6 +64,12 @@ onMounted(() => {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
.bottom-menu {
position: fixed;
bottom: 0;
width: 100%;
z-index: 2;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.content { .content {
@@ -56,3 +77,19 @@ onMounted(() => {
} }
} }
</style> </style>
<style>
@media (max-width: 768px) {
.t-head-menu__inner .t-menu:first-child {
margin-left: 0;
}
.t-head-menu__inner{
width: 100%;
}
.t-head-menu .t-menu{
justify-content: space-evenly;
}
.t-menu__content{
display: none;
}
}
</style>

View File

@@ -1,34 +1,20 @@
<template> <template>
<t-card class="layout"> <t-card class="layout" :bordered="false">
<div class="login-container"> <div class="login-container">
<h2 class="sotheby-font">QQ Login</h2> <h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods"> <div class="login-methods">
<t-tooltip content="快速登录"> <t-tooltip content="快速登录">
<t-button <t-button id="quick-login" class="login-method" :class="{ active: loginMethod === 'quick' }"
id="quick-login" @click="loginMethod = 'quick'">Quick Login</t-button>
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
</t-tooltip> </t-tooltip>
<t-tooltip content="二维码登录"> <t-tooltip content="二维码登录">
<t-button <t-button id="qrcode-login" class="login-method" :class="{ active: loginMethod === 'qrcode' }"
id="qrcode-login" @click="loginMethod = 'qrcode'">QR Code</t-button>
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</t-tooltip> </t-tooltip>
</div> </div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form"> <div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select <t-select id="quick-login-select" v-model="selectedAccount" placeholder="Select Account"
id="quick-login-select" @change="selectAccount">
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option> <t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select> </t-select>
</div> </div>
@@ -41,7 +27,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import * as QRCode from 'qrcode'; import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
@@ -55,6 +41,7 @@ const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || ''); const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null; let heartBeatTimer: number | null = null;
let qrcodeUrl: string = ''; 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) {
@@ -88,10 +75,6 @@ const HeartBeat = async (): Promise<void> => {
if (heartBeatTimer) { if (heartBeatTimer) {
clearInterval(heartBeatTimer); clearInterval(heartBeatTimer);
} }
// //判断是否已经调转
// if (router.currentRoute.value.path !== '/dashboard/basic-info') {
// return;
// }
await MessagePlugin.success('登录成功即将跳转'); 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) {
@@ -103,19 +86,38 @@ const HeartBeat = async (): Promise<void> => {
const InitPages = async (): Promise<void> => { const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList(); quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
qrcodeUrl = await qqLoginManager.getQQLoginQrcode(); qrcodeUrl = await qqLoginManager.getQQLoginQrcode();
await nextTick();
generateQrCode(qrcodeUrl, qrcodeCanvas.value); generateQrCode(qrcodeUrl, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
}; };
onMounted(() => { onMounted(() => {
InitPages(); InitPages().then().catch((err) => {
console.error('InitPages Error:', err);
});
heartBeatTimer = window.setInterval(HeartBeat, 3000);
}); });
onBeforeUnmount(() => {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
});
watch(loginMethod, async (newMethod) => {
if (newMethod === 'qrcode') {
await nextTick();
generateQrCode(qrcodeUrl, qrcodeCanvas.value);
}
});
</script> </script>
<style scoped> <style scoped>
.layout { .layout {
height: 100vh; height: 100vh;
} }
.login-container { .login-container {
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;
@@ -182,4 +184,4 @@ onMounted(() => {
left: 0; left: 0;
right: 0; right: 0;
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<t-card class="layout"> <t-card class="layout" :bordered="false">
<div class="login-container"> <div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2> <h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit"> <t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">

View File

@@ -1,5 +1,5 @@
<template> <template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu"> <t-menu theme="light" :width="menuWidth" :collapsed="collapsed" class="sidebar-menu">
<template #logo> <template #logo>
<div class="logo"> <div class="logo">
<img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" /> <img class="logo-img" :width="collapsed ? 35 : 'auto'" src="@/assets/logo_webui.png" alt="logo" />
@@ -33,7 +33,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, onMounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import emitter from '@/ts/event-bus'; import emitter from '@/ts/event-bus';
type MenuItem = { type MenuItem = {
@@ -43,10 +43,11 @@ type MenuItem = {
icon?: string; icon?: string;
disabled?: boolean; disabled?: boolean;
}; };
defineProps<{ defineProps<{
menuItems: MenuItem[]; menuItems: MenuItem[];
menuWidth: string | number | Array<string | number>;
}>(); }>();
const mediaQuery = window.matchMedia('(max-width: 800px)');
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true'); const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold'); const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const disBtn = ref<boolean>(false); const disBtn = ref<boolean>(false);
@@ -57,12 +58,10 @@ const changeCollapsed = (): void => {
localStorage.setItem('sidebar-collapsed', collapsed.value.toString()); localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
}; };
watch(collapsed, (newValue, oldValue) => { watch(collapsed, (newValue, oldValue) => {
setTimeout(() => { emitter.emit('sendMenu', collapsed.value);
emitter.emit('sendMenu', collapsed.value);
}, 300);
}); });
onMounted(() => { onMounted(() => {
const mediaQuery = window.matchMedia('(max-width: 800px)'); emitter.emit('sendMenu', collapsed.value);
const handleMediaChange = (e: MediaQueryListEvent) => { const handleMediaChange = (e: MediaQueryListEvent) => {
disBtn.value = e.matches; disBtn.value = e.matches;
if (e.matches) { if (e.matches) {

View File

@@ -0,0 +1,35 @@
<template>
<t-head-menu theme="light" class="bottom-menu">
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<t-tooltip :content="item.label" placement="top">
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
<template #icon>
<t-icon :name="item.icon" />
</template>
<!-- {{item.label}}-->
</t-menu-item>
</t-tooltip>
</router-link>
</t-head-menu>
</template>
<script setup lang="ts">
type MenuItem = {
value: string;
label: string;
route: string;
icon?: string;
disabled?: boolean;
};
defineProps<{
menuItems: MenuItem[];
}>();
</script>
<style scoped>
.bottom-menu {
display: flex;
justify-content: center;
border-top: 0.8px solid #ddd;
}
</style>

View File

@@ -3,4 +3,11 @@
src: url('../assets/Sotheby.ttf') format('truetype'); src: url('../assets/Sotheby.ttf') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face {
font-family: 'ProtoNerdFontItalic';
src: url('../assets/0xProtoNerdFont-Italic.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@@ -40,8 +40,13 @@ import {
Aside as TAside, Aside as TAside,
Popconfirm as Tpopconfirm, Popconfirm as Tpopconfirm,
Empty as TEmpty, Empty as TEmpty,
Dropdown as TDropdown,
Typography as TTypographyText,
TreeSelect as TTreeSelect,
Loading as TLoading,
HeadMenu as THeadMenu
} from 'tdesign-vue-next'; } from 'tdesign-vue-next';
import { router } from './router'; import router from './router';
import 'tdesign-vue-next/es/style/index.css'; import 'tdesign-vue-next/es/style/index.css';
const app = createApp(App); const app = createApp(App);
app.use(router); app.use(router);
@@ -84,4 +89,9 @@ app.use(TFooter);
app.use(TAside); app.use(TAside);
app.use(Tpopconfirm); app.use(Tpopconfirm);
app.use(TEmpty); app.use(TEmpty);
app.use(TDropdown);
app.use(TTypographyText);
app.use(TTreeSelect);
app.use(TLoading);
app.use(THeadMenu);
app.mount('#app'); app.mount('#app');

View File

@@ -1,23 +1,101 @@
<template> <template>
<div class="about-us"> <div class="about-us">
<div> <div>
<t-divider content="面板关于信息" align="left" /> <t-divider content="面板关于信息" align="left">
<t-alert theme="success" message="NapCat.WebUi is running" /> <template #content>
<t-list class="list"> <div style="display: flex; justify-content: center; align-items: center">
<t-list-item class="list-item"> <info-circle-icon></info-circle-icon>
<span class="item-label">开发人员:</span> <div style="margin-left: 5px">面板关于信息</div>
</div>
</template>
</t-divider>
<t-alert theme="success" class="header" message="NapCat.WebUi is running" />
<t-list>
<t-list-item>
<div class="label-box">
<star-filled-icon class="item-icon" size="large" />
<span class="item-label">Star:</span>
</div>
<span class="item-content"> <span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link> <t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/stargazers">{{
githubBastData?.stargazers_count
}}</t-link>
</span> </span>
</t-list-item> </t-list-item>
<t-list-item class="list-item"> <t-list-item>
<span class="item-label">版本信息:</span> <tips-filled-icon class="item-icon" size="large" />
<span class="item-label">issues:</span>
<span class="item-content"> <span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: {{ pkg.version }} </t-tag> <t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/issues">{{
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag> githubBastData?.open_issues_count
<t-tag class="tag-item" theme="success"> }}</t-link>
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </span>
</t-list-item>
<t-list-item>
<git-pull-request-filled-icon class="item-icon" size="large" />
<span class="item-label">Pull Requests:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/pulls">{{githubPullData?.length
}}</t-link>
</span>
</t-list-item>
<t-list-item >
<bookmark-add-filled-icon class="item-icon" size="large" />
<span class="item-label">Releases:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/releases">{{
githubReleasesData&&githubReleasesData[0]?timeDifference(githubReleasesData[0].published_at) + '前更新':''
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<usergroup-filled-icon class="item-icon" size="large" />
<span class="item-label">Contributors:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/graphs/contributors">{{githubContributorsData?.length}}</t-link>
</span>
</t-list-item>
<t-list-item>
<browse-filled-icon class="item-icon" size="large" />
<span class="item-label">Watchers:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/watchers">{{
githubBastData?.watchers
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<fork-filled-icon class="item-icon" size="large" />
<span class="item-label">Fork:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ/fork">{{
githubBastData?.forks_count
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<statue-of-jesus-filled-icon class="item-icon" size="large" />
<span class="item-label">License:</span>
<span class="item-content">
<t-link class="link-text" href="https://github.com/NapNeko/NapCatQQ#License-1-ov-file">{{
githubBastData?.license.key
}}</t-link>
</span>
</t-list-item>
<t-list-item>
<component-layout-filled-icon class="item-icon" size="large" />
<span class="item-label">Version:</span>
<span class="item-content">
<t-tag class="tag-item pgk-color"> WebUi: {{ pkg.version }} </t-tag>
<t-tag class="tag-item nc-color">
NapCat:
{{ napCatVersion }}
</t-tag> </t-tag>
<t-tag v-if="githubReleasesData&&githubReleasesData[0] ?.tag_name" class="tag-item nc-color">
New NapCat:
{{ githubReleasesData[0].tag_name }}
</t-tag>
<t-tag class="tag-item td-color"> TDesign: {{ pkg.dependencies['tdesign-vue-next'] }} </t-tag>
</span> </span>
</t-list-item> </t-list-item>
</t-list> </t-list>
@@ -28,6 +106,51 @@
<script setup lang="ts"> <script setup lang="ts">
import pkg from '../../package.json'; import pkg from '../../package.json';
import { napCatVersion } from '../../../src/common/version'; import { napCatVersion } from '../../../src/common/version';
import {
InfoCircleIcon,
TipsFilledIcon,
StarFilledIcon,
GitPullRequestFilledIcon,
ForkFilledIcon,
StatueOfJesusFilledIcon,
BookmarkAddFilledIcon,
UsergroupFilledIcon,
BrowseFilledIcon,
ComponentLayoutFilledIcon,
} from 'tdesign-icons-vue-next';
import { githubApiManager } from '@/backend/githubApi';
import { onMounted, ref } from 'vue';
const githubApi = new githubApiManager();
const githubBastData = ref<any>(null);
const githubReleasesData = ref<any>(null);
const githubContributorsData = ref<any>(null);
const githubPullData = ref<any>(null);
const getBaseData = async () => {
githubBastData.value = await githubApi.GetBaseData();
githubReleasesData.value = await githubApi.GetReleasesData();
githubContributorsData.value = await githubApi.GetContributors();
githubPullData.value = await githubApi.GetPullsData();
};
const timeDifference = (timestamp: string): string => {
const givenTime = new Date(timestamp);
const currentTime = new Date();
const diffInMilliseconds = currentTime.getTime() - givenTime.getTime();
const seconds = Math.floor(diffInMilliseconds / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}小时`;
} else if (minutes > 0) {
return `${minutes}分钟`;
} else {
return `${seconds}`;
}
};
onMounted(() => {
getBaseData();
});
</script> </script>
<style scoped> <style scoped>
@@ -35,23 +158,26 @@ import { napCatVersion } from '../../../src/common/version';
padding: 20px; padding: 20px;
text-align: left; text-align: left;
} }
.label-box {
.list {
display: flex; display: flex;
flex-direction: column; justify-content: center;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center; align-items: center;
} }
.item-icon {
padding: 5px;
color: #ffffff;
border-radius: 3px;
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
}
.item-label { .item-label {
flex: 1; flex: 1;
font-weight: bold; margin-left: 8px;
box-sizing: border-box;
height: auto;
padding: 0;
border: none;
font-size: 16px;
} }
.item-content { .item-content {
flex: 2; flex: 2;
display: flex; display: flex;
@@ -64,3 +190,37 @@ import { napCatVersion } from '../../../src/common/version';
margin-bottom: 10px; margin-bottom: 10px;
} }
</style> </style>
<style>
.t-list-item {
padding: 5px var(--td-comp-paddingLR-l);
}
.item-label {
flex: 2;
background-image: linear-gradient(to right, #fa709a 0%, #fee140 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.pgk-color {
color: white;
background-image: linear-gradient(-225deg, #9be15d 0%, #00e3ae 100%);
}
.nc-color {
color: white;
background-image: linear-gradient(-225deg, #2cd8d5 0%, #c5c1ff 56%, #ffbac3 100%);
}
.td-color {
color: white;
background-image: linear-gradient(225deg, #0acffe 0%, #495aff 100%);
}
.header {
background-image: linear-gradient(225deg, #dfffcd 0%, #90f9c4 48%, #39f3bb 100%) !important;
}
.link-text{
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #B6CEE8 0%, #F578DC 100%);
font-weight: bold;
}
</style>

View File

@@ -1,6 +1,600 @@
<template> <template>
<div class="log-view"> <div class="title">
<h1>面板日志信息</h1> <t-divider content="日志查看" align="left">
<p>这里显示面板的日志信息</p> <template #content>
<div style="display: flex; justify-content: center; align-items: center">
<system-log-icon></system-log-icon>
<div style="margin-left: 5px">日志查看</div>
</div>
</template>
</t-divider>
</div> </div>
<div class="tab-box">
<t-tabs default-value="realtime" @change="selectType">
<t-tab-panel value="realtime" label="实时日志"></t-tab-panel>
<t-tab-panel value="history" label="历史日志"></t-tab-panel>
</t-tabs>
</div>
<div class="card-box">
<t-card class="card" :bordered="true">
<template #actions>
<t-row :align="'middle'" justify="center" :style="{ gap: smallScreen.matches ? '5px' : '24px' }">
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<t-tooltip content="清理日志">
<t-button variant="text" shape="square" @click="clearLogs">
<clear-icon></clear-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<t-tooltip content="下载日志">
<t-button variant="text" shape="square" @click="downloadText">
<download-icon></download-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col
v-if="LogDataType === 'history'"
flex="auto"
style="display: inline-flex; justify-content: center">
<t-tooltip content="历史日志">
<t-button variant="text" shape="square" @click="historyLog">
<history-icon></history-icon>
</t-button>
</t-tooltip>
</t-col>
<t-col flex="auto" style="display: inline-flex; justify-content: center">
<div class="tag-box">
<t-tag class="t-tag" :style="{ backgroundImage: typeKey[optValue.description] }">{{
optValue.content }}</t-tag>
</div>
<t-dropdown :options="options" :min-column-width="112" @click="openTypeList">
<t-button variant="text" shape="square">
<more-icon />
</t-button>
</t-dropdown>
</t-col>
</t-row>
</template>
<template #content>
<div class="content" ref="contentBox">
<div v-for="item in LogDataType === 'realtime'
? realtimeLogHtmlList.get(optValue.description)
: historyLogHtmlList.get(optValue.description)">
<span>{{ item.time }}</span><span :id="item.type">{{ item.content }}</span>
</div>
</div>
</template>
</t-card>
</div>
<t-dialog v-model:visible="visibleBody" header="历史日志" :destroy-on-close="true" :show-in-attached-element="true"
:on-confirm="GetLogList" class=".t-dialog__ctx .t-dialog__position">
<t-select v-model="value" :options="logFileData" placeholder="请选择日志" :multiple="true"
style="text-align: left" />
</t-dialog>
</template> </template>
<script setup lang="ts">
import { MoreIcon, ClearIcon, DownloadIcon, HistoryIcon, SystemLogIcon } from 'tdesign-icons-vue-next';
import { nextTick, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { LogManager } from '@/backend/log';
import { MessagePlugin } from 'tdesign-vue-next';
import { EventSourcePolyfill } from 'event-source-polyfill';
const smallScreen = window.matchMedia('(max-width: 768px)');
const LogDataType = ref<string>('realtime');
const visibleBody = ref<boolean>(false);
const contentBox = ref<HTMLElement | null>(null);
let isMouseEntered = false;
const logManager = new LogManager(localStorage.getItem('auth') || '');
const eventSource = ref<EventSourcePolyfill | null>(null);
const intervalId = ref<number | null>(null);
const isPaused = ref(false);
interface OptionItem {
content: string;
value: number;
description: string;
}
const options = ref<OptionItem[]>([
{
content: '全部',
value: 1,
description: 'all',
},
{
content: '调试',
value: 2,
description: 'debug',
},
{
content: '提示',
value: 3,
description: 'info',
},
{
content: '警告',
value: 4,
description: 'warn',
},
{
content: '错误',
value: 5,
description: 'error',
},
{
content: '致命',
value: 5,
description: 'fatal',
},
]);
const typeKey = ref<Record<string, string>>({
all: 'linear-gradient(60deg,#16a085 0%, #f4d03f 100%)',
debug: 'linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%)',
info: 'linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%)',
warn: 'linear-gradient(to right, #e14fad 0%, #f9d423 48%, #e37318 100%)',
error: 'linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%)',
fatal: 'linear-gradient(-225deg, #fd0700, #ec567f)',
});
interface logHtml {
type?: string;
content: string;
color?: string;
time?: string;
}
type LogHtmlMap = Map<string, logHtml[]>;
const realtimeLogHtmlList = ref<LogHtmlMap>(
new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
])
);
const historyLogHtmlList = ref<LogHtmlMap>(
new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
])
);
const logFileData = ref<{ label: string; value: string }[]>([]);
const value = ref([]);
const optValue = ref<OptionItem>({
content: '全部',
value: 1,
description: 'all',
});
const openTypeList = (data: OptionItem) => {
optValue.value = data;
};
const logType = ['debug', 'info', 'warn', 'error', 'fatal'];
//清理log
const clearLogs = () => {
if (LogDataType.value === 'realtime') {
clearAllLogs(realtimeLogHtmlList);
} else {
clearAllLogs(historyLogHtmlList);
}
};
const clearAllLogs = (logList: Ref<Map<string, Array<logHtml>>>) => {
if ((optValue.value && optValue.value.description === 'all') || !optValue.value) {
logList.value = new Map([
['all', []],
['debug', []],
['info', []],
['warn', []],
['error', []],
['fatal', []],
]);
} else {
logList.value.set(optValue.value.description, []);
}
};
//定时清理log
const TimerClear = () => {
clearAllLogs(realtimeLogHtmlList);
};
const startTimer = () => {
if (!isPaused.value) {
intervalId.value = window.setInterval(TimerClear, 0.5 * 60 * 1000);
}
};
const pauseTimer = () => {
if (intervalId.value) {
window.clearInterval(intervalId.value);
isPaused.value = true;
}
};
const resumeTimer = () => {
if (isPaused.value) {
startTimer();
isPaused.value = false;
}
};
const stopTimer = () => {
if (intervalId.value) {
window.clearInterval(intervalId.value);
intervalId.value = null;
}
};
const extractContent = (text: string): string | null => {
const regex = /\[([^\]]+)]/;
const match = regex.exec(text);
if (match && match[1]) {
const extracted = match[1].toLowerCase();
if (logType.includes(extracted)) {
return match[1];
}
}
return null;
};
const loadData = (text: string, loadType: string) => {
const lines = text.split(/\r\n/);
lines.forEach((line) => {
if (loadType === 'realtime') {
let remoteJson = JSON.parse(line) as { message: string, level: string };
const type = remoteJson.level;
const actualType = type || 'other';
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
const data: logHtml = {
type: actualType,
content: remoteJson.message,
color: color,
time: '',
};
updateLogList(realtimeLogHtmlList, actualType, data);
} else if (loadType === 'history') {
const type = extractContent(line);
const actualType = type || 'other';
const timeRegex = /(\d{2}-\d{2} \d{2}:\d{2}:\d{2})|(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/;
const match = timeRegex.exec(line);
let time = match ? match[0] : null;
const color = actualType && typeKey.value[actualType] ? typeKey.value[actualType] : undefined;
const data: logHtml = {
type: actualType,
content: line.slice(match ? match[0].length : 0) || '',
color: color,
time: time ? time + ' ' : '',
};
updateLogList(historyLogHtmlList, actualType, data);
}
});
};
const updateLogList = (logList: Ref<Map<string, Array<logHtml>>>, actualType: string, data: logHtml) => {
const allLogs = logList.value.get('all');
if (Array.isArray(allLogs)) {
allLogs.push(data);
}
if (actualType !== 'other') {
const typeLogs = logList.value.get(actualType);
if (Array.isArray(typeLogs)) {
typeLogs.push(data);
}
}
};
const selectType = (key: string) => {
LogDataType.value = key;
};
interface CustomURL extends URL {
recycleObjectURL: (url: string) => void;
}
const isCompatibleWithCustomURL = (obj: any): obj is CustomURL => {
return typeof obj === 'object' && obj !== null && typeof (obj as any).recycleObjectURL === 'function';
};
const recycleURL = (url: string) => {
if (isCompatibleWithCustomURL(window.URL)) {
const customURL = window.URL as CustomURL;
customURL.recycleObjectURL(url);
}
};
const generateTXT = (textContent: string, fileName: string) => {
try {
const blob = new Blob([textContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
recycleURL(url);
} catch (error) {
console.error('下载文本时出现错误:', error);
}
};
const downloadText = () => {
if (LogDataType.value === 'realtime') {
const logs = realtimeLogHtmlList.value.get(optValue.value.description);
if (logs && logs.length > 0) {
const result = logs.map((obj) => obj.content).join('\r\n');
generateTXT(result, '实时日志');
} else {
MessagePlugin.error('暂无可下载日志');
}
} else {
const logs = historyLogHtmlList.value.get(optValue.value.description);
if (logs && logs.length > 0) {
const result = logs.map((obj) => obj.content).join('\r\n');
generateTXT(result, '历史日志');
} else {
MessagePlugin.error('暂无可下载日志');
}
}
};
const historyLog = async () => {
value.value = [];
visibleBody.value = true;
const res = await logManager.GetLogList();
clearAllLogs(historyLogHtmlList);
if (res.length > 0) {
logFileData.value = res.map((ele: string) => {
return { label: ele, value: ele };
});
} else {
logFileData.value = [];
}
};
const GetLogList = async () => {
if (value.value.length > 0) {
for (const ele of value.value) {
try {
const data = await logManager.GetLog(ele);
if (data && data !== 'null') {
loadData(data, 'history');
}
} catch (error) {
console.error(`获取日志 ${ele} 时出现错误:`, error);
}
}
visibleBody.value = false;
} else {
MessagePlugin.error('请选择日志');
}
};
const fetchRealTimeLogs = async () => {
eventSource.value = await logManager.getRealTimeLogs();
if (eventSource.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
eventSource.value.onmessage = (event: MessageEvent) => {
console.log(event.data)
loadData(event.data, 'realtime');
};
}
};
const closeRealTimeLogs = async () => {
if (eventSource.value) {
eventSource.value.close();
}
};
const scrollToBottom = () => {
if (!isMouseEntered) {
nextTick(() => {
if (contentBox.value) {
contentBox.value.scrollTop = contentBox.value.scrollHeight;
}
});
}
};
const observeDOMChanges = () => {
if (contentBox.value) {
const observer = new MutationObserver(() => {
scrollToBottom();
});
observer.observe(contentBox.value, {
childList: true,
subtree: true,
});
}
};
const showScrollbar = () => {
if (contentBox.value) {
contentBox.value.style.overflow = 'auto';
}
};
const hideScrollbar = () => {
if (contentBox.value) {
contentBox.value.style.overflow = 'hidden';
if (!isMouseEntered) {
scrollToBottom();
}
}
};
watch(
realtimeLogHtmlList,
() => {
if (!isMouseEntered) {
scrollToBottom();
}
},
{ immediate: true }
);
watch(
historyLogHtmlList,
() => {
if (!isMouseEntered) {
scrollToBottom();
}
},
{ immediate: true }
);
onMounted(() => {
fetchRealTimeLogs();
startTimer();
contentBox.value = document.querySelector('.content');
if (contentBox.value) {
contentBox.value.style.overflow = 'hidden';
contentBox.value.addEventListener('mouseenter', () => {
isMouseEntered = true;
showScrollbar();
pauseTimer();
});
contentBox.value.addEventListener('mouseleave', () => {
isMouseEntered = false;
hideScrollbar();
resumeTimer();
setTimeout(() => {
scrollToBottom();
}, 1000);
});
observeDOMChanges();
}
});
onUnmounted(() => {
closeRealTimeLogs();
stopTimer();
});
</script>
<style scoped>
.title {
padding: 20px 20px 0 20px;
}
.tab-box {
margin: 0 20px;
}
.card-box {
margin: 10px 20px;
}
.content {
height: 56vh;
background-image: url('@/assets/logo.png');
border: 1px solid #ddd6d6 !important;
padding: 5px 10px;
text-align: left;
overflow-y: auto;
margin-top: -10px;
font-family: monospace;
font-size: 15px;
line-height: 16px;
}
.content span {
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: break-word;
}
@keyframes fadeInOnce {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOutOnce {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.content div {
animation: fadeInOnce 0.5s forwards;
}
::-webkit-scrollbar {
width: 5px;
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #888888;
border-radius: 4px;
}
.tag-box {
display: flex;
justify-content: center;
align-items: center;
margin-right: 5px;
}
.t-tag {
min-width: 60px;
text-align: center;
display: flex;
justify-content: center;
color: white;
font-weight: 500;
}
#debug {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #5271c4 0%, #b19fff 48%, #eca1fe 100%);
}
#info {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(-225deg, #22e1ff 0%, #1d8fe1 48%, #625eb1 100%);
}
#warn {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(225deg, #e14fad 0%, #f9d423 48%, #e37318 100%);
}
#error {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to left, #ffe29f 0%, #ffa99f 48%, #d94541 100%);
}
#fatal {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to right, #fd0700, #ec567f);
}
#other {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%);
}
@media (max-width: 786px) {
.content {
height: 50vh;
font-family: ProtoNerdFontItalic, monospace;
font-size: 12px;
line-height: 14.3px;
}
}
</style>
<style>
.card {
padding: 5px 10px 20px 10px !important;
}
@media (max-width: 786px) {
.card {
padding: 0 !important;
}
}
</style>

View File

@@ -1,10 +1,18 @@
<template> <template>
<div ref="headerBox" class="title"> <div ref="headerBox" class="title">
<t-divider content="网络配置" align="left" /> <t-divider content="网络配置" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<wifi1-icon />
<div style="margin-left: 5px">网络配置</div>
</div>
</template>
</t-divider>
<t-divider align="right"> <t-divider align="right">
<t-button @click="addConfig()"> <t-button @click="addConfig()">
<template #icon><add-icon /></template> <template #icon><add-icon /></template>
添加配置</t-button> 添加配置</t-button
>
</t-divider> </t-divider>
</div> </div>
<div v-if="loadPage" ref="setting" class="setting"> <div v-if="loadPage" ref="setting" class="setting">
@@ -16,86 +24,142 @@
<t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel> <t-tab-panel value="websocketClients" label="WebSocket 客户端"></t-tab-panel>
</t-tabs> </t-tabs>
</div> </div>
<t-loading attach="#alice" :loading="!loadPage" :showOverlay="false">
<div id="alice" v-if="!loadPage" style="height: 80vh;position: relative" ></div>
</t-loading>
<div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }"> <div v-if="loadPage" class="card-box" :style="{ width: tabsWidth + 'px' }">
<div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0"> <div class="setting-box" :style="{ maxHeight: cardHeight + 'px' }" v-if="cardConfig.length > 0">
<div v-for="(item, index) in cardConfig" :key="index"> <div v-for="(item, index) in cardConfig" :key="index">
<t-card :title="item.name" :description="item.type" :style="{ width: cardWidth + 'px' }" <t-card
:header-bordered="true" class="setting-card"> :title="item.name"
:description="item.type"
:style="{ width: cardWidth + 'px' }"
:header-bordered="true"
class="setting-card"
>
<template #actions> <template #actions>
<t-space> <t-space>
<edit2-icon size="20px" @click="editConfig(item)"></edit2-icon> <edit2-icon size="20px" @click="editConfig(item)"></edit2-icon>
<t-popconfirm theme="danger" content="确认删除" @confirm="delConfig(item)"> <t-popconfirm content="确认删除" @confirm="delConfig(item)">
<delete-icon size="20px"></delete-icon> <delete-icon size="20px"></delete-icon>
</t-popconfirm> </t-popconfirm>
</t-space> </t-space>
</template> </template>
<div class="setting-content"> <div class="setting-content">
<t-card class="card-address" :style="{ <t-card
borderLeft: '7px solid ' + (item.enable ? class="card-address"
'var(--td-success-color)' : :style="{
'var(--td-error-color)') 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> <div class="local-box" v-if="item.host && item.port">
<copy-icon class="copy-icon" size="20px" @click="copyText(item.host + ':' + item.port)"></copy-icon> <server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
</div> <strong class="local">{{ item.host }}:{{ item.port }}</strong>
<div class="local-box" v-if="item.url"> <copy-icon
<server-filled-icon class="local-icon" size="20px"></server-filled-icon> class="copy-icon"
<strong class="local" >{{ item.url }}</strong> size="20px"
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon> @click="copyText(item.host + ':' + item.port)"
</div> ></copy-icon>
</div>
<div class="local-box" v-if="item.url">
<server-filled-icon class="local-icon" size="20px" @click="toggleProperty(item, 'enable')"></server-filled-icon>
<strong class="local">{{ item.url }}</strong>
<copy-icon class="copy-icon" size="20px" @click="copyText(item.url)"></copy-icon>
</div>
</t-card> </t-card>
<t-collapse :default-value="[0]" expand-mutex style="margin-top:10px;" class="info-coll"> <t-collapse :default-value="[0]" expand-mutex style="margin-top: 10px" class="info-coll">
<t-collapse-panel header="基础信息"> <t-collapse-panel header="基础信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'" <t-descriptions
class="setting-base-info"> size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions-item v-if="item.token" label="连接密钥"> <t-descriptions-item v-if="item.token" label="连接密钥">
<div v-if="mediumScreen.matches||largeScreen.matches" class="token-view"> <div v-if="mediumScreen.matches || largeScreen.matches" class="token-view">
<span>{{ showToken ? item.token : '******' }}</span> <span>{{ showToken ? item.token : '******' }}</span>
<browse-icon class="browse-icon" v-if="showToken" size="18px" <browse-icon
@click="showToken = false"></browse-icon> class="browse-icon"
<browse-off-icon class="browse-icon" v-else size="18px" v-if="showToken"
@click="showToken = true"></browse-off-icon> 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>
<div v-else> <div v-else>
<t-popup :showArrow="true" trigger="click"> <t-popup :showArrow="true" trigger="click">
<t-tag theme="primary">点击查看</t-tag> <t-tag theme="primary">点击查看</t-tag>
<template #content> <template #content>
<div @click="copyText(item.token)">{{item.token}}</div> <div @click="copyText(item.token)">{{ item.token }}</div>
</template> </template>
</t-popup> </t-popup>
</div> </div>
</t-descriptions-item> </t-descriptions-item>
<t-descriptions-item label="消息格式">{{ item.messagePostFormat }}</t-descriptions-item> <t-descriptions-item label="消息格式">{{
item.messagePostFormat
}}</t-descriptions-item>
</t-descriptions> </t-descriptions>
</t-collapse-panel> </t-collapse-panel>
<t-collapse-panel header="状态信息"> <t-collapse-panel header="状态信息">
<t-descriptions size="small" :layout="infoOneCol ? 'vertical' : 'horizontal'" <t-descriptions
class="setting-base-info"> size="small"
:layout="infoOneCol ? 'vertical' : 'horizontal'"
class="setting-base-info"
>
<t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志"> <t-descriptions-item v-if="item.hasOwnProperty('debug')" label="调试日志">
<t-tag class="tag-item" :theme="item.debug ? 'success' : 'danger'"> <t-tag
{{ item.debug ? '开启' : '关闭' }}</t-tag> :class="item.debug ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'debug')"
>
{{ item.debug ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item> </t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableWebsocket')" <t-descriptions-item
label="Websocket 功能"> v-if="item.hasOwnProperty('enableWebsocket')"
<t-tag class="tag-item" :theme="item.enableWebsocket ? 'success' : 'danger'"> label="Websocket 功能"
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag> >
<t-tag
:class="item.enableWebsocket ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableWebsocket')"
>
{{ item.enableWebsocket ? '启用' : '禁用' }}</t-tag
>
</t-descriptions-item> </t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableCors')" label="跨域放行"> <t-descriptions-item
<t-tag class="tag-item" :theme="item.enableCors ? 'success' : 'danger'"> v-if="item.hasOwnProperty('enableCors')"
{{ item.enableCors ? '开启' : '关闭' }}</t-tag> label="跨域放行"
>
<t-tag :class="item.enableCors ? 'tag-item-on' : 'tag-item-off'" @click="toggleProperty(item, 'enableCors')">
{{ item.enableCors ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item> </t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')" <t-descriptions-item
label="上报自身消息"> v-if="item.hasOwnProperty('enableForcePushEvent')"
<t-tag class="tag-item" :theme="item.reportSelfMessage ? 'success' : 'danger'"> label="上报自身消息"
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag> >
<t-tag
:class="item.reportSelfMessage ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'reportSelfMessage')"
>
{{ item.reportSelfMessage ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item> </t-descriptions-item>
<t-descriptions-item v-if="item.hasOwnProperty('enableForcePushEvent')" <t-descriptions-item
label="强制推送事件"> v-if="item.hasOwnProperty('enableForcePushEvent')"
<t-tag class="tag-item" label="强制推送事件"
:theme="item.enableForcePushEvent ? 'success' : 'danger'"> >
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag> <t-tag
class="tag-item"
:class="item.enableForcePushEvent ? 'tag-item-on' : 'tag-item-off'"
@click="toggleProperty(item, 'enableForcePushEvent')"
>
{{ item.enableForcePushEvent ? '开启' : '关闭' }}</t-tag
>
</t-descriptions-item> </t-descriptions-item>
</t-descriptions> </t-descriptions>
</t-collapse-panel> </t-collapse-panel>
@@ -105,20 +169,34 @@
</div> </div>
<div style="height: 20vh"></div> <div style="height: 20vh"></div>
</div> </div>
<t-card v-else> <t-card v-else>
<t-empty class="card-none" title="暂无网络配置"> </t-empty> <t-empty class="card-none" title="暂无网络配置"> </t-empty>
</t-card> </t-card>
</div> </div>
<t-dialog v-model:visible="visibleBody" :header="dialogTitle" :destroy-on-close="true" <t-dialog
:show-in-attached-element="true" placement="center" :on-confirm="saveConfig" class=".t-dialog__ctx .t-dialog--defaul"> v-model:visible="visibleBody"
<div slot="body" class="dialog-body" > :header="dialogTitle"
:destroy-on-close="true"
:show-in-attached-element="true"
:on-confirm="saveConfig"
class=".t-dialog__ctx .t-dialog__position"
>
<div slot="body" class="dialog-body">
<t-form ref="form" :data="newTab" labelAlign="left" :model="newTab"> <t-form ref="form" :data="newTab" labelAlign="left" :model="newTab">
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]" <t-form-item
label="名称" name="name"> style="text-align: left"
:rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]"
label="名称"
name="name"
>
<t-input v-model="newTab.name" /> <t-input v-model="newTab.name" />
</t-form-item> </t-form-item>
<t-form-item style="text-align: left" :rules="[{ required: true, message: '请选择类型', trigger: 'change' }]" <t-form-item
label="类型" name="type"> style="text-align: left"
:rules="[{ required: true, message: '请选择类型', trigger: 'change' }]"
label="类型"
name="type"
>
<t-select v-model="newTab.type" @change="onloadDefault"> <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>
@@ -127,8 +205,10 @@
</t-select> </t-select>
</t-form-item> </t-form-item>
<div> <div>
<component :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))" <component
:config="newTab.data" /> :is="resolveDynamicComponent(getComponent(newTab.type as ComponentKey))"
:config="newTab.data"
/>
</div> </div>
</t-form> </t-form>
</div> </div>
@@ -136,8 +216,17 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { AddIcon, DeleteIcon, Edit2Icon, ServerFilledIcon, CopyIcon, BrowseOffIcon, BrowseIcon } from 'tdesign-icons-vue-next'; import {
import { onMounted, onUnmounted, ref, resolveDynamicComponent } from 'vue'; AddIcon,
DeleteIcon,
Edit2Icon,
ServerFilledIcon,
CopyIcon,
BrowseOffIcon,
BrowseIcon,
Wifi1Icon,
} from 'tdesign-icons-vue-next';
import { onMounted, onUnmounted, ref, resolveDynamicComponent, watch } from 'vue';
import emitter from '@/ts/event-bus'; import emitter from '@/ts/event-bus';
import { import {
mergeNetworkDefaultConfig, mergeNetworkDefaultConfig,
@@ -187,7 +276,7 @@ const operateType = ref<string>('');
//配置项索引 //配置项索引
const configIndex = ref<number>(0); const configIndex = ref<number>(0);
//保存时所用数据 //保存时所用数据
const networkConfig: NetworkConfig & { [key: string]: any; } = { const networkConfig: NetworkConfig & { [key: string]: any } = {
websocketClients: [], websocketClients: [],
websocketServers: [], websocketServers: [],
httpClients: [], httpClients: [],
@@ -235,6 +324,18 @@ const editConfig = (item: any) => {
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name); configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
visibleBody.value = true; visibleBody.value = true;
}; };
const toggleProperty = async (item: any, tagData: string) => {
const type = getKeyByValue(typeCh, item.type);
const newData = { ...item };
newData[tagData] = !item[tagData];
if (type) {
newTab.value = { name: item.name, data: newData, type: type };
}
operateType.value = 'edit';
configIndex.value = networkConfig[newTab.value.type].findIndex((obj: any) => obj.name === item.name);
await saveConfig();
};
const delConfig = (item: any) => { const delConfig = (item: any) => {
const type = getKeyByValue(typeCh, item.type); const type = getKeyByValue(typeCh, item.type);
if (type) { if (type) {
@@ -252,7 +353,6 @@ const selectType = (key: ComponentKey) => {
}; };
const onloadDefault = (key: ComponentKey) => { const onloadDefault = (key: ComponentKey) => {
console.log(key);
newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]); newTab.value.data = structuredClone(mergeNetworkDefaultConfig[key]);
}; };
//检测重名 //检测重名
@@ -350,22 +450,21 @@ const loadConfig = async () => {
}; };
const copyText = async (text: string) => { const copyText = async (text: string) => {
const input = document.createElement('input'); const textarea = document.createElement('textarea');
input.value = text; textarea.value = text;
document.body.appendChild(input); document.body.appendChild(textarea);
input.select(); textarea.select();
await navigator.clipboard.writeText(text); try {
document.body.removeChild(input); document.execCommand('copy');
MessagePlugin.success('复制成功'); MessagePlugin.success('复制成功');
} catch (err) {
console.error('复制失败', err);
} finally {
document.body.removeChild(textarea);
}
}; };
const handleResize = () => { const handleResize = () => {
// 得根据卡片宽度改,懒得改了;先不管了
// if(window.innerWidth < 540) {
// infoOneCol.value= true
// } else {
// infoOneCol.value= false
// }
tabsWidth.value = window.innerWidth - 41 - menuWidth.value; tabsWidth.value = window.innerWidth - 41 - menuWidth.value;
if (mediumScreen.matches) { if (mediumScreen.matches) {
cardWidth.value = (tabsWidth.value - 20) / 2; cardWidth.value = (tabsWidth.value - 20) / 2;
@@ -375,29 +474,43 @@ const handleResize = () => {
cardWidth.value = tabsWidth.value; cardWidth.value = tabsWidth.value;
} }
loadPage.value = true; loadPage.value = true;
setTimeout(() => { setTimeout(()=>{
cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21; cardHeight.value = window.innerHeight - (headerBox.value?.offsetHeight ?? 0) - (setting.value?.offsetHeight ?? 0) - 21;
}, 300); },300)
}; };
emitter.on('sendWidth', (width) => { emitter.on('sendWidth', (width) => {
if (typeof width === 'number' && !isNaN(width)) { if (typeof width === 'string') {
menuWidth.value = width; const strWidth = width as string;
handleResize(); menuWidth.value = parseInt(strWidth);
} }
}); });
watch(menuWidth, (newValue, oldValue) => {
loadPage.value = false;
setTimeout(()=>{
handleResize();
},300)
});
onMounted(() => { onMounted(() => {
loadConfig(); loadConfig();
const cachedWidth = localStorage.getItem('menuWidth'); const cachedWidth = localStorage.getItem('menuWidth');
if (cachedWidth) { if (cachedWidth) {
menuWidth.value = parseInt(cachedWidth); menuWidth.value = parseInt(cachedWidth);
setTimeout(() => { setTimeout(()=>{
handleResize(); handleResize();
}, 300); },300)
} }
window.addEventListener('resize', handleResize); window.addEventListener('resize', ()=>{
setTimeout(()=>{
handleResize();
},300)
});
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', ()=>{
setTimeout(()=>{
handleResize();
},300)
});
}); });
</script> </script>
@@ -437,7 +550,7 @@ onUnmounted(() => {
display: flex; display: flex;
margin-top: 2px; margin-top: 2px;
} }
.local-icon{ .local-icon {
flex: 1; flex: 1;
} }
.local { .local {
@@ -448,14 +561,12 @@ onUnmounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.copy-icon { .copy-icon {
flex: 1; flex: 1;
cursor: pointer; cursor: pointer;
flex-direction: row; flex-direction: row;
} }
.token-view { .token-view {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -467,11 +578,22 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.browse-icon{
.tag-item-on{
color: white;
cursor: pointer;
background-image: linear-gradient(to top, #0ba360 0%, #3cba92 100%) !important;
}
.tag-item-off{
color: white;
cursor: pointer;
background-image: linear-gradient(to top, rgba(255, 8, 68, 0.93) 0%, #D54941 100%) !important;
}
.browse-icon {
flex: 2; flex: 2;
} }
:global(.t-dialog__ctx .t-dialog--defaul) { :global(.t-dialog__ctx .t-dialog__position) {
margin: 0 20px; padding: 48px 10px;
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.setting-box { .setting-box {
@@ -483,7 +605,6 @@ onUnmounted(() => {
.setting-box { .setting-box {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.card-box { .card-box {
@@ -494,9 +615,8 @@ onUnmounted(() => {
line-height: 400px !important; line-height: 400px !important;
} }
.dialog-body { .dialog-body {
max-height: 60vh; max-height: 50vh;
overflow-y: auto; overflow-y: auto;
} }
@@ -515,12 +635,6 @@ onUnmounted(() => {
font-size: 12px; font-size: 12px;
} }
.card-address .t-card__body {
display: flex;
flex-direction: row;
align-items: center;
}
.setting-base-info .t-descriptions__header { .setting-base-info .t-descriptions__header {
font-size: 15px; font-size: 15px;
margin-bottom: 0; margin-bottom: 0;
@@ -530,7 +644,7 @@ onUnmounted(() => {
padding: 0 var(--td-comp-paddingLR-l) !important; padding: 0 var(--td-comp-paddingLR-l) !important;
} }
.setting-base-info tr>td:last-child { .setting-base-info tr > td:last-child {
text-align: right; text-align: right;
} }

View File

@@ -1,6 +1,13 @@
<template> <template>
<div class="title"> <div class="title">
<t-divider content="其余配置" align="left" /> <t-divider content="其余配置" align="left">
<template #content>
<div style="display: flex; justify-content: center; align-items: center">
<setting-icon />
<div style="margin-left: 5px">其余配置</div>
</div>
</template>
</t-divider>
</div> </div>
<t-card class="card"> <t-card class="card">
<div class="other-config-container"> <div class="other-config-container">
@@ -29,11 +36,12 @@ import { ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next'; import { MessagePlugin } from 'tdesign-vue-next';
import { OneBotConfig } from '../../../src/onebot/config/config'; import { OneBotConfig } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell'; import { QQLoginManager } from '@/backend/shell';
import { SettingIcon } from 'tdesign-icons-vue-next';
const otherConfig = ref<Partial<OneBotConfig>>({ const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '', musicSignUrl: '',
enableLocalFile2Url: false, enableLocalFile2Url: false,
parseMultMsg: true parseMultMsg: true,
}); });
const labelAlign = ref<string>(); const labelAlign = ref<string>();

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-switch v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="URL"> <t-form-item label="URL">
<t-input v-model="config.url" /> <t-input v-model="config.url" />
@@ -11,20 +11,20 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="报告自身消息"> <t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" /> <t-switch v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-checkbox v-model="config.debug" /> <t-switch v-model="config.debug" />
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config'; import { HttpClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-switch v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="端口"> <t-form-item label="端口">
<t-input v-model.number="config.port" type="number" /> <t-input v-model.number="config.port" type="number" />
@@ -11,10 +11,10 @@
<t-input v-model="config.host" type="text" /> <t-input v-model="config.host" type="text" />
</t-form-item> </t-form-item>
<t-form-item label="启用 CORS"> <t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" /> <t-switch v-model="config.enableCors" />
</t-form-item> </t-form-item>
<t-form-item label="启用 WS"> <t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" /> <t-switch v-model="config.enableWebsocket" />
</t-form-item> </t-form-item>
<t-form-item label="消息格式"> <t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
@@ -23,14 +23,14 @@
<t-input v-model="config.token" type="text" /> <t-input v-model="config.token" type="text" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-checkbox v-model="config.debug" /> <t-switch v-model="config.debug" />
</t-form-item> </t-form-item>
</t-form> </t-form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config'; import { HttpServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-switch v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="URL"> <t-form-item label="URL">
<t-input v-model="config.url" /> <t-input v-model="config.url" />
@@ -11,13 +11,13 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="报告自身消息"> <t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" /> <t-switch v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-checkbox v-model="config.debug" /> <t-switch v-model="config.debug" />
</t-form-item> </t-form-item>
<t-form-item label="心跳间隔"> <t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" /> <t-input v-model.number="config.heartInterval" type="number" />
@@ -27,7 +27,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config'; import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -2,7 +2,7 @@
<div> <div>
<t-form labelAlign="left"> <t-form labelAlign="left">
<t-form-item label="启用"> <t-form-item label="启用">
<t-checkbox v-model="config.enable" /> <t-switch v-model="config.enable" />
</t-form-item> </t-form-item>
<t-form-item label="主机"> <t-form-item label="主机">
<t-input v-model="config.host" /> <t-input v-model="config.host" />
@@ -14,16 +14,16 @@
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" /> <t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item> </t-form-item>
<t-form-item label="上报自身消息"> <t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" /> <t-switch v-model="config.reportSelfMessage" />
</t-form-item> </t-form-item>
<t-form-item label="Token"> <t-form-item label="Token">
<t-input v-model="config.token" /> <t-input v-model="config.token" />
</t-form-item> </t-form-item>
<t-form-item label="强制推送事件"> <t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" /> <t-switch v-model="config.enableForcePushEvent" />
</t-form-item> </t-form-item>
<t-form-item label="调试模式"> <t-form-item label="调试模式">
<t-checkbox v-model="config.debug" /> <t-switch v-model="config.debug" />
</t-form-item> </t-form-item>
<t-form-item label="心跳间隔"> <t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" /> <t-input v-model.number="config.heartInterval" type="number" />
@@ -33,7 +33,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config'; import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{ const props = defineProps<{

View File

@@ -7,6 +7,8 @@ import NetWork from '../pages/NetWork.vue';
import QQLogin from '../components/QQLogin.vue'; import QQLogin from '../components/QQLogin.vue';
import WebUiLogin from '../components/WebUiLogin.vue'; import WebUiLogin from '../components/WebUiLogin.vue';
import OtherConfig from '../pages/OtherConfig.vue'; import OtherConfig from '../pages/OtherConfig.vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/webui' }, { path: '/', redirect: '/webui' },
@@ -26,7 +28,27 @@ const routes: Array<RouteRecordRaw> = [
}, },
]; ];
export const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes, routes,
}); });
router.beforeEach(async (to, from, next) => {
const isPublicRoute = ['/webui', '/qqlogin'].includes(to.path);
const token = localStorage.getItem('auth');
if (!isPublicRoute) {
if (!token) {
MessagePlugin.error('请先登录');
return next('/webui');
}
const login = await new QQLoginManager(token).checkWebUiLogined();
if (!login) {
MessagePlugin.error('请先登录');
return next('/webui');
}
}
next();
});
export default router;

View File

@@ -3,22 +3,15 @@
"target": "ESNext", "target": "ESNext",
"jsx": "preserve", "jsx": "preserve",
"jsxImportSource": "vue", "jsxImportSource": "vue",
"lib": [ "lib": ["DOM", "DOM.Iterable", "ES2022"],
"DOM",
"DOM.Iterable"
],
"baseUrl": ".", "baseUrl": ".",
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"paths": { "paths": {
"@/*": [ "@/*": ["src/*"]
"src/*"
]
}, },
"resolveJsonModule": true, "resolveJsonModule": true,
"types": [ "types": ["vite/client"],
"vite/client"
],
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -30,5 +23,5 @@
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules"], "exclude": ["node_modules"],
"references": [{"path": "./tsconfig.node.json"}] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@@ -2,27 +2,32 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.2.0", "version": "4.2.68",
"scripts": { "scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1", "build:framework": "npm run build:webui && vite build --mode framework || exit 1",
"build:shell": "npm run build:webui && vite build --mode shell || exit 1", "build:shell": "npm run build:webui && vite build --mode shell || exit 1",
"build:webui": "cd napcat.webui && vite build", "build:webui": "cd napcat.webui && vite build",
"dev:universal": "vite build --mode universal",
"dev:framework": "vite build --mode framework", "dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell", "dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev", "dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}", "lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev" "depend": "cd dist && npm install --omit=dev",
"dev:depend": "npm i && cd napcat.webui && npm i"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "0.24.0",
"@babel/preset-typescript": "^7.24.7", "@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2", "@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0", "@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2", "@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4", "@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-node-resolve": "^16.0.0",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@sinclair/typebox": "^0.34.9",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/node": "^22.0.1", "@types/node": "^22.0.1",
@@ -32,7 +37,7 @@
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.3.0",
"ajv": "^8.13.0", "ajv": "^8.13.0",
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"commander": "^12.1.0", "commander": "^13.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
@@ -41,7 +46,6 @@
"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": "^6.0.1", "vite": "^6.0.1",

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

107
src/common/cancel-task.ts Normal file
View File

@@ -0,0 +1,107 @@
export type TaskExecutor<T> = (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void, onCancel: (callback: () => void) => void) => void;
export class CancelableTask<T> {
private promise: Promise<T>;
private cancelCallback: (() => void) | null = null;
private isCanceled = false;
private cancelListeners: Array<() => void> = [];
constructor(executor: TaskExecutor<T>) {
this.promise = new Promise<T>((resolve, reject) => {
const onCancel = (callback: () => void) => {
this.cancelCallback = callback;
};
executor(
(value) => {
if (!this.isCanceled) {
resolve(value);
}
},
(reason) => {
if (!this.isCanceled) {
reject(reason);
}
},
onCancel
);
});
}
public cancel() {
if (this.cancelCallback) {
this.cancelCallback();
}
this.isCanceled = true;
this.cancelListeners.forEach(listener => listener());
}
public isTaskCanceled(): boolean {
return this.isCanceled;
}
public onCancel(listener: () => void) {
this.cancelListeners.push(listener);
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<T | TResult> {
return this.promise.catch(onrejected);
}
public finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.promise.finally(onfinally);
}
[Symbol.asyncIterator]() {
return {
next: () => this.promise.then(value => ({ value, done: true })),
};
}
}
async function demoAwait() {
const executor: TaskExecutor<number> = (resolve, reject, onCancel) => {
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Task is running... Count: ${count}`);
if (count === 5) {
clearInterval(intervalId);
resolve(count);
}
}, 1000);
onCancel(() => {
clearInterval(intervalId);
console.log('Task has been canceled.');
reject(new Error('Task was canceled'));
});
};
const task = new CancelableTask(executor);
task.onCancel(() => {
console.log('Cancel listener triggered.');
});
setTimeout(() => {
task.cancel(); // 取消任务
}, 6000);
try {
const result = await task;
console.log(`Task completed with result: ${result}`);
} catch (error) {
console.error('Task failed:', error);
}
}

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

22
src/common/decorator.ts Normal file
View File

@@ -0,0 +1,22 @@
// decoratorAsyncMethod(this,function,wrapper)
async function decoratorMethod<T, R>(
target: T,
method: () => Promise<R>,
wrapper: (result: R) => Promise<any>,
executeImmediately: boolean = true
): Promise<any> {
const execute = async () => {
try {
const result = await method.call(target);
return wrapper(result);
} catch (error) {
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
}
};
if (executeImmediately) {
return execute();
} else {
return execute;
}
}

43
src/common/fall-back.ts Normal file
View File

@@ -0,0 +1,43 @@
type Handler<T> = () => T | Promise<T>;
type Checker<T> = (result: T) => T | Promise<T>;
export class Fallback<T> {
private handlers: Handler<T>[] = [];
private checker: Checker<T>;
constructor(checker?: Checker<T>) {
this.checker = checker || (async (result: T) => result);
}
add(handler: Handler<T>): this {
this.handlers.push(handler);
return this;
}
// 执行处理程序链
async run(): Promise<T> {
const errors: Error[] = [];
for (const handler of this.handlers) {
try {
const result = await handler();
let data = await this.checker(result);
if (data) {
return data;
}
} catch (error) {
console.log(error);
errors.push(error instanceof Error ? error : new Error(String(error)));
}
}
throw new AggregateError(errors, 'All handlers failed');
}
}
export class FallbackUtil {
static boolchecker<T>(value: T, condition: boolean): T {
if (condition) {
return value;
} else {
throw new Error('Condition is false, throwing error');
}
}
}

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,
@@ -190,16 +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 = uri.slice(7); if (uri.startsWith('file:')) {
const filePath: string = decodeURIComponent(uri.startsWith('file:///') && process.platform === 'win32' ? uri.slice(8) : uri.slice(7));
return { Uri: filePath, Type: FileUriType.Local }; return { Uri: filePath, Type: FileUriType.Local };
} }
if (uri.startsWith('data:')) { if (uri.startsWith('data:')) {
@@ -212,63 +175,34 @@ export async function checkUriType(Uri: string) {
return { Uri: Uri, Type: FileUriType.Unknown }; return { Uri: Uri, Type: FileUriType.Unknown };
} }
export async function uri2local(dir: string, uri: string, filename: string | undefined = undefined): Promise<Uri2LocalRes> { export async function uriToLocalFile(dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri); const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
//解析失败 const filePath = path.join(dir, filename);
const tempName = randomUUID();
if (!filename) filename = randomUUID();
//解析Http和Https协议 switch (UriType) {
if (UriType == FileUriType.Unknown) { case FileUriType.Local: {
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
const fileExt = path.extname(HandledUri); const fileExt = path.extname(HandledUri);
let filename = path.basename(HandledUri, fileExt); const localFileName = path.basename(HandledUri, fileExt) + fileExt;
filename += fileExt; const tempFilePath = path.join(dir, filename + fileExt);
//复制文件到临时文件并保持后缀 fs.copyFileSync(HandledUri, tempFilePath);
const filenameTemp = tempName + fileExt; return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
const filePath = path.join(dir, filenameTemp);
fs.copyFileSync(HandledUri, filePath);
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
//接下来都要有文件名
if (UriType == FileUriType.Remote) {
const pathInfo = path.parse(decodeURIComponent(new URL(HandledUri).pathname));
if (pathInfo.name) {
const pathlen = 200 - dir.length - pathInfo.name.length;
filename = pathlen > 0 ? pathInfo.name.substring(0, pathlen) : pathInfo.name.substring(pathInfo.name.length, pathInfo.name.length - 10);//过长截断
if (pathInfo.ext) {
filename += pathInfo.ext;
}
}
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
const fileExt = path.extname(HandledUri).replace(/[/\\:*?"<>|]/g, '_').substring(0, 10);
const filePath = path.join(dir, tempName + fileExt);
const buffer = await httpDownload(HandledUri);
//没有文件就创建
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
} }
//解析Base64 case FileUriType.Remote: {
if (UriType == FileUriType.Base64) { const buffer = await httpDownload({ url: HandledUri, headers: headers });
const base64 = HandledUri.replace(/^base64:\/\//, ''); fs.writeFileSync(filePath, buffer, { flag: 'wx' });
const buffer = Buffer.from(base64, 'base64'); return { success: true, errMsg: '', fileName: filename, path: filePath };
let filePath = path.join(dir, filename); }
let fileExt = '';
fs.writeFileSync(filePath, buffer); case FileUriType.Base64: {
const { success, ext, path: fileTypePath } = await checkFileV2(filePath); const base64 = HandledUri.replace(/^base64:\/\//, '');
if (success) { const base64Buffer = Buffer.from(base64, 'base64');
filePath = fileTypePath; fs.writeFileSync(filePath, base64Buffer, { flag: 'wx' });
fileExt = ext; return { success: true, errMsg: '', fileName: filename, path: filePath };
filename = filename + '.' + ext; }
}
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath }; default:
return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
} }
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'),
]);
}
} }

165
src/common/umami.ts Normal file
View File

@@ -0,0 +1,165 @@
import https from 'node:https';
import { napCatVersion } from './version';
import os from 'node:os';
export class UmamiTraceCore {
napcatVersion = napCatVersion;
qqversion = '1.0.0';
guid = 'default-user';
heartbeatInterval: NodeJS.Timeout | null = null;
website: string = '596cbbb2-1740-4373-a807-cf3d0637bfa7';
referrer: string = 'https://trace.napneko.icu/';
hostname: string = 'trace.napneko.icu';
ua: string = '';
workname: string = 'default';
bootTime = Date.now();
cache: string = '';
platform = process.platform;
init(qqversion: string, guid: string, workname: string) {
this.qqversion = qqversion;
this.workname = workname;
const UaList = {
linux: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36',
win32: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36',
darwin: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36',
};
try {
if (this.platform === 'win32') {
const ntVersion = os.release();
UaList.win32 = `Mozilla/5.0 (Windows NT ${ntVersion}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.2128.93 Safari/537.36`;
} else if (this.platform === 'darwin') {
const macVersion = os.release();
UaList.darwin = `Mozilla/5.0 (Macintosh; Intel Mac OS X ${macVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36`;
}
} catch (error) {
this.ua = UaList.win32;
}
this.ua = UaList[this.platform as keyof typeof UaList] || UaList.win32;
this.identifyUser(guid);
this.startHeartbeat();
}
identifyUser(guid: string) {
this.guid = guid;
const data = {
napcat_version: this.napcatVersion,
qq_version: this.qqversion,
napcat_working: this.workname,
device_guid: this.guid,
device_platform: this.platform,
device_arch: os.arch(),
boot_time: new Date(this.bootTime + 8 * 60 * 60 * 1000).toISOString().replace('T', ' ').substring(0, 19),
sys_time: new Date(Date.now() - os.uptime() * 1000 + 8 * 60 * 60 * 1000).toISOString().replace('T', ' ').substring(0, 19),
};
this.sendEvent(
{
website: this.website,
hostname: this.hostname,
referrer: this.referrer,
title: 'NapCat ' + this.napcatVersion,
url: `/${this.qqversion}/${this.napcatVersion}/${this.workname}/identify`,
},
data,
'identify'
);
}
sendEvent(event: string | object, data?: object, type = 'event') {
const env = process.env;
const language = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES;
const payload = {
...(typeof event === 'string' ? { event } : event),
hostname: this.hostname,
referrer: this.referrer,
website: this.website,
language: language || 'en-US',
screen: '1920x1080',
data: {
...data,
},
};
this.sendRequest(payload, type);
}
sendTrace(eventName: string, data: string = '') {
const payload = {
website: this.website,
hostname: this.hostname,
title: 'NapCat ' + this.napcatVersion,
url: `/${this.qqversion}/${this.napcatVersion}/${this.workname}/${eventName}` + (data ? `/${data}` : ''),
referrer: this.referrer,
};
this.sendRequest(payload);
}
sendRequest(payload: object, type = 'event') {
const options = {
hostname: '104.19.42.72', // 固定 IP 或者从 hostUrl 获取
port: 443,
path: '/api/send',
method: 'POST',
headers: {
"Host": "umami.napneko.icu",
"Content-Type": "application/json",
"User-Agent": this.ua,
...(this.cache ? { 'x-umami-cache': this.filterInvalidChars(this.cache) } : {})
}
};
try {
const request = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
if (!this.cache) {
this.cache = responseData;
console.log('Umami cache:', this.cache);
}
});
res.on('error', (error) => {
});
});
request.on('error', (error) => {
});
request.write(JSON.stringify({ type, payload }));
request.end();
} catch (error) {
}
}
filterInvalidChars(value: string): string {
return value.replace(/[^\x00-\x7F]/g, '');
}
startHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
this.heartbeatInterval = setInterval(() => {
this.sendEvent({
name: 'heartbeat',
title: 'NapCat ' + this.napcatVersion,
url: `/${this.qqversion}/${this.napcatVersion}/${this.workname}/heartbeat`,
});
}, 5 * 60 * 1000);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
}
export const UmamiTrace = new UmamiTraceCore();

View File

@@ -1 +1 @@
export const napCatVersion = '4.2.0'; export const napCatVersion = '4.2.68';

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';
@@ -41,7 +40,7 @@ export class NTQQFileApi {
this.rkeyManager = new RkeyManager([ this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys' 'https://rkey.napneko.icu/rkeys'
], ],
this.context.logger this.context.logger
); );
} }
@@ -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; const extOrEmpty = await fileTypeFromFile(filePath).then(e => e?.ext ?? '').catch(e => '');
const ext = extOrEmpty ? `.${extOrEmpty}` : ''; const ext = extOrEmpty ? `.${extOrEmpty}` : '';
let fileName = `${path.basename(filePath)}`; let fileName = `${path.basename(filePath)}`;
if (fileName.indexOf('.') === -1) { if (fileName.indexOf('.') === -1) {
@@ -142,7 +141,6 @@ export class NTQQFileApi {
} }
async createValidSendVideoElement(context: SendMessageContext, 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 {
@@ -307,18 +305,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE element.elementType === ElementType.FILE
) { ) {
switch (element.elementType) { switch (element.elementType) {
case ElementType.PIC: case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex]; element.picElement!.sourcePath = elementResults[elementIndex];
break; break;
case ElementType.VIDEO: case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex]; element.videoElement!.filePath = elementResults[elementIndex];
break; break;
case ElementType.PTT: case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex]; element.pttElement!.filePath = elementResults[elementIndex];
break; break;
case ElementType.FILE: case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex]; element.fileElement!.filePath = elementResults[elementIndex];
break; break;
} }
elementIndex++; elementIndex++;
} }
@@ -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,8 @@ import {
KickMemberV2Req, KickMemberV2Req,
MemberExtSourceType, MemberExtSourceType,
NapCatCore, NapCatCore,
GroupNotify,
GroupInfoSource,
} from '@/core'; } from '@/core';
import { isNumeric, solveAsyncProblem } from '@/common/helper'; import { isNumeric, solveAsyncProblem } from '@/common/helper';
import { LimitedHashTable } from '@/common/message-unique'; import { LimitedHashTable } from '@/common/message-unique';
@@ -16,34 +17,36 @@ 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[] = []; groupMemberCacheEvent: Map<string, boolean> = new Map<string, boolean>();
essenceLRU = new LimitedHashTable<number, string>(1000); essenceLRU = new LimitedHashTable<number, string>(1000);
session: any;
constructor(context: InstanceContext, core: NapCatCore) { constructor(context: InstanceContext, core: NapCatCore) {
this.context = context; this.context = context;
this.core = core; this.core = core;
} }
async initApi() {
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger)); async fetchGroupDetail(groupCode: string) {
} const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
async initCache() { 'NodeIKernelGroupService/getGroupDetailInfo',
this.groups = await this.getGroups(); 'NodeIKernelGroupListener/onGroupDetailInfoChange',
for (const group of this.groups) { [groupCode, GroupInfoSource.KDATACARD],
this.groupCache.set(group.groupCode, group); (ret) => ret.result === 0,
} (detailInfo) => detailInfo.groupCode === groupCode,
this.context.logger.logDebug(`加载${this.groups.length}个群组缓存完成`); 1,
// process.pid 调试点 5000
);
return detailInfo;
} }
async getCoreAndBaseInfo(uids: string[]) { async initApi() {
return await this.core.eventWrapper.callNoListenerEvent( this.initCache().then().catch(e => this.context.logger.logError(e));
'NodeIKernelProfileService/getCoreAndBaseInfo', }
'nodeStore',
uids, async initCache() {
); for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
}
} }
async fetchGroupEssenceList(groupCode: string) { async fetchGroupEssenceList(groupCode: string) {
@@ -54,20 +57,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 +81,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 +123,54 @@ 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, isWait = true) {
this.groupMemberCacheEvent.set(groupCode, true);
const updateCache = async () => {
try {
const members = await this.getGroupMemberAll(groupCode, true);
this.groupMemberCache.set(groupCode, members.result.infos);
} catch (e) {
this.context.logger.logError(`刷新群成员缓存失败, 群号: ${groupCode}, 错误: ${e}`);
} finally {
this.groupMemberCacheEvent.set(groupCode, false);
}
};
if (isWait) {
await updateCache();
} else {
updateCache();
}
return this.groupMemberCache.get(groupCode);
}
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, true));
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, true));
member = getMember(); member = getMember();
} }
return member; return member;
@@ -174,26 +180,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 +210,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 +223,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 +277,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 +373,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

@@ -1,25 +0,0 @@
import { InstanceContext, NapCatCore } from '..';
export class NTQQMusicSignApi {
context: InstanceContext;
core: NapCatCore;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
}
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
//外域名不行得走qgroup中转
//https://proxy.gtimg.cn/tx_tls_gate=y.qq.com/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg
//可外域名
//https://pic.ugcimg.cn/500955bdd6657ecc8e82e02d2df06800/jpg1
//QQ音乐gtimg接口
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
//还有一处公告上传可以上传高质量图片 持久为qq域名
}

View File

@@ -2,6 +2,7 @@ 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 { Fallback, FallbackUtil } from '@/common/fall-back';
export class NTQQUserApi { export class NTQQUserApi {
context: InstanceContext; context: InstanceContext;
@@ -11,13 +12,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({
@@ -104,6 +107,19 @@ export class NTQQUserApi {
return retUser; return retUser;
} }
async getUserDetailInfoV2(uid: string): Promise<User> {
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
const retUser = await fallback.run().then(async (user) => {
if (user && user.uin === '0') {
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return user;
});
return retUser;
}
async modifySelfProfile(param: ModifyProfileParams) { async modifySelfProfile(param: ModifyProfileParams) {
return this.context.session.getProfileService().modifyDesktopMiniProfile(param); return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
} }
@@ -161,35 +177,39 @@ 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) { if (!uin) {
let uid = (await this.context.session.getGroupService().getUidByUins([Uin])).uids.get(Uin); return '';
if (uid) return uid; }
uid = (await this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [Uin])).get(Uin);
if (uid) return uid; const fallback =
uid = (await this.context.session.getUixConvertService().getUid([Uin])).uidInfo.get(Uin); new Fallback<string | undefined>((uid) => FallbackUtil.boolchecker(uid, uid !== undefined && uid.indexOf('*') === -1 && uid !== ''))
if (uid) return uid; .add(() => this.context.session.getUixConvertService().getUid([uin]).then((data) => data.uidInfo.get(uin)))
const unverifiedUid = (await this.getUserDetailInfoByUin(Uin)).detail.uid;//从QQ Native 特殊转换 .add(() => this.context.session.getProfileService().getUidByUin('FriendsServiceImpl', [uin]).get(uin))
if (unverifiedUid.indexOf('*') == -1) uid = unverifiedUid; .add(() => this.context.session.getGroupService().getUidByUins([uin]).then((data) => data.uids.get(uin)))
//if (uid) return uid; .add(() => this.getUserDetailInfoByUin(uin).then((data) => data.detail.uid));
return uid;
const uid = await fallback.run().catch(() => '');
return uid ?? '';
} }
//后期改成流水线处理 async getUinByUidV2(uid: string) {
async getUinByUidV2(Uid: string) { if (!uid) {
let uin = (await this.context.session.getGroupService().getUinByUids([Uid])).uins.get(Uid); return '0';
if (uin) return uin; }
uin = (await this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [Uid])).get(Uid);
if (uin) return uin; const fallback = new Fallback<string | undefined>((uin) => FallbackUtil.boolchecker(uin, uin !== undefined && uin !== '0' && uin !== ''))
uin = (await this.context.session.getUixConvertService().getUin([Uid])).uinInfo.get(Uid); .add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
if (uin) return uin; .add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
uin = (await this.core.apis.FriendApi.getBuddyIdMap(true)).getKey(Uid); .add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
if (uin) return uin; .add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
uin = (await this.getUserDetailInfo(Uid)).uin; //从QQ Native 转换
return uin; const uin = await fallback.run().catch(() => '0');
return uin ?? '0';
} }
async getRecentContactListSnapShot(count: number) { async getRecentContactListSnapShot(count: number) {

View File

@@ -8,7 +8,7 @@ import {
WebHonorType, WebHonorType,
} from '@/core'; } from '@/core';
import { NapCatCore } from '..'; import { NapCatCore } from '..';
import { createReadStream, readFileSync, statSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { basename } from 'node:path'; import { basename } from 'node:path';
@@ -366,50 +366,4 @@ export class NTQQWebApi {
return post; return post;
} }
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
const img_size = statSync(path).size;
const img_name = basename(path);
let seq = 0;
let offset = 0;
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const stream = createReadStream(path, { highWaterMark: slice_size });
for await (const chunk of stream) {
const end = Math.min(offset + chunk.length, img_size);
const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`;
const formData = await RequestUtil.createFormData(boundary, path);
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
const body = {
uin: uin,
appid: "qun",
session: session,
offset: offset,
data: formData,
checksum: "",
check_type: 0,
retry: 0,
seq: seq,
end: end,
cmd: "FileUpload",
slice_size: slice_size,
"biz_req.iUploadType": 0
};
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
"Cookie": cookie,
"Content-Type": `multipart/form-data; boundary=${boundary}`
});
offset += chunk.length;
seq++;
}
}
async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) {
const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session;
return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024);
}
} }

View File

@@ -98,5 +98,69 @@
"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"
},
"9.9.17-30483": {
"appid": 537258439,
"qua": "V1_WIN_NQ_9.9.17_30483_GW_B"
},
"6.9.62-30483": {
"appid": 537258463,
"qua": "V1_MAC_NQ_6.9.62_30483_GW_B"
},
"3.2.15-30483": {
"appid": 537258474,
"qua": "V1_LNX_NQ_3.2.15_30483_GW_B"
},
"9.9.17-30594": {
"appid": 537258439,
"qua": "V1_WIN_NQ_9.9.17_30594_GW_B"
},
"6.9.62-30594": {
"appid": 537258463,
"qua": "V1_MAC_NQ_6.9.62_30594_GW_B"
},
"3.2.15-30594": {
"appid": 537258474,
"qua": "V1_LNX_NQ_3.2.15_30594_GW_B"
},
"9.9.17-30851": {
"appid": 537263796,
"qua": "V1_WIN_NQ_9.9.17_30851_GW_B"
},
"3.2.15-30851": {
"appid": 537263831,
"qua": "V1_LNX_NQ_3.2.15_30851_GW_B"
},
"6.9.63-30851": {
"appid": 537263820,
"qua": "V1_MAC_NQ_6.9.63_30851_GW_B"
},
"9.9.17-30899": {
"appid": 537263796,
"qua": "V1_WIN_NQ_9.9.17_30899_GW_B"
},
"3.2.15-30899": {
"appid": 537263831,
"qua": "V1_LNX_NQ_3.2.15_30899_GW_B"
},
"6.9.63-30899": {
"appid": 537263820,
"qua": "V1_MAC_NQ_6.9.63_30899_GW_B"
},
"9.9.17-31219": {
"appid": 537266450,
"qua": "V1_WIN_NQ_9.9.17_31219_GW_B"
} }
} }

View File

@@ -102,5 +102,109 @@
"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"
},
"9.9.17-30483-x64": {
"send": "39AC1B0",
"recv": "39B05E4"
},
"6.9.62-30483-arm64": {
"send": "41896B0",
"recv": "418bec8"
},
"6.9.62-30483-x64": {
"send": "4669460",
"recv": "466BCCC"
},
"3.2.15-30483-x64": {
"send": "A402540",
"recv": "A405E40"
},
"3.2.15-30483-arm64": {
"send": "70C40E8",
"recv": "70C7920"
},
"9.9.17-30594-x64": {
"send": "39AC1B0",
"recv": "39B05E4"
},
"6.9.62-30594-arm64": {
"send": "41896B0",
"recv": "418bec8"
},
"6.9.62-30594-x64": {
"send": "4669460",
"recv": "466BCCC"
},
"3.2.15-30594-x64": {
"send": "A402540",
"recv": "A405E40"
},
"3.2.15-30594-arm64": {
"send": "70C40E8",
"recv": "70C7920"
},
"9.9.17-30851-x64": {
"send": "395C150",
"recv": "3960584"
},
"3.2.15-30851-x64": {
"send": "A4A03E0",
"recv": "A4A3CE0"
},
"3.2.15-30851-arm64": {
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30851-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
"6.9.63-30851-arm64": {
"send": "41DCBD8",
"recv": "41DF3F0"
},
"9.9.17-30899-x64": {
"send": "395C150",
"recv": "3960584"
},
"3.2.15-30899-x64": {
"send": "A4A03E0",
"recv": "A4A3CE0"
},
"3.2.15-30899-arm64": {
"send": "713A318",
"recv": "713DB50"
},
"6.9.63.30899-x64": {
"send": "46C8040",
"recv": "46CA8AC"
},
"6.9.63-30899-arm64": {
"send": "41DCBD8",
"recv": "41DF3F0"
},
"9.9.17-31219-x64": {
"send": "39C1350",
"recv": "39C5784"
} }
} }

View File

@@ -1,49 +0,0 @@
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const BodyInner = {
msgType: ProtoField(1, ScalarType.UINT32, true),
subType: ProtoField(2, ScalarType.UINT32, true)
};
const NoifyData = {
skip: ProtoField(1, ScalarType.BYTES, true),
innerData: ProtoField(2, ScalarType.BYTES, true)
};
const MsgHead = {
bodyInner: ProtoField(2, () => BodyInner, true),
noifyData: ProtoField(3, () => NoifyData, true)
};
const Message = {
msgHead: ProtoField(1, () => MsgHead)
};
const SubDetail = {
msgSeq: ProtoField(1, ScalarType.UINT32),
msgTime: ProtoField(2, ScalarType.UINT32),
senderUid: ProtoField(6, ScalarType.STRING)
};
const RecallDetails = {
operatorUid: ProtoField(1, ScalarType.STRING),
subDetail: ProtoField(3, () => SubDetail)
};
const RecallGroup = {
type: ProtoField(1, ScalarType.INT32),
peerUid: ProtoField(4, ScalarType.UINT32),
recallDetails: ProtoField(11, () => RecallDetails),
grayTipsSeq: ProtoField(37, ScalarType.UINT32)
};
export function decodeMessage(buffer: Uint8Array) {
const msg = new NapProtoMsg(Message);
return msg.decode(buffer);
}
export function decodeRecallGroup(buffer: Uint8Array){
const msg = new NapProtoMsg(RecallGroup);
return msg.decode(buffer);
}

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

138
src/core/helper/status.ts Normal file
View File

@@ -0,0 +1,138 @@
import os from "node:os";
import EventEmitter from "node:events";
export interface SystemStatus {
cpu: {
model: string,
speed: string
usage: {
system: string
qq: string
},
core: number
},
memory: {
total: string
usage: {
system: string
qq: string
}
},
arch: string
}
export class StatusHelper {
private psCpuUsage = process.cpuUsage();
private psCurrentTime = process.hrtime();
private cpuTimes = os.cpus().map(cpu => cpu.times);
private replaceNaN(value: number) {
return isNaN(value) ? 0 : value;
}
private sysCpuInfo() {
const currentTimes = os.cpus().map(cpu => cpu.times);
const { total, active } = currentTimes.map((times, index) => {
const prevTimes = this.cpuTimes[index];
const totalCurrent = times.user + times.nice + times.sys + times.idle + times.irq;
const totalPrev = prevTimes.user + prevTimes.nice + prevTimes.sys + prevTimes.idle + prevTimes.irq;
const activeCurrent = totalCurrent - times.idle;
const activePrev = totalPrev - prevTimes.idle;
return {
total: totalCurrent - totalPrev,
active: activeCurrent - activePrev
};
}).reduce((acc, cur) => ({
total: acc.total + cur.total,
active: acc.active + cur.active
}), { total: 0, active: 0 });
this.cpuTimes = currentTimes;
return {
usage: this.replaceNaN(((active / total) * 100)).toFixed(2),
model: os.cpus()[0].model,
speed: os.cpus()[0].speed,
core: os.cpus().length
};
}
private sysMemoryUsage() {
const { total, free } = { total: os.totalmem(), free: os.freemem() };
return ((total - free) / 1024 / 1024).toFixed(2);
}
private qqUsage() {
const mem = process.memoryUsage();
const numCpus = os.cpus().length;
const usageDiff = process.cpuUsage(this.psCpuUsage);
const endTime = process.hrtime(this.psCurrentTime);
this.psCpuUsage = process.cpuUsage();
this.psCurrentTime = process.hrtime();
const usageMS = (usageDiff.user + usageDiff.system) / 1e3;
const totalMS = endTime[0] * 1e3 + endTime[1] / 1e6;
const normPercent = (usageMS / totalMS / numCpus) * 100;
return {
cpu: this.replaceNaN(normPercent).toFixed(2),
memory: ((mem.heapTotal + mem.external + mem.arrayBuffers) / 1024 / 1024).toFixed(2)
};
}
systemStatus(): SystemStatus {
const qqUsage = this.qqUsage();
const sysCpuInfo = this.sysCpuInfo();
return {
cpu: {
core: sysCpuInfo.core,
model: sysCpuInfo.model,
speed: (sysCpuInfo.speed / 1000).toFixed(2),
usage: {
system: sysCpuInfo.usage,
qq: qqUsage.cpu
},
},
memory: {
total: (os.totalmem() / 1024 / 1024).toFixed(2),
usage: {
system: this.sysMemoryUsage(),
qq: qqUsage.memory
}
},
arch: `${os.platform()} ${os.arch()} ${os.release()}`
};
}
}
class StatusHelperSubscription extends EventEmitter {
private statusHelper: StatusHelper;
private interval: NodeJS.Timeout | null = null;
constructor(time: number = 3000) {
super();
this.statusHelper = new StatusHelper();
this.on('newListener', (event: string) => {
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
this.startInterval(time);
}
});
this.on('removeListener', (event: string) => {
if (event === 'statusUpdate' && this.listenerCount('statusUpdate') === 0) {
this.stopInterval();
}
});
}
private startInterval(time: number) {
this.interval ??= setInterval(() => {
const status = this.statusHelper.systemStatus();
this.emit('statusUpdate', status);
}, time);
}
private stopInterval() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
}
export const statusHelperSubscription = new StatusHelperSubscription();

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,
@@ -152,9 +152,10 @@ export class NapCatCore {
// Renamed from 'InitDataListener' // Renamed from 'InitDataListener'
async initNapCatCoreListeners() { async initNapCatCoreListeners() {
const msgListener = new NodeIKernelMsgListener(); const msgListener = new NodeIKernelMsgListener();
msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => { msgListener.onKickedOffLine = (Info: KickedOffLineInfo) => {
// 下线通知 // 下线通知
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 +164,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 +185,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

@@ -1,4 +1,4 @@
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types'; import { DataSource, Group, GroupDetailInfo, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
export class NodeIKernelGroupListener { export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): any { } onGroupListInited(listEmpty: boolean): any { }
@@ -28,7 +28,7 @@ export class NodeIKernelGroupListener {
onGroupConfMemberChange(...args: unknown[]): any { onGroupConfMemberChange(...args: unknown[]): any {
} }
onGroupDetailInfoChange(...args: unknown[]): any { onGroupDetailInfoChange(detailInfo: GroupDetailInfo): any {
} }
onGroupExtListUpdate(...args: unknown[]): any { onGroupExtListUpdate(...args: unknown[]): any {

View File

@@ -23,7 +23,9 @@ export class PacketClientSession {
get operation() { get operation() {
return this.context.operation; return this.context.operation;
} }
get client() {
return this.context.client;
}
// TODO: global message element adapter (? // TODO: global message element adapter (?
get msgConverter() { get msgConverter() {
return this.context.msgConverter; return this.context.msgConverter;

View File

@@ -21,7 +21,7 @@ export class PacketOperationContext {
} }
async GroupPoke(groupUin: number, uin: number) { async GroupPoke(groupUin: number, uin: number) {
const req = trans.SendPoke.build(groupUin, uin); const req = trans.SendPoke.build(uin, groupUin);
await this.context.client.sendOidbPacket(req); await this.context.client.sendOidbPacket(req);
} }

View File

@@ -3,6 +3,7 @@ export interface MiniAppReqCustomParams {
desc: string; desc: string;
picUrl: string; picUrl: string;
jumpUrl: string; jumpUrl: string;
webUrl?: string;
} }
export interface MiniAppReqTemplateParams { export interface MiniAppReqTemplateParams {

View File

@@ -124,7 +124,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
} }
get isGroupReply(): boolean { get isGroupReply(): boolean {
return this.messageClientSeq !== 0; return this.messageClientSeq === 0;
} }
buildElement(): NapProtoEncodeStructType<typeof Elem>[] { buildElement(): NapProtoEncodeStructType<typeof Elem>[] {

View File

@@ -30,7 +30,7 @@ class GetMiniAppAdaptShareInfo extends PacketTransformer<typeof proto.MiniAppAda
shareType: req.shareType, shareType: req.shareType,
versionId: req.versionId, versionId: req.versionId,
withShareTicket: req.withShareTicket, withShareTicket: req.withShareTicket,
webURL: "", webURL: req.webUrl ?? "",
appidRich: Buffer.alloc(0), appidRich: Buffer.alloc(0),
template: { template: {
templateId: "", templateId: "",

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,15 +54,31 @@ export const PushMsg = {
generalFlag: ProtoField(9, ScalarType.INT32, true), generalFlag: ProtoField(9, ScalarType.INT32, true),
}; };
export const GroupChangeInfo = {
operator: ProtoField(1, () => GroupChangeOperator, true),
};
export const GroupChangeOperator = {
operatorUid: ProtoField(1, ScalarType.STRING, true),
};
export const GroupChange = { export const GroupChange = {
groupUin: ProtoField(1, ScalarType.UINT32), groupUin: ProtoField(1, ScalarType.UINT32),
flag: ProtoField(2, ScalarType.UINT32), flag: ProtoField(2, ScalarType.UINT32),
memberUid: ProtoField(3, ScalarType.STRING, true), memberUid: ProtoField(3, ScalarType.STRING, true),
decreaseType: ProtoField(4, ScalarType.UINT32), decreaseType: ProtoField(4, ScalarType.UINT32),
operatorUid: ProtoField(5, ScalarType.STRING, true), operatorInfo: ProtoField(5, ScalarType.BYTES, true),
increaseType: ProtoField(6, ScalarType.UINT32), increaseType: ProtoField(6, ScalarType.UINT32),
field7: ProtoField(7, ScalarType.BYTES, true), field7: ProtoField(7, ScalarType.BYTES, true),
} };
export const GroupInvite = {
groupUin: ProtoField(1, ScalarType.UINT32),
field2: ProtoField(2, ScalarType.UINT32),
field3: ProtoField(2, ScalarType.UINT32),
field4: ProtoField(2, ScalarType.UINT32),
invitorUid: ProtoField(5, ScalarType.STRING),
};
export const PushMsgBody = { export const PushMsgBody = {
responseHead: ProtoField(1, () => ResponseHead), responseHead: ProtoField(1, () => ResponseHead),

View File

@@ -149,7 +149,7 @@ export interface NodeIKernelGroupService {
getGroupExtList(force: boolean): Promise<GeneralCallResult>; getGroupExtList(force: boolean): Promise<GeneralCallResult>;
getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<unknown>; getGroupDetailInfo(groupCode: string, groupInfoSource: GroupInfoSource): Promise<GeneralCallResult>;
getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req getMemberExtInfo(param: GroupExtParam): Promise<unknown>;//req
@@ -163,7 +163,7 @@ export interface NodeIKernelGroupService {
getGroupPortrait(): void; getGroupPortrait(): void;
modifyGroupName(groupCode: string, groupName: string, arg: false): void; modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise<GeneralCallResult>;
modifyGroupRemark(groupCode: string, remark: string): void; modifyGroupRemark(groupCode: string, remark: string): void;
@@ -187,13 +187,13 @@ export interface NodeIKernelGroupService {
destroyGroup(groupCode: string): void; destroyGroup(groupCode: string): void;
getSingleScreenNotifies(doubted: boolean, start_seq: string, num: number): Promise<GeneralCallResult>; getSingleScreenNotifies(doubt: boolean, startSeq: string, count: number): Promise<GeneralCallResult>;
clearGroupNotifies(groupCode: string): void; clearGroupNotifies(groupCode: string): void;
getGroupNotifiesUnreadCount(unknown: boolean): Promise<GeneralCallResult>; getGroupNotifiesUnreadCount(doubt: boolean): Promise<GeneralCallResult>;
clearGroupNotifiesUnreadCount(unknown: boolean): void; clearGroupNotifiesUnreadCount(doubt: boolean): void;
operateSysNotify( operateSysNotify(
doubt: boolean, doubt: boolean,

View File

@@ -4,6 +4,7 @@ import { GeneralCallResult } from '@/core/services/common';
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg'; import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '@/core/types/msg';
export interface NodeIKernelMsgService { export interface NodeIKernelMsgService {
buildMultiForwardMsg(req: { srcMsgIds: Array<string>, srcContact: Peer }): Promise<GeneralCallResult & { rspInfo: { elements: unknown } }>;
generateMsgUniqueId(chatType: number, time: string): string; generateMsgUniqueId(chatType: number, time: string): string;

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

@@ -16,7 +16,7 @@ export interface NodeIKernelSearchService {
penetrate: string penetrate: string
}): Promise<GeneralCallResult>;// needs 1 arguments }): Promise<GeneralCallResult>;// needs 1 arguments
searchLocalInfo(keywords: string, unknown: number/*4*/): unknown; searchLocalInfo(keywords: string, type: number/*4*/): unknown;
cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments cancelSearchLocalInfo(...args: any[]): unknown;// needs 3 arguments

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;
@@ -55,6 +56,7 @@ export interface GrayTipElement {
aioOpGrayTipElement: TipAioOpGrayTipElement; aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement; groupElement: TipGroupElement;
xmlElement: { xmlElement: {
busiId: string;
content: string; content: string;
templId: string; templId: string;
}; };

View File

@@ -17,7 +17,160 @@ export enum GroupInfoSource {
KRECENTCONTACT, KRECENTCONTACT,
KMOREPANEL KMOREPANEL
} }
export interface GroupDetailInfo {
groupCode: string;
groupUin: string;
ownerUid: string;
ownerUin: string;
groupFlag: number;
groupFlagExt: number;
maxMemberNum: number;
memberNum: number;
groupOption: number;
classExt: number;
groupName: string;
fingerMemo: string;
groupQuestion: string;
certType: number;
richFingerMemo: string;
tagRecord: any[];
shutUpAllTimestamp: number;
shutUpMeTimestamp: number;
groupTypeFlag: number;
privilegeFlag: number;
groupSecLevel: number;
groupFlagExt3: number;
isConfGroup: number;
isModifyConfGroupFace: number;
isModifyConfGroupName: number;
groupFlagExt4: number;
groupMemo: string;
cmdUinMsgSeq: number;
cmdUinJoinTime: number;
cmdUinUinFlag: number;
cmdUinMsgMask: number;
groupSecLevelInfo: number;
cmdUinPrivilege: number;
cmdUinFlagEx2: number;
appealDeadline: number;
remarkName: string;
isTop: boolean;
groupFace: number;
groupGeoInfo: {
ownerUid: string;
SetTime: number;
CityId: number;
Longitude: string;
Latitude: string;
GeoContent: string;
poiId: string;
};
certificationText: string;
cmdUinRingtoneId: number;
longGroupName: string;
autoAgreeJoinGroupUserNumForConfGroup: number;
autoAgreeJoinGroupUserNumForNormalGroup: number;
cmdUinFlagExt3Grocery: number;
groupCardPrefix: {
introduction: string;
rptPrefix: any[];
};
groupExt: {
groupInfoExtSeq: number;
reserve: number;
luckyWordId: string;
lightCharNum: number;
luckyWord: string;
starId: number;
essentialMsgSwitch: number;
todoSeq: number;
blacklistExpireTime: number;
isLimitGroupRtc: number;
companyId: number;
hasGroupCustomPortrait: number;
bindGuildId: string;
groupOwnerId: {
memberUin: string;
memberUid: string;
memberQid: string;
};
essentialMsgPrivilege: number;
msgEventSeq: string;
inviteRobotSwitch: number;
gangUpId: string;
qqMusicMedalSwitch: number;
showPlayTogetherSwitch: number;
groupFlagPro1: string;
groupBindGuildIds: {
guildIds: any[];
};
viewedMsgDisappearTime: string;
groupExtFlameData: {
switchState: number;
state: number;
dayNums: any[];
version: number;
updateTime: string;
isDisplayDayNum: boolean;
};
groupBindGuildSwitch: number;
groupAioBindGuildId: string;
groupExcludeGuildIds: {
guildIds: any[];
};
fullGroupExpansionSwitch: number;
fullGroupExpansionSeq: string;
inviteRobotMemberSwitch: number;
inviteRobotMemberExamine: number;
groupSquareSwitch: number;
};
msgLimitFrequency: number;
hlGuildAppid: number;
hlGuildSubType: number;
isAllowRecallMsg: number;
confUin: string;
confMaxMsgSeq: number;
confToGroupTime: number;
groupSchoolInfo: {
location: string;
grade: number;
school: string;
};
activeMemberNum: number;
groupGrade: number;
groupCreateTime: number;
subscriptionUin: string;
subscriptionUid: string;
noFingerOpenFlag: number;
noCodeFingerOpenFlag: number;
isGroupFreeze: number;
allianceId: string;
groupExtOnly: {
tribeId: number;
moneyForAddGroup: number;
};
isAllowConfGroupMemberModifyGroupName: number;
isAllowConfGroupMemberNick: number;
isAllowConfGroupMemberAtAll: number;
groupClassText: string;
groupFreezeReason: number;
headPortraitSeq: number;
groupHeadPortrait: {
portraitCnt: number;
portraitInfo: any[];
defaultId: number;
verifyingPortraitCnt: number;
verifyingPortraitInfo: any[];
};
cmdUinJoinMsgSeq: number;
cmdUinJoinRealMsgSeq: number;
groupAnswer: string;
groupAdminMaxNum: number;
inviteNoAuthNumLimit: string;
hlGuildOrgId: number;
isAllowHlGuildBinary: number;
localExitGroupReason: number;
}
export interface GroupExt0xEF0InfoFilter { export interface GroupExt0xEF0InfoFilter {
bindGuildId: number; bindGuildId: number;
blacklistExpireTime: number; blacklistExpireTime: number;

View File

@@ -9,4 +9,5 @@ export * from './sign';
export * from './element'; export * from './element';
export * from './constant'; export * from './constant';
export * from './graytip'; export * from './graytip';
export * from './emoji'; export * from './emoji';
export * from './service';

View File

@@ -508,7 +508,7 @@ export interface RawMessage {
*/ */
export interface QueryMsgsParams { export interface QueryMsgsParams {
chatInfo: Peer; chatInfo: Peer;
filterMsgType: []; filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
filterSendersUid: string[]; filterSendersUid: string[];
filterMsgFromTime: string; filterMsgFromTime: string;
filterMsgToTime: string; filterMsgToTime: string;

35
src/core/types/service.ts Normal file
View File

@@ -0,0 +1,35 @@
export enum LoginErrorCode {
KLOGINERRORACCOUNTNOTUIN = 140022018,
KLOGINERRORACCOUNTORPASSWORDERROR = 140022013,
KLOGINERRORBLACKACCOUNT = 150022021,
KLOGINERRORDEFAULT = 140022000,
KLOGINERROREXPIRETICKET = 140022014,
KLOGINERRORFROZEN = 140022005,
KLOGINERRORILLAGETICKET = 140022016,
KLOGINERRORINVAILDCOOKIE = 140022012,
KLOGINERRORINVALIDPARAMETER = 140022001,
KLOGINERRORKICKEDTICKET = 140022015,
KLOGINERRORMUTIPLEPASSWORDINCORRECT = 150022029,
KLOGINERRORNEEDUPDATE = 140022004,
KLOGINERRORNEEDVERIFYREALNAME = 140022019,
KLOGINERRORNEWDEVICE = 140022010,
KLOGINERRORNICEACCOUNTEXPIRED = 150022020,
KLOGINERRORNICEACCOUNTPARENTCHILDEXPIRED = 150022025,
KLOGINERRORPASSWORD = 2,
KLOGINERRORPROOFWATER = 140022008,
KLOGINERRORPROTECT = 140022006,
KLOGINERRORREFUSEPASSOWRDLOGIN = 140022009,
KLOGINERRORREMINDCANAELLATEDSTATUS = 150022028,
KLOGINERRORSCAN = 1,
KLOGINERRORSCCESS = 0,
KLOGINERRORSECBEAT = 140022017,
KLOGINERRORSMSINVALID = 150022026,
KLOGINERRORSTRICK = 140022007,
KLOGINERRORSYSTEMFAILED = 140022002,
KLOGINERRORTGTGTEXCHAGEA1FORBID = 150022027,
KLOGINERRORTIMEOUTRETRY = 140022003,
KLOGINERRORTOOMANYTIMESTODAY = 150022023,
KLOGINERRORTOOOFTEN = 150022022,
KLOGINERRORUNREGISTERED = 150022024,
KLOGINERRORUNUSUALDEVICE = 140022011,
}

View File

@@ -1,6 +1,26 @@
//LiteLoader需要提供部分IPC接口以便于其他插件调用 //LiteLoader需要提供部分IPC接口以便于其他插件调用
const { ipcMain } = require('electron'); const { ipcMain, BrowserWindow } = require('electron');
const napcat = require('./napcat.cjs'); const napcat = require('./napcat.cjs');
const { shell } = require('electron');
ipcMain.handle('napcat_get_webtoken', async (event, arg) => { ipcMain.handle('napcat_get_webtoken', async (event, arg) => {
return napcat.NCgetWebUiUrl(); return napcat.NCgetWebUiUrl();
}); });
ipcMain.on('open_external_url', (event, url) => {
shell.openExternal(url);
});
ipcMain.handle('napcat_get_reactweb', async (event, arg) => {
let url = new URL(await napcat.NCgetWebUiUrl());
let port = url.port;
let token = url.searchParams.get('token');
return `https://napcat.152710.xyz/web_login?back=http://127.0.0.1:${port}&token=${token}`;
});
ipcMain.on('napcat_open_inner_url', (event, url) => {
const win = new BrowserWindow({
autoHideMenuBar: true,
});
win.loadURL(url);
win.webContents.setWindowOpenHandler(details => {
win.loadURL(details.url)
})
});

View File

@@ -27,6 +27,7 @@ export async function NCoreInitFramework(
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
console.log('[NapCat] [Error] Unhandled Exception:', err.message); console.log('[NapCat] [Error] Unhandled Exception:', err.message);
}); });
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, promise) => {
console.log('[NapCat] [Error] unhandledRejection:', reason); console.log('[NapCat] [Error] unhandledRejection:', reason);
}); });
@@ -58,7 +59,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,17 @@
const { contextBridge } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
const { ipcRenderer } = require('electron');
const napcat = { const napcat = {
getWebUiUrl: async () => { getWebUiUrl: async () => {
return ipcRenderer.invoke('napcat_get_webtoken'); return ipcRenderer.invoke('napcat_get_webtoken');
}, },
openExternalUrl: async (url) => {
ipcRenderer.send('open_external_url', url);
},
openInnerUrl: async (url) => {
ipcRenderer.send('napcat_open_inner_url', url);
},
getWebUiUrlReact: async () => {
return ipcRenderer.invoke('napcat_get_reactweb');
}
}; };
// 在window对象下导出只读对象 // 在window对象下导出只读对象
contextBridge.exposeInMainWorld('napcat', napcat); contextBridge.exposeInMainWorld('napcat', napcat);

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.napcat.openInnerUrl(webui);
}); });
view.querySelector('.nc_openwebui_ex').addEventListener('click', () => {
window.napcat.openExternalUrl(webui);
});
view.querySelector('.nc_openwebui_ex_react').addEventListener('click', () => {
window.napcat.openExternalUrl(webuiReact);
});
view.querySelector('.nc_webui').innerText = webui; view.querySelector('.nc_webui').innerText = webui;
};
// 添加点击复制功能
view.querySelector('.nc_webui').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(webui);
alert('WebUi URL 已复制到剪贴板');
} catch (err) {
console.error('复制到剪贴板失败: ', err);
}
});
};

View File

@@ -29,7 +29,7 @@ export class OB11Response {
} }
export abstract class OneBotAction<PayloadType, ReturnDataType> { export abstract class OneBotAction<PayloadType, ReturnDataType> {
actionName: ActionName = ActionName.Unknown; actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
core: NapCatCore; core: NapCatCore;
private validate: ValidateFunction<any> | undefined = undefined; private validate: ValidateFunction<any> | undefined = undefined;
payloadSchema: any = undefined; payloadSchema: any = undefined;
@@ -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[];
@@ -83,5 +83,5 @@ export abstract class OneBotAction<PayloadType, ReturnDataType> {
} }
} }
abstract _handle(payload: PayloadType, adaptername: string): PromiseLike<ReturnDataType>; abstract _handle(payload: PayloadType, adaptername: string): Promise<ReturnDataType>;
} }

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

@@ -0,0 +1,14 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
interface GetClientkeyResponse {
clientkey?: string;
}
export class GetClientkey extends OneBotAction<void, GetClientkeyResponse> {
actionName = ActionName.GetClientkey;
async _handle() {
return { clientkey: (await this.core.apis.UserApi.forceFetchClientKey()).clientKey };
}
}

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,39 @@
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' }, webUrl: Type.Optional(Type.String()),
desc: { type: 'string' }, rawArkData: Type.Optional(Type.Union([Type.String()]))
picUrl: { type: 'string' }, }),
jumpUrl: { type: 'string' }, Type.Object({
iconUrl: { type: 'string' }, title: Type.String(),
sdkId: { type: 'string' }, desc: Type.String(),
appId: { type: 'string' }, picUrl: Type.String(),
scene: { type: ['number', 'string'] }, jumpUrl: Type.String(),
templateType: { type: ['number', 'string'] }, iconUrl: Type.String(),
businessType: { type: ['number', 'string'] }, webUrl: Type.Optional(Type.String()),
verType: { type: ['number', 'string'] }, appId: Type.String(),
shareType: { type: ['number', 'string'] }, scene: Type.Union([Type.Number(), Type.String()]),
versionId: { type: 'string' }, templateType: Type.Union([Type.Number(), Type.String()]),
withShareTicket: { type: ['number', 'string'] }, businessType: Type.Union([Type.Number(), Type.String()]),
rawArkData: { type: ['boolean', 'string'] } verType: Type.Union([Type.Number(), Type.String()]),
}, shareType: Type.Union([Type.Number(), Type.String()]),
oneOf: [ versionId: Type.String(),
{ sdkId: Type.String(),
required: ['type', 'title', 'desc', 'picUrl', 'jumpUrl'] withShareTicket: Type.Union([Type.Number(), Type.String()]),
}, rawArkData: Type.Optional(Type.Union([Type.String()]))
{ })
required: [ ]);
'title', 'desc', 'picUrl', 'jumpUrl', type Payload = Static<typeof SchemaData>;
'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
@@ -55,9 +47,10 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
title: payload.title, title: payload.title,
desc: payload.desc, desc: payload.desc,
picUrl: payload.picUrl, picUrl: payload.picUrl,
jumpUrl: payload.jumpUrl jumpUrl: payload.jumpUrl,
webUrl: payload.webUrl,
} 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;
@@ -73,13 +66,13 @@ export class GetMiniAppArk extends GetPacketStatusDepends<Payload, {
verType: +verType, verType: +verType,
shareType: +shareType, shareType: +shareType,
versionId: versionId, versionId: versionId,
withShareTicket: +withShareTicket withShareTicket: +withShareTicket,
} }
); );
} }
const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam); const arkData = await this.core.apis.PacketApi.pkt.operation.GetMiniAppAdaptShareInfo(reqParam);
return { return {
data: payload.rawArkData ? arkData : MiniAppInfoHelper.RawToSend(arkData) data: payload.rawArkData === 'true' ? arkData : MiniAppInfoHelper.RawToSend(arkData)
}; };
} }
} }

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,43 +1,43 @@
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> { class OCRImageBase extends OneBotAction<Payload, any> {
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字段可能格式不正确`);
} }
if (path) { if (path) {
await checkFileExist(path, 5000); // 避免崩溃 try {
const ret = await this.core.apis.SystemApi.ocrImage(path); await checkFileExist(path, 5000); // 避免崩溃
fs.unlink(path, () => { }); const ret = await this.core.apis.SystemApi.ocrImage(path);
if (!ret) {
if (!ret) { throw new Error(`OCR ${payload.image}失败`);
throw new Error(`OCR ${payload.file}失败`); }
return ret.result;
} finally {
fs.unlink(path, () => { });
} }
return ret.result;
} }
fs.unlink(path, () => { }); throw new Error(`OCR ${payload.image}失败, 文件可能不存在`);
throw new Error(`OCR ${payload.file}失败,文件可能不存在`);
} }
} }
export class IOCRImage extends OCRImage { export class OCRImage extends OCRImageBase {
actionName = ActionName.IOCRImage; actionName = ActionName.OCRImage;
} }
export class IOCRImage extends OCRImageBase {
actionName = ActionName.IOCRImage;
}

View File

@@ -0,0 +1,21 @@
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
cmd: Type.String(),
data: Type.String(),
rsp: Type.Union([Type.String(), Type.Boolean()], { default: true }),
});
type Payload = Static<typeof SchemaData>;
export class SendPacket extends GetPacketStatusDepends<Payload, any> {
payloadSchema = SchemaData;
actionName = ActionName.SendPacket;
async _handle(payload: Payload) {
const rsp = typeof payload.rsp === 'boolean' ? payload.rsp : payload.rsp === 'true';
const data = await this.core.apis.PacketApi.pkt.client.sendOidbPacket({ cmd: payload.cmd, data: payload.data as any }, rsp);
return typeof data === 'object' ? data.toString('hex') : undefined;
}
}

View File

@@ -1,25 +1,25 @@
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> { class SetGroupSignBase extends GetPacketStatusDepends<Payload, any> {
actionName = ActionName.SetGroupSign;
payloadSchema = SchemaData; payloadSchema = SchemaData;
async _handle(payload: Payload) { async _handle(payload: Payload) {
return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id); return await this.core.apis.PacketApi.pkt.operation.GroupSign(+payload.group_id);
} }
} }
export class SendGroupSign extends SetGroupSign {
export class SetGroupSign extends SetGroupSignBase {
actionName = ActionName.SetGroupSign;
}
export class SendGroupSign extends SetGroupSignBase {
actionName = ActionName.SendGroupSign; actionName = ActionName.SendGroupSign;
} }

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('设置在线状态失败');

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