Compare commits

...

441 Commits

Author SHA1 Message Date
手瓜一十雪
eeec905df0 fix: 反向ws 2024-11-16 20:21:38 +08:00
手瓜一十雪
0c6aac7f66 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 20:20:07 +08:00
手瓜一十雪
86d22db141 feat: remove hasBeenClosed 2024-11-16 20:15:02 +08:00
Mlikiowa
48a5d0eef3 release: v4.1.2 2024-11-16 12:14:28 +00:00
手瓜一十雪
bda174bed4 fix: 异常 2024-11-16 20:13:36 +08:00
Mlikiowa
caf98b8655 release: v4.1.1 2024-11-16 11:26:41 +00:00
手瓜一十雪
c9833c5988 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 19:25:58 +08:00
手瓜一十雪
55ef7e529e fix: 4.1.1 2024-11-16 19:25:54 +08:00
Mlikiowa
9b04ddcefd release: v4.1.0 2024-11-16 10:41:27 +00:00
手瓜一十雪
6dc4f38581 refactor: AdapterConfig 2024-11-16 18:38:44 +08:00
手瓜一十雪
93ce8bfb85 refactor: emitMsg 2024-11-16 18:31:24 +08:00
手瓜一十雪
e7d138448a refactor: reloadNetwork 2024-11-16 18:10:03 +08:00
手瓜一十雪
02c4a468cb fix 2024-11-16 16:56:34 +08:00
手瓜一十雪
d392e653e1 refactor: network 2024-11-16 16:56:20 +08:00
手瓜一十雪
e8faa09f1d refactor: fetch->RequestUtil
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 17s
Build Action / Build-Shell (push) Failing after 9s
2024-11-16 13:58:46 +08:00
手瓜一十雪
e80ed3b33e fix: 切分依赖 2024-11-16 13:47:33 +08:00
手瓜一十雪
41a346e1cf Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-16 13:44:06 +08:00
手瓜一十雪
5e19fc112a fix: ws 0.0.0. 兼容 2024-11-16 13:44:02 +08:00
手瓜一十雪
2f7aff2b56 feat: 调整配置顺序 2024-11-16 13:35:10 +08:00
手瓜一十雪
ccb0e1fb4f docs: thank tdesign 2024-11-16 13:24:19 +08:00
手瓜一十雪
d4163c913a docs: tdesign 2024-11-16 13:23:05 +08:00
手瓜一十雪
8087ba0e4a fix: webui 2024-11-16 13:20:42 +08:00
手瓜一十雪
6700523b61 feat: 网络重载日志 2024-11-16 13:18:42 +08:00
手瓜一十雪
49f1c3f9ba fix: 翻新网络重载 2024-11-16 13:07:20 +08:00
手瓜一十雪
575ab4f1d1 fix: 打包流程 2024-11-16 12:57:12 +08:00
手瓜一十雪
3658547731 Merge pull request #524 from NapNeko/refactor-config-webui
refactor: new config & vue webui & new network & new parseMsg
2024-11-16 12:53:46 +08:00
手瓜一十雪
eb6590e9e2 feat: 打包脚本 2024-11-16 12:50:45 +08:00
手瓜一十雪
83f28795f2 feat: 移除无用代码 2024-11-16 12:45:27 +08:00
手瓜一十雪
e98bfaac11 fix: error 2024-11-16 12:34:50 +08:00
手瓜一十雪
4f4bd3c6e0 fix: 抑制警告 2024-11-16 11:45:57 +08:00
手瓜一十雪
bd1faccaa8 fix: build 2024-11-16 11:44:23 +08:00
手瓜一十雪
25751b8149 fix 2024-11-16 11:39:12 +08:00
手瓜一十雪
e34b60315c fix: build 2024-11-16 11:37:47 +08:00
手瓜一十雪
046afc0c23 Merge branch 'main' into refactor-config-webui 2024-11-16 11:35:59 +08:00
手瓜一十雪
2f61ba7f25 feat: 优化上报问题 2024-11-16 11:32:27 +08:00
手瓜一十雪
8981f12b1a fix: 修复大部分逻辑 2024-11-16 11:25:16 +08:00
手瓜一十雪
34e96b1089 fix: 逻辑操作 2024-11-16 11:14:21 +08:00
手瓜一十雪
41db435ef5 fix: 样式 2024-11-16 11:08:45 +08:00
手瓜一十雪
b525fa81bb fix: 一处样式问题 2024-11-16 10:57:15 +08:00
手瓜一十雪
6382b29da8 feat: 提升交互体验 2024-11-16 10:55:26 +08:00
手瓜一十雪
8bc0403139 feat: 微调样式 2024-11-16 10:42:01 +08:00
手瓜一十雪
9f261e78c3 fix: name保存问题 2024-11-16 10:38:03 +08:00
手瓜一十雪
15d9390ee4 feat: 基础样式 2024-11-16 10:34:31 +08:00
手瓜一十雪
572b8809a5 feat: dev webui 2024-11-16 10:20:17 +08:00
pk5ls20
623799c049 fix: migrateOneBotConfigsV1 2024-11-16 07:22:29 +08:00
pk5ls20
4271acc6ab feat: add migrateOneBotConfigsV2 2024-11-16 06:52:18 +08:00
pk5ls20
609e83a824 refactor: more comprehensive dev and prod env isolation and build process 2024-11-16 06:10:36 +08:00
pk5ls20
e98910c9ff chore: adjust eslint 2024-11-16 05:50:44 +08:00
pk5ls20
c432799580 refactor: webui network 2024-11-16 05:43:44 +08:00
pk5ls20
fa87f7c8c3 fix: vite config 2024-11-16 00:13:05 +08:00
pk5ls20
4a44062814 chore: revert package.json 2024-11-15 23:41:17 +08:00
pk5ls20
fe0bda11d3 refactor: webui 2024-11-15 23:39:19 +08:00
手瓜一十雪
1ec1040e43 feat: 产物替换 2024-11-15 20:39:57 +08:00
pk5ls20
e44595334a perf: avoid silk encoding blocking the main event loop (#527)
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 13s
Build Action / Build-Shell (push) Failing after 12s
2024-11-15 20:39:18 +08:00
手瓜一十雪
f40de023b0 feat: 加入注释 2024-11-15 20:35:30 +08:00
手瓜一十雪
9799d02ad2 fix: 清除基础信息 2024-11-15 20:34:42 +08:00
手瓜一十雪
bec88fee04 feat: 修复渲染异常bug 2024-11-15 20:33:06 +08:00
手瓜一十雪
1a94e20691 fix: 关闭按钮 2024-11-15 20:26:00 +08:00
手瓜一十雪
3690307d0b fix: 添加配置 2024-11-15 20:22:42 +08:00
手瓜一十雪
2d5b4bc90a feat: 保存数据 2024-11-15 20:00:49 +08:00
手瓜一十雪
cc93ed3567 fix: shallowRef 2024-11-15 19:54:21 +08:00
手瓜一十雪
dce4988767 refactor: network 2024-11-15 19:51:19 +08:00
手瓜一十雪
5c81b60b58 feat: 渲染网络配置 2024-11-15 19:48:27 +08:00
手瓜一十雪
a668bfbc13 refactor: emitMsg 2024-11-15 18:40:35 +08:00
手瓜一十雪
bc0fc96b9b refactor: rkey get 2024-11-15 18:35:34 +08:00
手瓜一十雪
ae14692d5b refactor: parseMsg 2024-11-15 18:29:42 +08:00
手瓜一十雪
d445dc6644 refactor: 初步可用 2024-11-15 17:35:09 +08:00
手瓜一十雪
db3d435402 feat: 彻底 扬了Old WebUi 2024-11-15 16:56:53 +08:00
手瓜一十雪
7ee48f1443 feat: 迁移后端与前端 大部分逻辑 2024-11-15 16:50:12 +08:00
手瓜一十雪
a54f30acc1 feat: 翻新除了配置文件的所有代码了 2024-11-15 16:45:57 +08:00
手瓜一十雪
75e7bc7275 feat: 开始迁移webui 2024-11-15 16:10:19 +08:00
手瓜一十雪
f1b2c8b1cf fix 2024-11-15 15:24:44 +08:00
手瓜一十雪
50079e7a96 fix: 面板关于信息 2024-11-15 14:46:05 +08:00
手瓜一十雪
6d37868ae8 refactor: nnetwork -> network 2024-11-15 13:57:45 +08:00
手瓜一十雪
543961e980 feat: 调整基础样式 2024-11-15 13:52:23 +08:00
手瓜一十雪
1e2c76bb47 feat: 布局面板基础结构 2024-11-15 13:25:02 +08:00
手瓜一十雪
ddc0ed066d feat: nav 2024-11-15 13:03:18 +08:00
手瓜一十雪
6708903c65 feat: 基础逻辑拼接 2024-11-15 11:32:30 +08:00
手瓜一十雪
5ee0afb604 Merge branch 'main' into refactor-config-webui 2024-11-15 10:42:02 +08:00
手瓜一十雪
9b20e9db29 feat: 路由 2024-11-15 10:41:55 +08:00
Mlikiowa
74b4d9bf49 release: v4.0.3
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 7s
Build Action / Build-Shell (push) Failing after 8s
2024-11-14 15:34:43 +00:00
手瓜一十雪
89f7892681 fix: rkey 2024-11-14 23:30:52 +08:00
手瓜一十雪
f83bf197d2 feat: QQLogin 2024-11-14 22:02:15 +08:00
手瓜一十雪
5bcc130dd7 feat: 拆分组件 2024-11-14 21:46:54 +08:00
手瓜一十雪
4be6d8ec01 feat: 初始化webui login 2024-11-14 21:41:43 +08:00
Mlikiowa
aad5ed55d2 release: v4.0.2
Some checks are pending
Build Action / Build-LiteLoader (push) Waiting to run
Build Action / Build-Shell (push) Waiting to run
2024-11-14 12:45:02 +00:00
手瓜一十雪
86da417c17 feat: add SetMsgEmojiLike 2024-11-14 20:44:14 +08:00
手瓜一十雪
ae57ab78f3 fix: adapter api 2024-11-14 20:25:08 +08:00
手瓜一十雪
4487db4e0a feat: msg push 2024-11-14 20:18:19 +08:00
手瓜一十雪
a0a50755d3 feat: mergeOnebotConfigs 2024-11-14 18:00:31 +08:00
手瓜一十雪
621e41cc96 feat: new config helper/nnetwork 2024-11-14 17:00:34 +08:00
Mlikiowa
96b1f71437 release: v4.0.1 2024-11-14 07:43:36 +00:00
手瓜一十雪
5e0b3b2f35 fix: fluent-ffmpeg 2024-11-14 15:43:11 +08:00
pk5ls20
6829fad5bd chore: workflow build check 2024-11-14 14:30:56 +08:00
pk5ls20
7af0d9e87b refactor: simplify code 2024-11-14 14:29:38 +08:00
Mlikiowa
c089ebea99 release: v4.0.0 2024-11-14 06:00:34 +00:00
手瓜一十雪
d2a2c1c39c fix: #50 2024-11-14 13:58:25 +08:00
手瓜一十雪
ce9b09e8d1 fix: 简化代码 2024-11-14 13:49:37 +08:00
手瓜一十雪
2f6dfe51f5 fix: error 2024-11-14 13:43:39 +08:00
手瓜一十雪
bd227cd0b8 refactor: getGroupHonorInfo 2024-11-14 13:42:03 +08:00
手瓜一十雪
96003724ab refactor: nc shell login 2024-11-14 13:40:01 +08:00
pk5ls20
6a08b15095 chore: eslint 2024-11-14 13:29:39 +08:00
手瓜一十雪
dab0f9ab45 refactor: getImageUrl 2024-11-14 13:26:12 +08:00
手瓜一十雪
e733a6b69a fix: error 2024-11-14 13:22:58 +08:00
手瓜一十雪
9aca98bf13 fix: type 2024-11-14 13:19:50 +08:00
手瓜一十雪
b7c95e53dc fix: unuse import 2024-11-14 13:14:27 +08:00
手瓜一十雪
f762c450ca fix: error 2024-11-14 13:10:52 +08:00
手瓜一十雪
d58bbe53da Merge pull request #521 from abc1763613206/main
feat: add `emoji_package_id` for MarketFace
2024-11-14 13:10:04 +08:00
abc1763613206
f32edd8af7 feat: add emoji_package_id for MarketFace 2024-11-14 13:07:57 +08:00
手瓜一十雪
c747a86e5b fix 2024-11-14 13:07:10 +08:00
手瓜一十雪
abfda0dd58 fix: || -> ?? 2024-11-14 13:00:25 +08:00
手瓜一十雪
f66d7b11a8 fix: error throw 2024-11-14 12:54:58 +08:00
手瓜一十雪
f425c9478e fix 2024-11-14 12:48:19 +08:00
手瓜一十雪
756dea71fc remove: todo -> work 2024-11-14 12:44:21 +08:00
手瓜一十雪
71a6c4ccc5 fix: error handle 2024-11-14 12:37:16 +08:00
手瓜一十雪
ae2f4777ec fix 2024-11-14 12:32:48 +08:00
手瓜一十雪
dcd9b8168a feat: any listener 2024-11-14 12:28:08 +08:00
手瓜一十雪
4bb03ae5ba fix 2024-11-14 12:22:28 +08:00
手瓜一十雪
8bd6f8397b fix: assertion is unnecessary 2024-11-14 12:07:26 +08:00
手瓜一十雪
096e52d93e fix: promise async 2024-11-14 12:06:45 +08:00
手瓜一十雪
037065291d fix: 代码精简 2024-11-14 12:02:24 +08:00
手瓜一十雪
4cf52e1b13 fix: error 2024-11-14 11:53:55 +08:00
手瓜一十雪
21b228552d fix 2024-11-14 11:46:37 +08:00
手瓜一十雪
76b404cdd8 rename: WsPacketClient 2024-11-14 11:40:16 +08:00
手瓜一十雪
937c594ff7 fix 2024-11-14 11:35:10 +08:00
手瓜一十雪
b463140de7 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-14 11:30:56 +08:00
手瓜一十雪
f518fb9214 fix: readonly 2024-11-14 11:28:06 +08:00
手瓜一十雪
1092831718 Merge pull request #520 from NapNeko/refactor-4.0.0
refactor: 4.0.0
2024-11-14 11:24:57 +08:00
手瓜一十雪
6b377416da refactor: fix 2024-11-14 11:24:00 +08:00
手瓜一十雪
8f5baa47ec refactor: log最佳实践 2024-11-14 11:10:26 +08:00
手瓜一十雪
5494ff0553 refactor: build 2024-11-14 11:03:11 +08:00
手瓜一十雪
7a4805b464 refactor: registerListen 2024-11-14 10:57:57 +08:00
手瓜一十雪
8435375810 style: lint 2024-11-14 10:53:50 +08:00
手瓜一十雪
c893ec6030 refactor: apiInit Refactor 2024-11-14 10:52:03 +08:00
手瓜一十雪
e8bf6fa0a6 style: lint 2024-11-14 10:45:16 +08:00
手瓜一十雪
f228129c19 refactor: Init Core 2024-11-14 10:43:37 +08:00
手瓜一十雪
cbf98ffb89 refactor: async Init 2024-11-14 10:36:51 +08:00
手瓜一十雪
f6067b002f refactor: Shell Init 2024-11-14 10:34:38 +08:00
手瓜一十雪
636d1103e3 refactor: 删除反撤回模块 未来合并到MoeHoo 2024-11-14 10:30:01 +08:00
手瓜一十雪
bede517f7e refactor: package 2024-11-14 10:27:10 +08:00
手瓜一十雪
16e4891b7d fix 2024-11-13 22:54:56 +08:00
手瓜一十雪
3bcd79fbb7 docs: 文档精简 2024-11-13 22:46:48 +08:00
Mlikiowa
aacf6c2917 release: v3.7.0 2024-11-13 09:53:56 +00:00
pk5ls20
92d720cd57 Merge pull request #512 from NapNeko/refactor/packet-2
refactor: packet
2024-11-13 17:51:00 +08:00
pk5ls20
2ea025047f feat: comment out logic that is not currently needed 2024-11-13 17:48:42 +08:00
pk5ls20
f7f7e09cab feat: sysMessage oldProto adapter 2024-11-13 17:44:12 +08:00
pk5ls20
75866b435e feat: errorStack 2024-11-13 16:52:03 +08:00
手瓜一十雪
f07941685b Merge branch 'main' into refactor/packet-2 2024-11-13 16:00:45 +08:00
Mlikiowa
60a0539216 release: v3.6.17 2024-11-13 07:57:01 +00:00
手瓜一十雪
3dd4b6549f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-13 15:56:31 +08:00
手瓜一十雪
0802c35dc1 feat: mface emoji_id 2024-11-13 15:56:17 +08:00
Mlikiowa
7d9d7226ec release: v3.6.16 2024-11-13 07:55:21 +00:00
手瓜一十雪
b5ef6ce6b0 feat: mface key 扩展 2024-11-13 15:54:31 +08:00
pk5ls20
49ec6181b0 fix: macos 2024-11-13 15:35:07 +08:00
pk5ls20
783a534768 fix: NativePacketClient 2024-11-13 14:16:42 +08:00
Mlikiowa
704ac11cbb release: v3.6.15 2024-11-13 05:16:36 +00:00
手瓜一十雪
aa9663d85e fix: 移除chalk 2024-11-13 13:16:09 +08:00
Mlikiowa
05291f34fb release: v3.6.14 2024-11-13 04:31:12 +00:00
手瓜一十雪
2260fe32a1 Merge pull request #514 from NapNeko/refactor-log4js--winston
refactor: log4js to winston
2024-11-13 12:29:35 +08:00
手瓜一十雪
2c398a6832 fix: error 2024-11-13 12:29:17 +08:00
手瓜一十雪
3e1f566699 feat: 异常处理与log4js替换 2024-11-13 12:26:23 +08:00
手瓜一十雪
4f89f184b8 refactor: package json 2024-11-13 12:19:18 +08:00
Mlikiowa
787685c937 release: v3.6.13 2024-11-12 10:45:11 +00:00
手瓜一十雪
ed9cd2fe38 fix: #513 2024-11-12 18:37:44 +08:00
pk5ls20
740d80e851 chore: minor adjust packet module 2024-11-12 04:14:08 +08:00
pk5ls20
4520a20bd4 chore: remove debug output 2024-11-12 04:08:51 +08:00
pk5ls20
98c65c4923 refactor: packet x1 2024-11-12 04:02:19 +08:00
Mlikiowa
e287906a9d release: v3.6.12 2024-11-11 13:03:29 +00:00
手瓜一十雪
8bae789020 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-11 21:00:27 +08:00
手瓜一十雪
ce57b7b725 fix: #509 2024-11-11 21:00:12 +08:00
pk5ls20
1d9872195d fix: offset name 2024-11-11 20:57:30 +08:00
pk5ls20
98d1f8e29f feat: add some macOS offset 2024-11-11 20:54:57 +08:00
手瓜一十雪
221b3fb730 fix 2024-11-11 20:35:13 +08:00
Mlikiowa
90a834495a release: v3.6.11 2024-11-11 12:17:25 +00:00
手瓜一十雪
8bfd102232 fix: macos arm64 28971 2024-11-11 20:16:44 +08:00
Mlikiowa
65e784f169 release: v3.6.10 2024-11-11 02:45:39 +00:00
手瓜一十雪
0fc81c672f fix: once升级 2024-11-11 10:45:15 +08:00
手瓜一十雪
62ae0f4321 feat: 文档镜像 2024-11-10 12:40:18 +08:00
Mlikiowa
a01a0a1a18 release: v3.6.9 2024-11-10 04:11:16 +00:00
手瓜一十雪
4c30cc69ad fix: #73 2024-11-10 12:10:43 +08:00
Mlikiowa
1d43b75df4 release: v3.6.8 2024-11-09 10:22:17 +00:00
手瓜一十雪
d02afdfc3e fix: 缓存 2024-11-09 18:21:50 +08:00
Mlikiowa
5d6dee9fd0 release: v3.6.7 2024-11-09 04:36:58 +00:00
手瓜一十雪
60c67ef41c Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-09 12:36:21 +08:00
手瓜一十雪
917d7c1f19 feat: user boot script 2024-11-09 12:35:50 +08:00
Mlikiowa
ad19f2c99e release: v3.6.6 2024-11-09 04:22:55 +00:00
手瓜一十雪
8a61f5a03f fix: #503 2024-11-09 12:22:28 +08:00
手瓜一十雪
8c164910f6 docs: 因为优点太多所以只好去掉几条了 2024-11-08 16:23:20 +08:00
pk5ls20
a560d3d266 chore: eslint migrate 2024-11-08 15:43:27 +08:00
手瓜一十雪
532f739272 Merge pull request #501 from NapNeko/eslint9
feat: eslint9
2024-11-08 12:38:12 +08:00
手瓜一十雪
a120727f2d feat: eslint9 2024-11-08 12:36:25 +08:00
手瓜一十雪
a9bcb830a8 feat: 迁移29456为标准版本 2024-11-08 12:14:15 +08:00
Mlikiowa
56e5f0033f release: v3.6.5 2024-11-08 01:20:08 +00:00
手瓜一十雪
101106996a feat: 29456 2024-11-08 09:18:40 +08:00
pk5ls20
41a81534dc feat: support 29456 2024-11-08 00:26:09 +08:00
Mlikiowa
1425e8f229 release: v3.6.4 2024-11-07 08:14:56 +00:00
手瓜一十雪
75bb1d2193 Merge pull request #499 from kanocence/main
修复 config 页面样式
2024-11-07 16:14:01 +08:00
手瓜一十雪
2a23820f9b Merge pull request #500 from Stapxs/patch-1
fix: 在处理 file uri 时可能会意外忽略 fragment 段
2024-11-07 16:13:06 +08:00
林小槐
2ee0fed047 fix: 在处理 file uri 时可能会意外忽略 fragment 段
在处理类似 C:\\test\\test#1.txt 时 #1.txt 由于为 url fragment 而被意外截断
2024-11-07 16:10:59 +08:00
kanocence
40be6b9c43 Merge branch 'NapNeko:main' into main 2024-11-07 15:34:19 +08:00
kanocence
a06b3f0246 feat: 💄 修改页面样式 2024-11-07 15:33:57 +08:00
Mlikiowa
4787fa53b4 release: v3.6.3 2024-11-07 04:16:13 +00:00
手瓜一十雪
a06158bf01 fix: 标准化接口 2024-11-07 12:15:49 +08:00
pk5ls20
314e7485b8 chore: format 2024-11-07 10:33:01 +08:00
pk5ls20
aed5d2d9f0 chore: log 2024-11-07 10:31:50 +08:00
pk5ls20
f44e48a28b fix: remove useless import 2024-11-06 16:58:12 +08:00
手瓜一十雪
38be90450c feat: 兼容gocq标准 2024-11-06 16:48:18 +08:00
手瓜一十雪
2dd57d7676 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-06 16:45:04 +08:00
手瓜一十雪
6b3b163fa8 fix: 错误代码 2024-11-06 16:45:01 +08:00
Mlikiowa
9792ebafdc release: v3.6.2 2024-11-06 08:00:05 +00:00
手瓜一十雪
d10e7c37cb fix: link 2024-11-06 15:59:39 +08:00
Mlikiowa
d38f1853a4 release: v3.6.1 2024-11-06 06:42:33 +00:00
手瓜一十雪
bdec16266e fix: #498 2024-11-06 14:42:05 +08:00
Mlikiowa
49ca698ab9 release: v3.6.0 2024-11-06 03:30:17 +00:00
pk5ls20
3efd8163c9 fix: MoeHoo-Linux Amd64 2024-11-06 11:28:57 +08:00
pk5ls20
cc2d11449c release: v3.5.2 2024-11-06 09:28:14 +08:00
pk5ls20
7e9c19ca5b fix: MoeHoo-Linux Arm64 2024-11-06 09:26:15 +08:00
Mlikiowa
3b01b6827f release: v3.5.1 2024-11-05 14:45:36 +00:00
手瓜一十雪
8d9ef851ba fix: linux arm64 2024-11-05 22:45:00 +08:00
手瓜一十雪
b070bc59bc fix: MoeHoo-Linux Amd64 2024-11-05 22:36:47 +08:00
Mlikiowa
8d663946e1 release: v3.5.0 2024-11-05 14:13:33 +00:00
pk5ls20
2a2328b029 feat: better edge case handling 2024-11-05 22:11:01 +08:00
pk5ls20
efc9064abb fix: better log 2024-11-05 21:54:52 +08:00
Mlikiowa
dd70adf071 release: v3.4.11 2024-11-05 13:52:22 +00:00
pk5ls20
0f427375cb fix: sendCommand 2024-11-05 21:48:39 +08:00
Mlikiowa
4001270b93 release: v3.4.10 2024-11-05 13:34:25 +00:00
手瓜一十雪
e7f5ed3bcc Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-05 21:33:51 +08:00
手瓜一十雪
05cdc37d0a fix: qqnt 29271 最新版兼容问题 2024-11-05 21:33:36 +08:00
Mlikiowa
27920e0bee release: v3.4.9 2024-11-05 13:22:43 +00:00
手瓜一十雪
ae409b7249 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-05 21:22:10 +08:00
手瓜一十雪
8276258348 fix: Once 2024-11-05 21:21:59 +08:00
Mlikiowa
1bf96a97a5 release: v3.4.8 2024-11-05 13:18:35 +00:00
手瓜一十雪
d672680c4c feat: 复活吧我的arm64 2024-11-05 21:17:07 +08:00
手瓜一十雪
b89f2805e7 feat: linux.x64'packet 2024-11-05 21:12:19 +08:00
手瓜一十雪
78b4aa9295 feat: linux x64 support 2024-11-05 21:11:41 +08:00
手瓜一十雪
0a06637e78 style: lint 2024-11-05 20:59:18 +08:00
手瓜一十雪
13afa2c7ab fix: 去掉开发日志 2024-11-05 20:54:39 +08:00
手瓜一十雪
51d34d17cc feat: 去掉无用日志 2024-11-05 20:52:36 +08:00
手瓜一十雪
18a99341d5 Merge pull request #494 from NapNeko/dependabot/npm_and_yarn/vite-tsconfig-paths-5.1.0
chore(deps-dev): bump vite-tsconfig-paths from 4.3.2 to 5.1.0
2024-11-05 20:50:59 +08:00
手瓜一十雪
f01c8f0110 Merge pull request #493 from NapNeko/multi-packet
refactor: automatically select the optimal packet backend
2024-11-05 20:50:36 +08:00
手瓜一十雪
d8070eee2a fix: LL 2024-11-05 20:49:16 +08:00
手瓜一十雪
8519b7f4df update: LiteLoader 2024-11-05 20:48:28 +08:00
手瓜一十雪
591ab1b1df feat: 去掉开发输出 2024-11-05 20:40:25 +08:00
手瓜一十雪
393815b11e fix 2024-11-05 20:33:55 +08:00
dependabot[bot]
341a397bc4 chore(deps-dev): bump vite-tsconfig-paths from 4.3.2 to 5.1.0
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 4.3.2 to 5.1.0.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v4.3.2...v5.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-05 08:39:26 +00:00
pk5ls20
e46d274a75 feat: add new NapCat config key: packetBackend
- Acceptable values: `native`, `frida`, `auto`, `disable`
- Default value is set to `auto`
2024-11-05 14:45:02 +08:00
pk5ls20
ad6f21980c refactor: auto judge client 2024-11-05 14:24:54 +08:00
pk5ls20
017b8b7f15 chore: better log 2024-11-05 13:52:56 +08:00
pk5ls20
9b448b17e6 refactor: NapProto -> https://github.com/NapNeko/NapProto 2024-11-05 12:47:28 +08:00
手瓜一十雪
f9996a9987 fix: 日志乱飞版本 2024-11-05 11:24:36 +08:00
手瓜一十雪
000ef55273 fix: 一点小问题 2024-11-05 10:25:41 +08:00
手瓜一十雪
e1ac0f02b4 fix: 搞炸了 让我思考下 2024-11-05 10:22:52 +08:00
手瓜一十雪
b9297e3f1d fix 2024-11-05 10:18:11 +08:00
手瓜一十雪
34d0669ca8 fix 2024-11-05 10:16:06 +08:00
手瓜一十雪
25e42720cf fix: 开始初步调试 2024-11-05 10:14:00 +08:00
手瓜一十雪
f7c1951191 fix: 一些异常类型 2024-11-05 10:07:56 +08:00
pk5ls20
479b971b0c refactor: automatically select the optimal packet backend 2024-11-04 23:52:52 +08:00
手瓜一十雪
347ba5f354 feat: 初步封装 NativePacketClient 2024-11-04 21:09:11 +08:00
pk5ls20
81dbb9d980 perf: use cache in NapProto 2024-11-04 14:42:16 +08:00
pk5ls20
c4e1a3ab04 fix: SendGroupAiRecord wrong return id 2024-11-04 14:41:16 +08:00
pk5ls20
90ec774a21 feat: better mface toPreview 2024-11-03 17:29:50 +08:00
手瓜一十雪
db7a27e624 style: lint 2024-11-03 12:13:56 +08:00
Mlikiowa
f7d965eda2 release: v3.4.7 2024-11-03 01:50:09 +00:00
手瓜一十雪
74ca2e2e16 Merge pull request #485 from clansty/revert/get_forward_msg
revert: 还原 ob11 风格 get_forward_msg
2024-11-03 09:48:07 +08:00
Clansty
8ab550f2f5 revert: 还原 ob11 风格 get_forward_msg 2024-11-03 09:44:35 +08:00
pk5ls20
018aca4db2 fix: type hint 2024-11-03 02:45:58 +08:00
Mlikiowa
d4327166c1 release: v3.4.6 2024-11-02 05:20:34 +00:00
手瓜一十雪
fa25d2e779 feat: arm64 29271 2024-11-02 13:19:13 +08:00
手瓜一十雪
3ce1c3f0ec feat: support 3.2.13-29271-x64 2024-11-02 10:58:36 +08:00
手瓜一十雪
96dff5141e feat: packet 29271 2024-11-02 10:46:15 +08:00
手瓜一十雪
78d85d9965 feat: 29271 2024-11-02 10:02:39 +08:00
手瓜一十雪
37ec455b02 Merge pull request #484 from NapNeko/feat/ai-voice
feat: ai voice
2024-11-02 08:15:05 +08:00
pk5ls20
6ab82739a6 feat: ai voice 2024-11-02 01:51:57 +08:00
手瓜一十雪
a36917e7c0 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-01 12:13:24 +08:00
手瓜一十雪
21f3428b36 feat: support 6.9.58-28971 2024-11-01 12:13:04 +08:00
Mlikiowa
f8a487db25 release: v3.4.5 2024-10-31 12:30:25 +00:00
手瓜一十雪
73a859be04 fix: report self 2024-10-31 20:30:02 +08:00
手瓜一十雪
63bcee01a1 fix: report self 2024-10-31 20:27:17 +08:00
Mlikiowa
85b4966ba8 release: v3.4.4 2024-10-31 11:32:42 +00:00
手瓜一十雪
36c2c567b7 fix: #444 2024-10-31 19:32:10 +08:00
Mlikiowa
7b1ac224f6 release: v3.4.3 2024-10-31 10:18:21 +00:00
手瓜一十雪
34d9f04f15 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-31 18:17:53 +08:00
手瓜一十雪
be5da7cc6f fix: 正向ws异常推送事件问题 2024-10-31 18:17:41 +08:00
Mlikiowa
8d32ccb5d4 release: v3.4.2 2024-10-31 10:03:19 +00:00
手瓜一十雪
6acceb884c fix: #473 2024-10-31 18:00:55 +08:00
Hao Guan
4c834fd640 chore: Major获取Appid添加提示 (#480) 2024-10-31 16:10:27 +08:00
Mlikiowa
301278c7a9 release: v3.4.1 2024-10-31 00:36:50 +00:00
凌莞~(=^▽^=)
42ee83c54f feat: GetStrangerInfo 加回以前的完整信息 (#479) 2024-10-31 07:25:13 +08:00
Mlikiowa
e631f69621 release: v3.4.0 2024-10-30 13:11:06 +00:00
Nepenthe
ce8760a39a 修复<get_record>接口 (#478) 2024-10-30 21:09:32 +08:00
Mlikiowa
ff952956de release: v3.3.27 2024-10-30 07:28:17 +00:00
手瓜一十雪
28f3ff4971 fix: reply msg 大坐牢 #452 #477 2024-10-30 15:27:26 +08:00
手瓜一十雪
19e728c3cb feat: 提升全平台兼容性 2024-10-30 13:50:47 +08:00
Mlikiowa
269773ed6b release: v3.3.26 2024-10-30 01:23:01 +00:00
Hao Guan
e0d32417e1 chore: AppID for macOS 6.9.58-28971 (#476) 2024-10-30 09:20:32 +08:00
pk5ls20
9fa6083bed refactor: kill any (#475)
* refactor: kill any stage 1

* refactor: kill any stage 2

* refactor: kill any stage 3
2024-10-30 09:10:30 +08:00
手瓜一十雪
4d2fccdfb4 style: lint 2024-10-29 18:48:20 +08:00
Mlikiowa
c1c4bdfe94 release: v3.3.25 2024-10-29 10:42:12 +00:00
手瓜一十雪
8a0e9e8b61 release: v3.3.25 2024-10-29 18:41:39 +08:00
手瓜一十雪
1190e14171 docs: 调整文档优先级 2024-10-29 14:25:36 +08:00
Mlikiowa
00292b177a release: v3.3.22 2024-10-29 02:56:37 +00:00
手瓜一十雪
88de57f984 Merge pull request #472 from pohgxz/main
完善<set_input_status>接口
2024-10-29 10:53:29 +08:00
手瓜一十雪
61ddf38892 fix: Error 2024-10-29 10:52:50 +08:00
Nepenthe
52b3540ec3 修改<get_profile_like>接口 2024-10-29 07:51:16 +08:00
Nepenthe
5f831958c3 完善<set_input_status>接口 2024-10-28 23:21:49 +08:00
手瓜一十雪
c3d4698af3 try fix: error 2024-10-28 21:34:13 +08:00
Mlikiowa
bd6e83217d release: v3.3.21 2024-10-28 04:05:30 +00:00
pk5ls20
50ec49d9a2 feat: GetMiniAppArk 2024-10-28 10:12:24 +08:00
pk5ls20
dc3a089070 chore: rename msg to message in packet module 2024-10-28 07:59:24 +08:00
Mlikiowa
530e380178 release: v3.3.20 2024-10-27 14:46:24 +00:00
手瓜一十雪
10e4387add fix: script 2024-10-27 22:45:51 +08:00
手瓜一十雪
e925bc3aa8 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 22:44:04 +08:00
手瓜一十雪
427b3a7560 release: v3.3.18 2024-10-27 22:43:55 +08:00
Version
c8da950725 chore:version change 2024-10-27 14:42:23 +00:00
手瓜一十雪
743c5b8196 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 22:41:56 +08:00
手瓜一十雪
5e62abea57 fix: version 控制 2024-10-27 22:41:46 +08:00
Version
6bfc545582 chore:version change 2024-10-27 14:37:13 +00:00
手瓜一十雪
411108a2d2 fix: version check 2024-10-27 22:36:48 +08:00
Version
308a6fa9e4 chore:version change 2024-10-27 14:33:40 +00:00
Version
2dc7b785d0 chore:version change 2024-10-27 14:33:19 +00:00
手瓜一十雪
0e69e9e839 fix: checkVersion 2024-10-27 22:32:52 +08:00
手瓜一十雪
b83229b5da feat: 自动化版本发布控制 2024-10-27 22:30:01 +08:00
手瓜一十雪
6f053f5f7d feat: 我补药要手动release啦! 2024-10-27 22:20:11 +08:00
手瓜一十雪
c3dc53eaaf release: v3.3.12 2024-10-27 22:14:17 +08:00
手瓜一十雪
ffdc34cfe2 Merge pull request #470 from pohgxz/main
修复<get_group_at_all_remain>接口总是返回<Error: atInfo not found>
2024-10-27 22:10:55 +08:00
手瓜一十雪
4825a0e341 fix: type Error 2024-10-27 22:07:11 +08:00
Nepenthe
95a00d7f35 修复<get_group_at_all_remain>接口总是返回<Error: atInfo not found> 2024-10-27 22:04:48 +08:00
手瓜一十雪
d885bab426 feat: 类型修复 2024-10-27 22:03:22 +08:00
手瓜一十雪
e2a6a0bc02 release: v3.2.12 2024-10-27 20:56:27 +08:00
手瓜一十雪
ff7d8609ce fix: #452 修复seq搜索的老问题 可能修好了 2024-10-27 20:52:09 +08:00
手瓜一十雪
7507b90e03 fix: #458 2024-10-27 20:38:02 +08:00
手瓜一十雪
2b226a4b27 release: v3.1.11 2024-10-27 19:29:42 +08:00
手瓜一十雪
8b0232c4fe fix: error 2024-10-27 11:17:01 +08:00
手瓜一十雪
0728ee9ad6 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 11:09:00 +08:00
手瓜一十雪
8c6f04d0bc feat: #469 回收连接(未测试) 2024-10-27 11:08:48 +08:00
pk5ls20
c67fad789e fix: compatibility bigint 2024-10-27 10:50:48 +08:00
手瓜一十雪
4072339d70 release: v3.1.10 2024-10-27 10:03:07 +08:00
手瓜一十雪
3a244f5804 style: lint 2024-10-27 10:02:42 +08:00
pk5ls20
f12cf59137 feat: enhance compatibility of upload_forward_msg with go-cqhttp 2024-10-27 09:59:38 +08:00
手瓜一十雪
c76f556a11 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-27 09:44:26 +08:00
手瓜一十雪
e0f3d07b98 release: 3.1.9 2024-10-27 09:44:14 +08:00
手瓜一十雪
378d85dc67 Merge pull request #468 from NapNeko/refactor/msg-element
refactor: core msg entity & packet msg converter & resolve #455
2024-10-27 09:40:41 +08:00
pk5ls20
875e91fc0e chore: simplify logic 2024-10-27 09:37:17 +08:00
pk5ls20
15f7cd9814 feat: better fake forwardMsg logic & display 2024-10-27 09:33:20 +08:00
pk5ls20
1eb5cd6237 fix: downloadRawMsgMedia edge case 2024-10-27 09:04:24 +08:00
pk5ls20
ad2f843c8f fix: downloadRawMsgMedia 2024-10-27 07:31:32 +08:00
pk5ls20
8e550e216e chore: i18n for packet log messages 2024-10-27 07:04:53 +08:00
pk5ls20
9f07b07c82 feat: support node id in fake forward (with auto download richMedia in reference message) 2024-10-27 06:50:59 +08:00
pk5ls20
0be6effc32 feat: support node id in fake forward (broken impl) 2024-10-27 05:19:53 +08:00
pk5ls20
7ab6a10fc9 refactor & fix: refactor msg entity & adjust some wrong definition 2024-10-27 04:16:15 +08:00
手瓜一十雪
fb09af0e64 release: v3.1.8 2024-10-26 21:25:03 +08:00
手瓜一十雪
0d99d30b2d feat: version hint 2024-10-26 21:24:24 +08:00
手瓜一十雪
0000ec8b5b fix: fetchFavEmojiList 2024-10-26 21:15:11 +08:00
手瓜一十雪
0085bd8a1f fix: q-gate 2024-10-26 20:46:38 +08:00
手瓜一十雪
617139dfa4 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-26 20:36:59 +08:00
手瓜一十雪
4eb4a612d0 fix: type 2024-10-26 20:25:34 +08:00
pk5ls20
cda5e784f6 fix: payload basic check in GetPacketStatusDepends 2024-10-26 19:51:43 +08:00
手瓜一十雪
d93a280ab3 fix: 进一步getNextMemberList 2024-10-26 18:40:21 +08:00
手瓜一十雪
f7e2b3a4a7 feat: new function 2024-10-26 18:10:08 +08:00
手瓜一十雪
39d9c8fa74 release: v3.1.7 2024-10-26 16:26:30 +08:00
手瓜一十雪
8823895a03 Merge pull request #466 from cnxysoft/upmain
perf: 群成员拉取
2024-10-26 16:18:17 +08:00
手瓜一十雪
b44a9e696c Merge branch 'main' into pr/466 2024-10-26 15:47:13 +08:00
手瓜一十雪
cf28a3dc17 fix: ai solve 2024-10-26 10:49:18 +08:00
手瓜一十雪
7416e6caf6 feat: GoCQHTTPDeleteFriend 2024-10-26 10:36:41 +08:00
手瓜一十雪
90f6896f3c feat: GoCQ兼容性提高 2024-10-26 10:22:04 +08:00
Alen
eebcd0700d Merge branch 'main' into upmain 2024-10-26 07:22:25 +08:00
Alen
133eee0c66 perf: 群成员拉取
getgroupmemberlist启用no_cache
2024-10-26 07:20:40 +08:00
pk5ls20
640fb75f74 feat: support for customizing the timestamp of fake forwardMsg 2024-10-26 04:06:42 +08:00
Alen
51dcc1add6 Merge branch 'main' into upmain 2024-10-25 23:58:18 +08:00
Alen
730c928f91 Merge pull request #465 from cnxysoft/upmain
refactor: 群成员列表获取
2024-10-25 23:49:27 +08:00
Alen
c3b7e111b9 style: 2024-10-25 22:26:18 +08:00
pk5ls20
1874e48925 Merge pull request #464 from clansty/feat/nested-forward
feat: 嵌套合并转发消息
2024-10-25 22:13:32 +08:00
pk5ls20
e7a082c91c feat: better recursive parsing with depth limits 2024-10-25 22:10:24 +08:00
Alen
5d4f45407e fix: 群成员拉取 2024-10-25 21:50:19 +08:00
Clansty
17c37ec32f feat: 嵌套合并转发消息 2024-10-25 19:37:04 +08:00
手瓜一十雪
b5f8140c79 feat: v3 Logo 2024-10-25 19:31:44 +08:00
手瓜一十雪
63f746c237 style: lint 2024-10-25 18:09:41 +08:00
手瓜一十雪
dac6709f27 feat: 6.9.56-28418-mac 2024-10-25 17:57:28 +08:00
手瓜一十雪
470c8d0b29 release: v3.1.6 2024-10-25 17:44:30 +08:00
Wesley F. Young
b0d35e803b update: bump express version to 5.0.0 (Why use beta.2?) 2024-10-25 09:59:02 +08:00
pk5ls20
a71475be8b feat: allow pass string user_id in handleForwardedNodesPacket 2024-10-25 09:17:16 +08:00
pk5ls20
b9f2cc5142 feat: reject >100MB video highway upload 2024-10-25 08:59:56 +08:00
pk5ls20
2d46e55b9b feat: better highway upload log 2024-10-25 08:54:18 +08:00
pk5ls20
684e254996 feat: make PacketMsgPttElement invalid 2024-10-25 08:33:26 +08:00
手瓜一十雪
a2f7903960 Merge pull request #460 from NapNeko/feat/packet-more
feat: support more element in proto
2024-10-25 08:26:52 +08:00
pk5ls20
c0c757d6bd Merge branch 'main' into feat/packet-more 2024-10-25 08:17:28 +08:00
pk5ls20
da0fad743d feat: maybe more stable fake forwardMsg 2024-10-25 08:09:17 +08:00
手瓜一十雪
80b10d6025 Merge pull request #463 from clansty/feat/custom-forward-display
feat: 自定义合并转发外显信息
2024-10-25 08:05:41 +08:00
pk5ls20
a27c2a69c4 feat: maybe more stable fake forwardMsg 2024-10-25 07:27:35 +08:00
pk5ls20
9ed2a2fd19 refactor: simplify oidb packet pack & send 2024-10-25 06:48:01 +08:00
pk5ls20
aa9d96718c refactor: outer calculation 2024-10-25 05:54:46 +08:00
pk5ls20
aa67a2b71c chore: cv多了( 2024-10-25 05:17:01 +08:00
pk5ls20
d3405edd42 refactor: packet highway & etc, kill some todo 2024-10-25 05:11:10 +08:00
Clansty
3612098d62 feat: 自定义合并转发外显信息 2024-10-25 02:42:50 +08:00
Alen
2f08b72d69 fix: 群成员拉取 2024-10-24 23:00:38 +08:00
手瓜一十雪
ab66904c1a feat: 3.2.13-28971-arm64 2024-10-24 21:57:54 +08:00
手瓜一十雪
55542a3dbe feat: 28971 Linux 2024-10-24 20:40:58 +08:00
手瓜一十雪
8569a45114 release: v3.1.5 2024-10-24 20:16:12 +08:00
手瓜一十雪
c790311fc3 release: v3.1.5 2024-10-24 20:11:39 +08:00
手瓜一十雪
3c45c8bd80 feat: 28971 2024-10-24 20:11:07 +08:00
手瓜一十雪
d5b7b3ae31 feat: ntappid 2024-10-24 17:55:33 +08:00
手瓜一十雪
43e73a5f24 doc: big Logo 2024-10-24 17:03:23 +08:00
手瓜一十雪
698947ed97 Merge branch 'main' into feat/packet-more 2024-10-24 14:00:17 +08:00
手瓜一十雪
f3d967ae07 release: 3.1.4 2024-10-24 13:39:05 +08:00
手瓜一十雪
dbe72fa07e feat: SetGroupSign 2024-10-24 13:38:22 +08:00
pk5ls20
801a97d85b chore: remove useless log 2024-10-24 04:58:53 +08:00
pk5ls20
9f8f938c47 feat: build & upload file 2024-10-24 04:53:41 +08:00
Wesley F. Young
8fe37d1c1e chore: reformat package.json 2024-10-23 17:54:12 +08:00
pk5ls20
5cca8457e7 chore: 有笨蛋 2024-10-23 16:33:05 +08:00
pk5ls20
e9332e7646 feat: add ptt msg pack & upload 2024-10-23 16:12:31 +08:00
手瓜一十雪
31365505d8 Merge pull request #461 from huankong233/main
优化 contact 支持群聊和私聊
2024-10-23 09:09:07 +08:00
huankong233
b3fbe9e34a 优化 contact 支持群聊和私聊 2024-10-23 09:05:45 +08:00
pk5ls20
4082b651c5 feat & fix: add video msg pack & upload, fix some bugs in uploading c2c elements 2024-10-23 06:14:48 +08:00
Alen
0081000ef0 Merge branch 'main' into upmain 2024-10-23 01:08:56 +08:00
Alen
ad4d6a1070 refactor: 群成员获取 2024-10-23 01:07:52 +08:00
手瓜一十雪
5190b26399 Merge pull request #457 from huankong233/main
删除一些过时的接口
2024-10-22 17:57:04 +08:00
手瓜一十雪
29a8db96f4 fix 2024-10-22 17:56:51 +08:00
huankong233
1a4c2cabfd 删除一些过时的接口 2024-10-22 16:45:52 +08:00
手瓜一十雪
ef9189055c release: 3.1.3 2024-10-22 12:43:54 +08:00
手瓜一十雪
5cc3719125 fix: rkey 2024-10-22 12:42:24 +08:00
手瓜一十雪
5d46f41348 fix: dep 2024-10-22 12:14:43 +08:00
手瓜一十雪
3c2c1963f4 release: 3.1.2 2024-10-22 12:11:02 +08:00
手瓜一十雪
4896ca9279 fix 2024-10-22 11:37:01 +08:00
手瓜一十雪
f0afba6cd9 fix: GetOnlineClient 2024-10-22 11:34:28 +08:00
手瓜一十雪
bd717c298a fix: get_online_clients 2024-10-22 11:17:39 +08:00
手瓜一十雪
baaa8a70dc release: 3.1.1 2024-10-22 11:08:24 +08:00
手瓜一十雪
6d561c6e6f Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-10-22 11:04:43 +08:00
手瓜一十雪
e6b6947d49 feat: 标准化凭据获取 2024-10-22 11:04:28 +08:00
手瓜一十雪
52e99a2175 Merge pull request #454 from huankong233/main
简单修复一些小问题
2024-10-22 10:17:08 +08:00
手瓜一十雪
052d17a46f fix: getfile 2024-10-22 10:15:16 +08:00
huankong233
1aa1f4c212 优化 getFile 处理逻辑 2024-10-22 09:48:55 +08:00
huankong233
c3a48e3344 修复标记好友/群聊信息已读逻辑 2024-10-21 19:51:17 +08:00
手瓜一十雪
1d5483dc28 Merge pull request #451 from huankong233/main
对接口顺序和文档同步
2024-10-21 16:47:46 +08:00
huankong233
54277fa0df CleanCache 未实现 2024-10-21 16:09:27 +08:00
huankong233
ab04bd262f 对接口顺序和文档同步 2024-10-21 15:49:57 +08:00
手瓜一十雪
fb23087b65 release: 3.1.0 2024-10-21 14:35:57 +08:00
手瓜一十雪
846fee7ac8 fix: error import 2024-10-21 14:33:12 +08:00
手瓜一十雪
977eacc679 try: fix arm64 2024-10-21 14:12:10 +08:00
手瓜一十雪
dacfefe644 style: lint 2024-10-21 10:17:31 +08:00
pk5ls20
345e941e11 chore: remove unnecessary comments 2024-10-21 04:10:53 +08:00
pk5ls20
6cb7d45464 feat & refactor: decouple the forwardMsg construction logic and implement the OB11 element conversion for the forward node. 2024-10-21 04:05:02 +08:00
301 changed files with 9650 additions and 7130 deletions

View File

@@ -1,64 +0,0 @@
module.exports = {
'env': {
'browser': true,
'es2021': true,
'node': true
},
'ignorePatterns': ['src/core/proto/'],
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
'overrides': [
{
'env': {
'node': true
},
'files': [
'.eslintrc.{js,cjs}'
],
'parserOptions': {
'sourceType': 'script'
}
}
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 'latest',
'sourceType': 'module'
},
'plugins': [
'@typescript-eslint',
'import'
],
'settings': {
'import/parsers': {
'@typescript-eslint/parser': ['.ts']
},
'import/resolver': {
'typescript': {
'alwaysTryTypes': true
}
}
},
'rules': {
'indent': [
'error',
4
],
'linebreak-style': [
'error',
'unix'
],
'semi': [
'error',
'always'
],
'no-unused-vars': 'off',
'no-async-promise-executor': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-var-requires': 'off',
'object-curly-spacing': ['error', 'always'],
}
};

View File

@@ -1,5 +1,8 @@
name: "Build Action"
on:
push:
branches:
- main
workflow_dispatch:
permissions: write-all
@@ -22,6 +25,9 @@ jobs:
- name: Build NuCat Framework
run: |
npm i
cd napcat.webui
npm i
cd ..
npm run build:framework
cd dist
npm i --omit=dev
@@ -49,6 +55,9 @@ jobs:
- name: Build NuCat LiteLoader
run: |
npm i
cd napcat.webui
npm i
cd ..
npm run build:shell
cd dist
npm i --omit=dev

View File

@@ -49,6 +49,9 @@ jobs:
- name: Build NuCat Framework
run: |
npm i
cd napcat.webui
npm i
cd ..
npm run build:framework
cd dist
npm i --omit=dev
@@ -78,6 +81,9 @@ jobs:
- name: Build NuCat Shell
run: |
npm i
cd napcat.webui
npm i
cd ..
npm run build:shell
cd dist
npm i --omit=dev

View File

@@ -1,29 +1,26 @@
<div align="center">
<img src="https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Flogo.png&name=1&owner=1&pattern=Diagonal%20Stripes&stargazers=1&theme=Auto" alt="NapCatQQ" width="640" height="320" />
![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)
</div>
---
## 欢迎回
NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
## 欢迎回
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 猫猫技能
- [x] **超高性能**:轻松数千群聊 独创消息队列
- [x] **启动方式**支持以无头、LiteLoader 插件、仅 QQ GUI 三种方式启动
- [x] **覆盖平台**: 覆盖 Windows / Linux (可选 Docker) / Android Termux / MacOS
- [x] **安装简单**: 支持一键脚本/程序自动部署/镜像部署等多种覆盖范围
- [x] **超低占用**:无头模式占用资源极低,适合在服务器上运行
- [x] **超多接口**:实现大部分 OneBot 和 go-cqhttp 接口,超多扩展 API
- [x] **远程管理**:自带 WebUI 支持,远程管理更加便捷
- [x] **扩展支持**:基于 MoeHoo 的Native 可实现发包与收包
## 碎碎叨叨
- [x] **安装简单**:就算是笨蛋也能使用
- [x] **性能友好**:就算是低内存也能使用
- [x] **接口丰富**:就算是没有也能使用
- [x] **稳定好用**:就算是被捉也能使用
## 使用猫猫
## 使用框架
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程
### 文档地址
[Github.IO](https://napneko.github.io/)
[Cloudflare.Worker](https://doc.napneko.icu/)
@@ -31,22 +28,24 @@ NapCatQQ (aka 猫猫框架) 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.Pages](https://napneko.pages.dev/)
[Server.China](https://napneko.com/)
[Server.Other](https://napcat.cyou/)
[Github.IO](https://napneko.github.io/)
## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
[Telegram Link](https://t.me/+nLZEnpne-pQ1OWFl)
## 感谢他们
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
## 猫猫朋友
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 开源附加
## 约法三章
> [!CAUTION]\
> **请不要在 QQ 官方群聊和任何影响力较大的简中互联网平台(包括但不限于: 哔哩哔哩,微博,知乎,抖音等)发布和讨论*任何*与本项目存在相关性的信息**
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经授权二次分发或基于 NapCat 代码开发。**
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**

70
eslint.config.mjs Normal file
View File

@@ -0,0 +1,70 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import _import from "eslint-plugin-import";
import { fixupPluginRules } from "@eslint/compat";
import globals from "globals";
import tsParser from "@typescript-eslint/parser";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
const compat = new FlatCompat({
baseDirectory: dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default [{
ignores: ["src/core/proto/"],
}, ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"), {
plugins: {
"@typescript-eslint": typescriptEslint,
import: fixupPluginRules(_import),
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
rules: {
indent: ["error", 4],
semi: ["error", "always"],
"no-unused-vars": "off",
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"object-curly-spacing": ["error", "always"],
},
}, {
files: ["**/.eslintrc.{js,cjs}"],
languageOptions: {
globals: {
...globals.node,
},
ecmaVersion: 5,
sourceType: "commonjs",
},
}];

Binary file not shown.

View File

@@ -0,0 +1,32 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
pause

View File

@@ -0,0 +1,33 @@
@echo off
chcp 65001
set NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json
set NAPCAT_LOAD_PATH=%cd%\loadNapCat.js
set NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll
set NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe
set NAPCAT_MAIN_PATH=%cd%\napcat.mjs
:loop_read
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
set RetString=%%b
goto :napcat_boot
)
:napcat_boot
for %%a in ("%RetString%") do (
set "pathWithoutUninstall=%%~dpa"
)
SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath%
pause
exit /b
)
set NAPCAT_MAIN_PATH=%NAPCAT_MAIN_PATH:\=/%
echo (async () =^> {await import("file:///%NAPCAT_MAIN_PATH%")})() > "%NAPCAT_LOAD_PATH%"
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %1
REM "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" 123456
pause

View File

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

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

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

24
napcat.webui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
napcat.webui/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
napcat.webui/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

View File

@@ -0,0 +1,52 @@
import globals from 'globals';
import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import prettier from 'eslint-plugin-prettier/recommended';
export default [
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
...ts.configs.recommended,
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
},
},
...vue.configs['flat/base'],
{
files: ['*.vue', '**/*.vue'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
},
{
rules: {
indent: ['error', 4],
semi: ['error', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-var-requires': 'warn',
'object-curly-spacing': ['error', 'always'],
'vue/v-for-delimiter-style': ['error', 'in'],
'vue/require-name-property': 'warn',
'vue/prefer-true-attribute-shorthand': 'warn',
'prefer-arrow-callback': 'warn',
},
},
prettier,
{
rules: {
'prettier/prettier': 'warn',
},
},
];

13
napcat.webui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NapCat WebUI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

31
napcat.webui/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "napcat.webui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"webui:lint": "eslint . --fix",
"webui:dev": "vite",
"webui:build": "vue-tsc -b && vite build",
"webui:preview": "vite preview"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"qrcode": "^1.5.4",
"tdesign-vue-next": "^1.10.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@types/qrcode": "^1.5.5",
"@vitejs/plugin-vue": "^5.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"typescript": "~5.6.2",
"vite": "^5.4.10",
"vue-tsc": "^2.1.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

7
napcat.webui/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script setup lang="ts"></script>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,185 @@
import { OneBotConfig } from '../../../src/onebot/config/config';
export class QQLoginManager {
private retCredential: string;
private readonly apiPrefix: string;
//调试时http://127.0.0.1:6099/api 打包时 ../api
constructor(retCredential: string, apiPrefix: string = '../api') {
this.retCredential = retCredential;
this.apiPrefix = apiPrefix;
}
// TODO:
public async GetOB11Config(): Promise<OneBotConfig> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/GetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return ConfigResponseJson?.data as OneBotConfig;
}
}
} catch (error) {
console.error('Error getting OB11 config:', error);
}
return {} as OneBotConfig;
}
public async SetOB11Config(config: OneBotConfig): Promise<boolean> {
try {
const ConfigResponse = await fetch(`${this.apiPrefix}/OB11Config/SetConfig`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ config: JSON.stringify(config) }),
});
if (ConfigResponse.status == 200) {
const ConfigResponseJson = await ConfigResponse.json();
if (ConfigResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error setting OB11 config:', error);
}
return false;
}
public async checkQQLoginStatus(): Promise<boolean> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/CheckLoginStatus`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.isLogin;
}
}
} catch (error) {
console.error('Error checking QQ login status:', error);
}
return false;
}
public async checkWebUiLogined(): Promise<boolean> {
try {
const LoginResponse = await fetch(`${this.apiPrefix}/auth/check`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (LoginResponse.status == 200) {
const LoginResponseJson = await LoginResponse.json();
if (LoginResponseJson.code == 0) {
return true;
}
}
} catch (error) {
console.error('Error checking web UI login status:', error);
}
return false;
}
public async loginWithToken(token: string): Promise<string | null> {
try {
const loginResponse = await fetch(`${this.apiPrefix}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: token }),
});
const loginResponseJson = await loginResponse.json();
const retCode = loginResponseJson.code;
if (retCode === 0) {
this.retCredential = loginResponseJson.data.Credential;
return this.retCredential;
}
} catch (error) {
console.error('Error logging in with token:', error);
}
return null;
}
public async getQQLoginQrcode(): Promise<string> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQQLoginQrcode`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data.qrcode || '';
}
}
} catch (error) {
console.error('Error getting QQ login QR code:', error);
}
return '';
}
public async getQQQuickLoginList(): Promise<string[]> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/GetQuickLoginList`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return QQLoginResponseJson.data || [];
}
}
} catch (error) {
console.error('Error getting QQ quick login list:', error);
}
return [];
}
public async setQuickLogin(uin: string): Promise<{ result: boolean; errMsg: string }> {
try {
const QQLoginResponse = await fetch(`${this.apiPrefix}/QQLogin/SetQuickLogin`, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + this.retCredential,
'Content-Type': 'application/json',
},
body: JSON.stringify({ uin: uin }),
});
if (QQLoginResponse.status == 200) {
const QQLoginResponseJson = await QQLoginResponse.json();
if (QQLoginResponseJson.code == 0) {
return { result: true, errMsg: '' };
} else {
return { result: false, errMsg: QQLoginResponseJson.message };
}
}
} catch (error) {
console.error('Error setting quick login:', error);
}
return { result: false, errMsg: '接口异常' };
}
}

View File

@@ -0,0 +1,55 @@
<template>
<div class="dashboard-container">
<SidebarMenu :menu-items="menuItems" class="sidebar-menu" />
<div class="content">
<router-view />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import SidebarMenu from './webui/Nav.vue';
interface MenuItem {
value: string;
icon: string;
label: string;
route: string;
}
const menuItems = ref<MenuItem[]>([
{ value: 'item1', icon: 'dashboard', label: '基础信息', route: '/dashboard/basic-info' },
{ value: 'item3', icon: 'wifi-1', label: '网络配置', route: '/dashboard/network-config' },
{ value: 'item4', icon: 'setting', label: '其余配置', route: '/dashboard/other-config' },
{ value: 'item5', icon: 'system-log', label: '日志查看', route: '/dashboard/log-view' },
{ value: 'item6', icon: 'info-circle', label: '关于我们', route: '/dashboard/about-us' },
]);
</script>
<style scoped>
.dashboard-container {
display: flex;
flex-direction: row;
height: 100vh;
}
.sidebar-menu {
position: relative;
z-index: 2;
}
.content {
flex: 1;
/* padding: 20px; */
overflow: auto;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.content {
padding: 10px;
}
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="login-container">
<h2 class="sotheby-font">QQ Login</h2>
<div class="login-methods">
<t-button
id="quick-login"
class="login-method"
:class="{ active: loginMethod === 'quick' }"
@click="loginMethod = 'quick'"
>Quick Login</t-button
>
<t-button
id="qrcode-login"
class="login-method"
:class="{ active: loginMethod === 'qrcode' }"
@click="loginMethod = 'qrcode'"
>QR Code</t-button
>
</div>
<div v-show="loginMethod === 'quick'" id="quick-login-dropdown" class="login-form">
<t-select
id="quick-login-select"
v-model="selectedAccount"
placeholder="Select Account"
@change="selectAccount"
>
<t-option v-for="account in quickLoginList" :key="account" :value="account">{{ account }}</t-option>
</t-select>
</div>
<div v-show="loginMethod === 'qrcode'" id="qrcode" class="qrcode">
<canvas ref="qrcodeCanvas"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as QRCode from 'qrcode';
import { useRouter } from 'vue-router';
import { MessagePlugin } from 'tdesign-vue-next';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
const loginMethod = ref<'quick' | 'qrcode'>('quick');
const quickLoginList = ref<string[]>([]);
const selectedAccount = ref<string>('');
const qrcodeCanvas = ref<HTMLCanvasElement | null>(null);
const qqLoginManager = new QQLoginManager(localStorage.getItem('auth') || '');
let heartBeatTimer: number | null = null;
const selectAccount = async (accountName: string): Promise<void> => {
const { result, errMsg } = await qqLoginManager.setQuickLogin(accountName);
if (result) {
await MessagePlugin.success('登录成功即将跳转');
await router.push({ path: '/dashboard/basic-info' });
} else {
await MessagePlugin.error('登录失败,' + errMsg);
}
};
const generateQrCode = (data: string, canvas: HTMLCanvasElement | null): void => {
if (!canvas) {
console.error('Canvas element not found');
return;
}
QRCode.toCanvas(canvas, data, function (error: Error | null | undefined) {
if (error) {
console.error('Error generating QR Code:', error);
} else {
console.log('QR Code generated!');
}
});
};
const HeartBeat = async (): Promise<void> => {
const isLogined = await qqLoginManager.checkQQLoginStatus();
if (isLogined) {
if (heartBeatTimer) {
clearInterval(heartBeatTimer);
}
await router.push({ path: '/dashboard/basic-info' });
}
};
const InitPages = async (): Promise<void> => {
quickLoginList.value = await qqLoginManager.getQQQuickLoginList();
const qrcodeData = await qqLoginManager.getQQLoginQrcode();
generateQrCode(qrcodeData, qrcodeCanvas.value);
heartBeatTimer = window.setInterval(HeartBeat, 3000);
};
onMounted(() => {
InitPages();
});
</script>
<style scoped>
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.login-methods {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.login-method {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
}
.login-method.active {
background-color: #e6f0ff;
color: #007bff;
}
.login-form,
.qrcode {
display: flex;
flex-direction: column;
gap: 15px;
}
.qrcode {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
text-align: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="login-container">
<h2 class="sotheby-font">WebUi Login</h2>
<t-form ref="form" :data="formData" colon :label-width="0" @submit="onSubmit">
<t-form-item name="password">
<t-input v-model="formData.token" type="password" clearable placeholder="请输入Token">
<template #prefix-icon>
<lock-on-icon />
</template>
</t-input>
</t-form-item>
<t-form-item>
<t-button theme="primary" type="submit" block>登录</t-button>
</t-form-item>
</t-form>
</div>
<div class="footer">Power By NapCat.WebUi</div>
</template>
<script setup lang="ts">
import '../css/style.css';
import '../css/font.css';
import { reactive, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { LockOnIcon } from 'tdesign-icons-vue-next';
import { useRouter } from 'vue-router';
import { QQLoginManager } from '@/backend/shell';
const router = useRouter();
interface FormData {
token: string;
}
const formData: FormData = reactive({
token: '',
});
const handleLoginSuccess = async (credential: string) => {
localStorage.setItem('auth', credential);
await checkLoginStatus();
};
const handleLoginFailure = (message: string) => {
MessagePlugin.error(message);
};
const checkLoginStatus = async () => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
return;
}
const loginManager = new QQLoginManager(storedCredential);
const isWenUiLoggedIn = await loginManager.checkWebUiLogined();
console.log('isWenUiLoggedIn', isWenUiLoggedIn);
if (!isWenUiLoggedIn) {
return;
}
const isQQLoggedIn = await loginManager.checkQQLoginStatus();
if (isQQLoggedIn) {
await router.push({ path: '/dashboard/basic-info' });
} else {
await router.push({ path: '/qqlogin' });
}
};
const loginWithToken = async (token: string) => {
const loginManager = new QQLoginManager('');
const credential = await loginManager.loginWithToken(token);
if (credential) {
await handleLoginSuccess(credential);
} else {
handleLoginFailure('登录失败请检查Token');
}
};
onMounted(() => {
const url = new URL(window.location.href);
const token = url.searchParams.get('token');
if (token) {
loginWithToken(token);
}
checkLoginStatus();
});
const onSubmit = async ({ validateResult }: { validateResult: boolean }) => {
if (validateResult) {
await loginWithToken(formData.token);
} else {
handleLoginFailure('请填写Token');
}
};
</script>
<style scoped>
.login-container {
padding: 20px;
border-radius: 5px;
background-color: white;
max-width: 400px;
min-width: 300px;
position: relative;
margin: 0 auto;
}
@media (max-width: 600px) {
.login-container {
width: 90%;
min-width: unset;
}
}
.tdesign-demo-block-column {
display: flex;
flex-direction: column;
row-gap: 16px;
}
.tdesign-demo-block-column-large {
display: flex;
flex-direction: column;
row-gap: 32px;
}
.tdesign-demo-block-row {
display: flex;
column-gap: 16px;
align-items: center;
}
.sotheby-font {
font-family: Sotheby, Helvetica, monospace;
font-size: 3.125rem;
line-height: 1.2;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
margin: 0;
font-size: 0.875rem;
color: #888;
position: fixed;
bottom: 20px;
left: 0;
right: 0;
width: 100%;
background-color: white;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<t-menu theme="light" default-value="2-1" :collapsed="collapsed" class="sidebar-menu">
<template #logo> </template>
<router-link v-for="item in menuItems" :key="item.value" :to="item.route">
<t-menu-item :value="item.value" :disabled="item.disabled" class="menu-item">
<template #icon>
<t-icon :name="item.icon" />
</template>
{{ item.label }}
</t-menu-item>
</router-link>
<template #operations>
<t-button class="t-demo-collapse-btn" variant="text" shape="square" @click="changeCollapsed">
<template #icon><t-icon :name="iconName" /></template>
</t-button>
</template>
</t-menu>
</template>
<script setup lang="ts">
import { ref, defineProps } from 'vue';
type MenuItem = {
value: string;
label: string;
route: string;
icon?: string;
disabled?: boolean;
};
defineProps<{
menuItems: MenuItem[];
}>();
const collapsed = ref<boolean>(localStorage.getItem('sidebar-collapsed') === 'true');
const iconName = ref<string>(collapsed.value ? 'menu-unfold' : 'menu-fold');
const changeCollapsed = (): void => {
collapsed.value = !collapsed.value;
iconName.value = collapsed.value ? 'menu-unfold' : 'menu-fold';
localStorage.setItem('sidebar-collapsed', collapsed.value.toString());
};
</script>
<style scoped>
.sidebar-menu {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 200px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.sidebar-menu {
width: 100px; /* 移动端侧边栏宽度 */
}
}
.logo-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.menu-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,6 @@
@font-face {
font-family: 'Sotheby';
src: url('../assets/Sotheby.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@@ -0,0 +1,84 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
height: 100%;
width: 100%;
margin: 0;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

62
napcat.webui/src/main.ts Normal file
View File

@@ -0,0 +1,62 @@
import { createApp } from 'vue';
import App from './App.vue';
import {
Button as TButton,
Input as TInput,
Form as TForm,
FormItem as TFormItem,
Select as TSelect,
Option as TOption,
Menu as TMenu,
MenuItem as TMenuItem,
Icon as TIcon,
Submenu as TSubmenu,
Col as TCol,
Row as TRow,
Card as TCard,
Divider as TDivider,
Link as TLink,
List as TList,
Alert as TAlert,
Tag as TTag,
ListItem as TListItem,
Tabs as TTabs,
TabPanel as TTabPanel,
Space as TSpace,
Checkbox as TCheckbox,
Popup as TPopup,
Dialog as TDialog,
Switch as TSwitch,
} from 'tdesign-vue-next';
import { router } from './router';
import 'tdesign-vue-next/es/style/index.css';
const app = createApp(App);
app.use(router);
app.use(TButton);
app.use(TInput);
app.use(TForm);
app.use(TFormItem);
app.use(TSelect);
app.use(TOption);
app.use(TMenu);
app.use(TMenuItem);
app.use(TIcon);
app.use(TSubmenu);
app.use(TCol);
app.use(TRow);
app.use(TCard);
app.use(TDivider);
app.use(TLink);
app.use(TList);
app.use(TAlert);
app.use(TTag);
app.use(TListItem);
app.use(TTabs);
app.use(TTabPanel);
app.use(TSpace);
app.use(TCheckbox);
app.use(TPopup);
app.use(TDialog);
app.use(TSwitch);
app.mount('#app');

View File

@@ -0,0 +1,63 @@
<template>
<div class="about-us">
<div>
<t-divider content="面板关于信息" align="left" />
<t-alert theme="success" message="NapCat.WebUi is running" />
<t-list class="list">
<t-list-item class="list-item">
<span class="item-label">开发人员:</span>
<span class="item-content">
<t-link href="mailto:nanaeonn@outlook.com">Mlikiowa</t-link>
</span>
</t-list-item>
<t-list-item class="list-item">
<span class="item-label">版本信息:</span>
<span class="item-content">
<t-tag class="tag-item" theme="success"> WebUi: 1.0.0 </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: 4.?.? </t-tag>
<t-tag class="tag-item" theme="success"> Tdesign: 1.10.3 </t-tag>
</span>
</t-list-item>
</t-list>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
.about-us {
padding: 20px;
text-align: left;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-label {
flex: 1;
font-weight: bold;
}
.item-content {
flex: 2;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
}
.tag-item {
margin-right: 10px;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
<div class="basic-info">
<h1>面板基础信息</h1>
<p>这里显示面板的基础信息</p>
</div>
</template>

View File

@@ -0,0 +1,6 @@
<template>
<div class="log-view">
<h1>面板日志信息</h1>
<p>这里显示面板的日志信息</p>
</div>
</template>

View File

@@ -0,0 +1,244 @@
<template>
<t-space class="full-space">
<template v-if="clientPanelData.length > 0">
<t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs">
<t-tab-panel
v-for="(config, idx) in clientPanelData"
:key="idx"
:label="config.name"
:removable="true"
:value="idx"
class="full-tab-panel"
>
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button>
</div>
</t-tab-panel>
</t-tabs>
</template>
<template v-else>
<EmptyStateComponent :showAddTabDialog="showAddTabDialog" />
</template>
<t-dialog
v-model:visible="isDialogVisible"
header="添加网络配置"
@close="isDialogVisible = false"
@confirm="addTab"
>
<t-form ref="form" :model="newTab">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-select v-model="newTab.type">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select>
</t-form-item>
</t-form>
</t-dialog>
</t-space>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
NetworkConfig,
OneBotConfig,
mergeOneBotConfigs,
} from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
import EmptyStateComponent from '@/pages/network/EmptyStateComponent.vue';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
interface ClientPanel {
name: string;
key: ConfigKey;
data: Ref<ConfigUnion>;
}
type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
});
};
const parsePanelData = (): NetworkConfig => {
return {
websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig),
};
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
// It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<div>
<t-divider content="其余配置" align="left" />
</div>
<div class="other-config-container">
<div class="other-config">
<t-form ref="form" :model="otherConfig" class="form">
<t-form-item label="音乐签名地址" name="musicSignUrl" class="form-item">
<t-input v-model="otherConfig.musicSignUrl" />
</t-form-item>
<t-form-item label="启用本地文件到URL" name="enableLocalFile2Url" class="form-item">
<t-switch v-model="otherConfig.enableLocalFile2Url" />
</t-form-item>
</t-form>
<div class="button-container">
<t-button @click="saveConfig">保存</t-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { OneBotConfig } from '../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
const otherConfig = ref<Partial<OneBotConfig>>({
musicSignUrl: '',
enableLocalFile2Url: false,
});
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
otherConfig.value.musicSignUrl = userConfig.musicSignUrl;
otherConfig.value.enableLocalFile2Url = userConfig.enableLocalFile2Url;
}
} catch (error) {
console.error('Error loading config:', error);
}
};
const saveConfig = async () => {
try {
const userConfig = await getOB11Config();
if (userConfig) {
userConfig.musicSignUrl = otherConfig.value.musicSignUrl || '';
userConfig.enableLocalFile2Url = otherConfig.value.enableLocalFile2Url ?? false;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
}
} catch (error) {
console.error('Error saving config:', error);
MessagePlugin.error('配置保存失败');
}
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.other-config-container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.other-config {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
.form {
display: flex;
flex-direction: column;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.button-container {
display: flex;
justify-content: center;
}
@media (min-width: 768px) {
.form-item {
flex-direction: row;
align-items: center;
}
.form-item t-input,
.form-item t-switch {
flex: 1;
margin-left: 20px;
}
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<div class="empty-state">
<p>当前没有网络配置</p>
<t-button @click="showAddTabDialog">添加网络配置</t-button>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
defineProps<{ showAddTabDialog: () => void }>();
</script>
<style scoped>
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div class="container">
<div class="form-container">
<h3>HTTP Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" type="text" />
</t-form-item>
<t-form-item label="启用 CORS">
<t-checkbox v-model="config.enableCors" />
</t-form-item>
<t-form-item label="启用 WS">
<t-checkbox v-model="config.enableWebsocket" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" type="text" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { HttpServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: HttpServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Client 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="URL">
<t-input v-model="config.url" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="报告自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketClientConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketClientConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="container">
<div class="form-container">
<h3>WebSocket Server 配置</h3>
<t-form>
<t-form-item label="启用">
<t-checkbox v-model="config.enable" />
</t-form-item>
<t-form-item label="主机">
<t-input v-model="config.host" />
</t-form-item>
<t-form-item label="端口">
<t-input v-model.number="config.port" type="number" />
</t-form-item>
<t-form-item label="消息格式">
<t-select v-model="config.messagePostFormat" :options="messageFormatOptions" />
</t-form-item>
<t-form-item label="上报自身消息">
<t-checkbox v-model="config.reportSelfMessage" />
</t-form-item>
<t-form-item label="Token">
<t-input v-model="config.token" />
</t-form-item>
<t-form-item label="强制推送事件">
<t-checkbox v-model="config.enableForcePushEvent" />
</t-form-item>
<t-form-item label="调试模式">
<t-checkbox v-model="config.debug" />
</t-form-item>
<t-form-item label="心跳间隔">
<t-input v-model.number="config.heartInterval" type="number" />
</t-form-item>
</t-form>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue';
import { WebsocketServerConfig } from '../../../../src/onebot/config/config';
const props = defineProps<{
config: WebsocketServerConfig;
}>();
const messageFormatOptions = ref([
{ label: 'Array', value: 'array' },
{ label: 'String', value: 'string' }
]);
watch(() => props.config.messagePostFormat, (newValue) => {
if (newValue !== 'array' && newValue !== 'string') {
props.config.messagePostFormat = 'array';
}
});
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
box-sizing: border-box;
}
.form-container {
width: 100%;
max-width: 600px;
background: #fff;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<t-space class="full-space">
<t-tabs v-model="activeTab" :addable="true" theme="card" @add="showAddTabDialog" @remove="removeTab" class="full-tabs">
<t-tab-panel
v-for="(config, idx) in clientPanelData"
:key="idx"
:label="config.name"
:removable="true"
:value="idx"
class="full-tab-panel"
>
<component :is="resolveDynamicComponent(getComponent(config.key))" :config="config.data" />
<div class="button-container">
<t-button @click="saveConfig" style="width: 100px; height: 40px;">保存</t-button>
</div>
</t-tab-panel>
</t-tabs>
<t-dialog
v-model:visible="isDialogVisible"
header="添加新选项卡"
@close="isDialogVisible = false"
@confirm="addTab"
>
<t-form ref="form" :model="newTab">
<t-form-item :rules="[{ required: true, message: '请输入名称' }]" label="名称" name="name">
<t-input v-model="newTab.name" />
</t-form-item>
<t-form-item :rules="[{ required: true, message: '请选择类型' }]" label="类型" name="type">
<t-select v-model="newTab.type">
<t-option value="httpServers">HTTP 服务器</t-option>
<t-option value="httpClients">HTTP 客户端</t-option>
<t-option value="websocketServers">WebSocket 服务器</t-option>
<t-option value="websocketClients">WebSocket 客户端</t-option>
</t-select>
</t-form-item>
</t-form>
</t-dialog>
</t-space>
</template>
<script setup lang="ts">
import { ref, resolveDynamicComponent, nextTick, Ref, onMounted, reactive, Reactive } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import {
httpServerDefaultConfigs,
httpClientDefaultConfigs,
websocketServerDefaultConfigs,
websocketClientDefaultConfigs,
HttpClientConfig,
HttpServerConfig,
WebsocketClientConfig,
WebsocketServerConfig,
NetworkConfig,
OneBotConfig,
mergeOneBotConfigs,
} from '../../../../src/onebot/config/config';
import { QQLoginManager } from '@/backend/shell';
import HttpServerComponent from '@/pages/network/HttpServerComponent.vue';
import HttpClientComponent from '@/pages/network/HttpClientComponent.vue';
import WebsocketServerComponent from '@/pages/network/WebsocketServerComponent.vue';
import WebsocketClientComponent from '@/pages/network/WebsocketClientComponent.vue';
type ConfigKey = 'httpServers' | 'httpClients' | 'websocketServers' | 'websocketClients';
type ConfigUnion = HttpClientConfig | HttpServerConfig | WebsocketServerConfig | WebsocketClientConfig;
const defaultConfigs: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
const componentMap: Record<
ConfigKey,
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent
> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
interface ClientPanel {
name: string;
key: ConfigKey;
data: Ref<ConfigUnion>;
}
type ComponentKey = keyof typeof componentMap;
// TODO: store these state in global store (aka pinia)
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ComponentKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Reactive<Array<ClientPanel>> = reactive([]);
const getComponent = (type: ComponentKey) => {
return componentMap[type];
};
const getOB11Config = async (): Promise<OneBotConfig | undefined> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.GetOB11Config();
};
const setOB11Config = async (config: OneBotConfig): Promise<boolean> => {
const storedCredential = localStorage.getItem('auth');
if (!storedCredential) {
console.error('No stored credential found');
return false;
}
const loginManager = new QQLoginManager(storedCredential);
return await loginManager.SetOB11Config(config);
};
const addToPanel = <T extends ConfigUnion>(configs: T[], key: ConfigKey) => {
configs.forEach((config) => clientPanelData.push({ name: config.name, data: config, key: key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
Object.entries(data).forEach(([key, configs]) => {
if (key in defaultConfigs) {
addToPanel(configs as ConfigUnion[], key as ConfigKey);
}
});
};
const parsePanelData = (): NetworkConfig => {
return {
websocketClients: clientPanelData
.filter((panel) => panel.key === 'websocketClients')
.map((panel) => panel.data as WebsocketClientConfig),
websocketServers: clientPanelData
.filter((panel) => panel.key === 'websocketServers')
.map((panel) => panel.data as WebsocketServerConfig),
httpClients: clientPanelData
.filter((panel) => panel.key === 'httpClients')
.map((panel) => panel.data as HttpClientConfig),
httpServers: clientPanelData
.filter((panel) => panel.key === 'httpServers')
.map((panel) => panel.data as HttpServerConfig),
};
};
const loadConfig = async () => {
try {
const userConfig = await getOB11Config();
if (!userConfig) return;
const mergedConfig = mergeOneBotConfigs(userConfig);
addConfigDataToPanel(mergedConfig.network);
} catch (error) {
console.error('Error loading config:', error);
}
};
// It's better to "saveConfig" instead of using deep watch
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) return;
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
MessagePlugin.success('配置保存成功');
} else {
MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.some(panel => panel.name === name)) {
MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigs[type]);
defaultConfig.name = name;
clientPanelData.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.length - 1;
MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
loadConfig();
});
</script>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
<style scoped>
.full-space {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.full-tabs {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.full-tab-panel {
flex: 1;
display: flex;
flex-direction: column;
}
.button-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,32 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Dashboard from '../components/Dashboard.vue';
import BasicInfo from '../pages/BasicInfo.vue';
import AboutUs from '../pages/AboutUs.vue';
import LogView from '../pages/Log.vue';
import NetWork from '../pages/NetWork.vue';
import QQLogin from '../components/QQLogin.vue';
import WebUiLogin from '../components/WebUiLogin.vue';
import OtherConfig from '../pages/OtherConfig.vue';
const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/webui' },
{ path: '/webui', component: WebUiLogin, name: 'WebUiLogin' },
{ path: '/qqlogin', component: QQLogin, name: 'QQLogin' },
{
path: '/dashboard',
component: Dashboard,
children: [
{ path: '', redirect: 'basic-info' },
{ path: 'basic-info', component: BasicInfo, name: 'BasicInfo' },
{ path: 'network-config', component: NetWork, name: 'NetWork' },
{ path: 'log-view', component: LogView, name: 'LogView' },
{ path: 'other-config', component: OtherConfig, name: 'OtherConfig' },
{ path: 'about-us', component: AboutUs, name: 'AboutUs' },
],
},
];
export const router = createRouter({
history: createWebHashHistory(),
routes,
});

1
napcat.webui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"jsxImportSource": "vue",
"lib": [
"DOM",
"DOM.Iterable"
],
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
},
"resolveJsonModule": true,
"types": [
"vite/client"
],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"experimentalDecorators": true,
"useDefineForClassFields": true
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [{"path": "./tsconfig.node.json"}]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strictNullChecks": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,30 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
proxy: {
'/api': 'http://localhost:6099',
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
}
}
}
}
});

View File

@@ -2,17 +2,24 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "3.0.6",
"version": "4.1.2",
"scripts": {
"build:framework": "vite build --mode framework",
"build:shell": "vite build --mode shell",
"build:webui": "cd ./src/webui && vite build",
"lint": "eslint --fix src/**/*.{js,ts}",
"build:framework": "npm run build:webui && vite build --mode framework",
"build:shell": "npm run build:webui && vite build --mode shell",
"build:webui": "cd napcat.webui && vite build",
"dev:framework": "vite build --mode framework",
"dev:shell": "vite build --mode shell",
"dev:webui": "cd napcat.webui && npm run webui:dev",
"lint": "eslint --fix src/**/*.{js,ts,vue}",
"depend": "cd dist && npm install --omit=dev"
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
"@eslint/compat": "^1.2.2",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@log4js-node/log4js-api": "^1.0.2",
"@napneko/nap-proto-core": "^0.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/cors": "^2.8.17",
@@ -23,30 +30,31 @@
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"eslint": "^8.57.0",
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^12.1.0",
"cors": "^2.8.5",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"fast-xml-parser": "^4.3.6",
"file-type": "^19.0.0",
"globals": "^15.12.0",
"image-size": "^1.1.1",
"json-schema-to-ts": "^3.1.1",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^5.2.6",
"vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^4.3.2",
"@protobuf-ts/runtime": "^2.9.4",
"ajv": "^8.13.0",
"fast-xml-parser": "^4.3.6",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"async-mutex": "^0.5.0",
"file-type": "^19.0.0",
"json-schema-to-ts": "^3.1.0",
"image-size": "^1.1.1",
"cors": "^2.8.5"
"vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0"
},
"dependencies": {
"qrcode-terminal": "^0.12.0",
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"express": "^5.0.0-beta.2",
"log4js": "^6.9.1",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
"ws": "^8.18.0",
"piscina": "^4.7.0"
}
}

View File

@@ -4,16 +4,27 @@ const process = require("process");
console.log("[NapCat] [CheckVersion] 开始检测当前仓库版本...");
try {
const packageJson = require("../package.json");
const manifsetJson = require("../manifest.json");
const currentVersion = packageJson.version;
const targetVersion = process.env.VERSION;
const manifestCurrentVersion = manifsetJson.version;
const manifestTargetVersion = process.env.VERSION;
console.log("[NapCat] [CheckVersion] currentVersion:", currentVersion, "targetVersion:", targetVersion);
console.log("[NapCat] [CheckVersion] manifestCurrentVersion:", manifestCurrentVersion, "manifestTargetVersion:", manifestTargetVersion);
// 验证 targetVersion 格式
if (!targetVersion || typeof targetVersion !== 'string') {
console.log("[NapCat] [CheckVersion] 目标版本格式不正确或未设置!");
return;
}
// 验证 manifestTargetVersion 格式
if (!manifestTargetVersion || typeof manifestTargetVersion !== 'string') {
console.log("[NapCat] [CheckVersion] manifest目标版本格式不正确或未设置");
return;
}
// 写入脚本文件的统一函数
const writeScriptToFile = (content) => {
@@ -21,7 +32,7 @@ try {
console.log("[NapCat] [CheckVersion] checkVersion.sh 文件已更新。");
};
if (currentVersion === targetVersion) {
if (currentVersion === targetVersion && manifestCurrentVersion === manifestTargetVersion) {
// 不需要更新版本,写入一个简单的脚本
const simpleScript = "#!/bin/bash\necho \"CheckVersion Is Done\"";
writeScriptToFile(simpleScript);
@@ -29,11 +40,13 @@ try {
// 更新版本构建安全的sed命令
const safeScriptContent = `
#!/bin/bash
git config --global user.email "bot@test.wumiao.wang"
git config --global user.name "Version"
sed -i "s/\\\"version\\\": \\\"${currentVersion}\\\"/\\\"version\\\": \\\"${targetVersion}\\\"/g" package.json
git config --global user.email "nanaeonn@outlook.com"
git config --global user.name "Mlikiowa"
sed -i "s/\\"version\\": \\"${currentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" package.json
sed -i "s/\\"version\\": \\"${manifestCurrentVersion}\\"/\\"version\\": \\"${targetVersion}\\"/g" manifest.json
sed -i "s/napCatVersion = '.*'/napCatVersion = '${targetVersion}'/g" ./src/common/version.ts
git add .
git commit -m "chore:version change"
git commit -m "release: v${targetVersion}"
git push -u origin main`;
writeScriptToFile(safeScriptContent);
}

View File

@@ -0,0 +1,9 @@
import { encode } from "silk-wasm";
export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer
sampleRate: number
}
export default async ({ input, sampleRate }: EncodeArgs) => {
return await encode(input, sampleRate);
};

View File

@@ -1,13 +1,23 @@
import Piscina from 'piscina';
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
import { spawn } from 'node:child_process';
import { encode, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from './log';
import { EncodeArgs } from "@/common/audio-worker";
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
const EXIT_CODES = [0, 255];
const FFMPEG_PATH = process.env.FFMPEG_PATH || 'ffmpeg';
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
async function getWorkerPath() {
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
}
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath);
@@ -41,8 +51,11 @@ async function convert(filePath: string, pcmPath: string, logger: LogWrapper): P
}
async function handleWavFile(
file: Buffer, filePath: string, pcmPath: string, logger: LogWrapper
): Promise<{input: Buffer, sampleRate: number}> {
file: Buffer,
filePath: string,
pcmPath: string,
logger: LogWrapper
): Promise<{ input: Buffer; sampleRate: number }> {
const { fmt } = getWavFileInfo(file);
if (!ALLOW_SAMPLE_RATE.includes(fmt.sampleRate)) {
return { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
@@ -60,8 +73,8 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
const { input, sampleRate } = isWav(file)
? (await handleWavFile(file, filePath, pcmPath, logger))
: { input: await convert(filePath, pcmPath, logger), sampleRate: 24000 };
const silk = await encode(input, sampleRate);
await fsPromise.writeFile(pttPath, silk.data);
const silk = await piscina.run({ input: input, sampleRate: sampleRate });
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
return {
converted: true,
@@ -86,4 +99,4 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
logger.logError.bind(logger)('convert silk failed', error.stack);
return {};
}
}
}

View File

@@ -8,12 +8,12 @@ export abstract class ConfigBase<T> {
configPath: string;
configData: T = {} as T;
protected constructor(name: string, core: NapCatCore, configPath: string) {
protected constructor(name: string, core: NapCatCore, configPath: string, copy_default: boolean = true) {
this.name = name;
this.core = core;
this.configPath = configPath;
fs.mkdirSync(this.configPath, { recursive: true });
this.read();
this.read(copy_default);
}
protected getKeys(): string[] | null {
@@ -32,16 +32,18 @@ export abstract class ConfigBase<T> {
}
}
read(): T {
read(copy_default: boolean = true): T {
const logger = this.core.context.logger;
const configPath = this.getConfigPath(this.core.selfInfo.uin);
if (!fs.existsSync(configPath)) {
if (!fs.existsSync(configPath) && copy_default) {
try {
fs.writeFileSync(configPath, fs.readFileSync(this.getConfigPath(undefined), 'utf-8'));
logger.log(`[Core] [Config] 配置文件创建成功!\n`);
} catch (e: any) {
logger.logError.bind(logger)(`[Core] [Config] 创建配置文件时发生错误:`, e.message);
}
} else if (!fs.existsSync(configPath) && !copy_default) {
fs.writeFileSync(configPath, '{}');
}
try {
this.configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));

View File

@@ -9,12 +9,21 @@ interface InternalMapKey {
checker: ((...args: any[]) => boolean) | undefined;
}
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
type FuncKeys<T> = Extract<
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],
string
>;
export type ListenerClassBase = Record<string, string>;
export class NTEventWrapper {
private WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
private listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
private EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
private readonly WrapperSession: NodeIQQNTWrapperSession | undefined; //WrapperSession
private readonly listenerManager: Map<string, ListenerClassBase> = new Map<string, ListenerClassBase>(); //ListenerName-Unique -> Listener实例
private readonly EventTask = new Map<string, Map<string, Map<string, InternalMapKey>>>(); //tasks ListenerMainName -> ListenerSubName-> uuid -> {timeout,createtime,func}
constructor(
wrapperSession: NodeIQQNTWrapperSession,
@@ -43,10 +52,8 @@ export class NTEventWrapper {
createEventFunction<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
// eslint-disable-next-line
// @ts-ignore
T extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
T extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(eventName: `${Service}/${ServiceMethod}`): T | undefined {
const eventNameArr = eventName.split('/');
type eventType = {
@@ -98,10 +105,8 @@ export class NTEventWrapper {
async callNoListenerEvent<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
// eslint-disable-next-line
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
...args: Parameters<EventType>
@@ -111,15 +116,13 @@ export class NTEventWrapper {
async registerListen<
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
// eslint-disable-next-line
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod],
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>,
>(
listenerAndMethod: `${Listener}/${ListenerMethod}`,
checker: (...args: Parameters<ListenerType>) => boolean,
waitTimes = 1,
timeout = 5000,
checker: (...args: Parameters<ListenerType>) => boolean,
) {
return new Promise<Parameters<ListenerType>>((resolve, reject) => {
const ListenerNameList = listenerAndMethod.split('/');
@@ -164,15 +167,11 @@ export class NTEventWrapper {
async callNormalEventV2<
Service extends keyof ServiceNamingMapping,
ServiceMethod extends Exclude<keyof ServiceNamingMapping[Service], symbol>,
ServiceMethod extends FuncKeys<ServiceNamingMapping[Service]>,
Listener extends keyof ListenerNamingMapping,
ListenerMethod extends Exclude<keyof ListenerNamingMapping[Listener], symbol>,
// eslint-disable-next-line
// @ts-ignore
EventType extends (...args: any) => any = ServiceNamingMapping[Service][ServiceMethod],
// eslint-disable-next-line
// @ts-ignore
ListenerType extends (...args: any) => any = ListenerNamingMapping[Listener][ListenerMethod]
ListenerMethod extends FuncKeys<ListenerNamingMapping[Listener]>,
EventType extends (...args: any) => any = EnsureFunc<ServiceNamingMapping[Service][ServiceMethod]>,
ListenerType extends (...args: any) => any = EnsureFunc<ListenerNamingMapping[Listener][ListenerMethod]>
>(
serviceAndMethod: `${Service}/${ServiceMethod}`,
listenerAndMethod: `${Listener}/${ListenerMethod}`,
@@ -182,36 +181,36 @@ export class NTEventWrapper {
callbackTimesToWait = 1,
timeout = 5000,
) {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
function sendDataCallback(resolve: any, reject: any) {
if (complete == 0) {
reject(
new Error(
'Timeout: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
}
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0];
const ListenerSubName = ListenerNameList[1];
return new Promise<[EventRet: Awaited<ReturnType<EventType>>, ...Parameters<ListenerType>]>(
async (resolve, reject) => {
const id = randomUUID();
let complete = 0;
let retData: Parameters<ListenerType> | undefined = undefined;
let retEvent: any = {};
function sendDataCallback() {
if (complete == 0) {
reject(
new Error(
'Timeout: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
} else {
resolve([retEvent as Awaited<ReturnType<EventType>>, ...retData!]);
}
}
const ListenerNameList = listenerAndMethod.split('/');
const ListenerMainName = ListenerNameList[0];
const ListenerSubName = ListenerNameList[1];
const timeoutRef = setTimeout(sendDataCallback, timeout);
(resolve, reject) => {
const timeoutRef = setTimeout(() => sendDataCallback(resolve, reject), timeout);
const eventCallback = {
timeout: timeout,
@@ -222,7 +221,7 @@ export class NTEventWrapper {
retData = args as Parameters<ListenerType>;
if (complete >= callbackTimesToWait) {
clearTimeout(timeoutRef);
sendDataCallback();
sendDataCallback(resolve, reject);
}
},
};
@@ -234,23 +233,26 @@ export class NTEventWrapper {
}
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
const eventFunction = this.createEventFunction(serviceAndMethod);
retEvent = await eventFunction!(...(args));
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
clearTimeout(timeoutRef);
reject(
new Error(
'EventChecker Failed: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
}
this.createEventFunction(serviceAndMethod)!(...(args))
.then((eventResult: any) => {
retEvent = eventResult;
if (!checkerEvent(retEvent) && timeoutRef.hasRef()) {
clearTimeout(timeoutRef);
reject(
new Error(
'EventChecker Failed: NTEvent serviceAndMethod:' +
serviceAndMethod +
' ListenerName:' +
listenerAndMethod +
' EventRet:\n' +
JSON.stringify(retEvent, null, 4) +
'\n',
),
);
}
})
.catch(reject);
},
);
}

View File

@@ -215,7 +215,7 @@ export async function checkUriType(Uri: string) {
}
if (uri.startsWith('file://')) {
let filePath: string;
const pathname = decodeURIComponent(new URL(uri).pathname);
const pathname = decodeURIComponent(new URL(uri).pathname + new URL(uri).hash);
if (process.platform === 'win32') {
filePath = pathname.slice(1);
} else {

View File

@@ -0,0 +1,114 @@
import * as crypto from "node:crypto";
import { PacketMsg } from "@/core/packet/message/message";
interface ForwardMsgJson {
app: string
config: ForwardMsgJsonConfig,
desc: string,
extra: ForwardMsgJsonExtra,
meta: ForwardMsgJsonMeta,
prompt: string,
ver: string,
view: string
}
interface ForwardMsgJsonConfig {
autosize: number,
forward: number,
round: number,
type: string,
width: number
}
interface ForwardMsgJsonExtra {
filename: string,
tsum: number,
}
interface ForwardMsgJsonMeta {
detail: ForwardMsgJsonMetaDetail
}
interface ForwardMsgJsonMetaDetail {
news: {
text: string
}[],
resid: string,
source: string,
summary: string,
uniseq: string
}
interface ForwardAdaptMsg {
senderName?: string;
isGroupMsg?: boolean;
msg?: ForwardAdaptMsgElement[];
}
interface ForwardAdaptMsgElement {
preview?: string;
}
export class ForwardMsgBuilder {
private static build(resId: string, msg: ForwardAdaptMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) {
source = isGroupMsg ? "群聊的聊天记录" : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录';
}
if (!news) {
news = msg.length === 0 ? [{
text: "Nya~ This message is send from NapCat.Packet!",
}] : msg.map(m => ({
text: `${m.senderName}: ${m.msg?.map(msg => msg.preview).join('')}`,
}));
}
if (!summary) {
summary = `查看${msg.length}条转发消息`;
}
if (!prompt) {
prompt = "[聊天记录]";
}
return {
app: "com.tencent.multimsg",
config: {
autosize: 1,
forward: 1,
round: 1,
type: "normal",
width: 300
},
desc: prompt,
extra: {
filename: id,
tsum: msg.length,
},
meta: {
detail: {
news,
resid: resId,
source,
summary,
uniseq: id,
}
},
prompt,
ver: "0.0.0.5",
view: "contact",
};
}
static fromResId(resId: string): ForwardMsgJson {
return this.build(resId, []);
}
static fromPacketMsg(resId: string, packetMsg: PacketMsg[], source?: string, news?: ForwardMsgJsonMetaDetail["news"], summary?: string, prompt?: string): ForwardMsgJson {
return this.build(resId, packetMsg.map(msg => ({
senderName: msg.senderName,
isGroupMsg: msg.groupId !== undefined,
msg: msg.msg.map(m => ({
preview: m.valid ? m.toPreview() : "[该消息类型暂不支持查看]",
}))
})), source, news, summary, prompt);
}
}

View File

@@ -52,7 +52,7 @@ export class FileNapCatOneBotUUID {
const [, , chatType, peerUid, modelId, fileId, fileUUID = undefined] = data;
return {
peer: {
chatType: chatType as any,
chatType: +chatType,
peerUid: peerUid,
},
modelId,
@@ -89,7 +89,7 @@ export class FileNapCatOneBotUUID {
const [, , chatType, peerUid, msgId, elementId, fileUUID = undefined] = data;
return {
peer: {
chatType: chatType as any,
chatType: +chatType,
peerUid: peerUid,
},
msgId,
@@ -239,3 +239,42 @@ export function calcQQLevel(level?: QQLevel) {
const { crownNum, sunNum, moonNum, starNum } = level;
return crownNum * 64 + sunNum * 16 + moonNum * 4 + starNum;
}
export function stringifyWithBigInt(obj: any) {
return JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
}
export function parseAppidFromMajor(nodeMajor: string): string | undefined {
const hexSequence = "A4 09 00 00 00 35";
const sequenceBytes = Buffer.from(hexSequence.replace(/ /g, ""), "hex");
const filePath = path.resolve(nodeMajor);
const fileContent = fs.readFileSync(filePath);
let searchPosition = 0;
while (true) {
const index = fileContent.indexOf(sequenceBytes, searchPosition);
if (index === -1) {
break;
}
const start = index + sequenceBytes.length - 1;
const end = fileContent.indexOf(0x00, start);
if (end === -1) {
break;
}
const content = fileContent.subarray(start, end);
if (!content.every(byte => byte === 0x00)) {
try {
return content.toString("utf-8");
} catch (error) {
break;
}
}
searchPosition = end + 1;
}
return undefined;
}

View File

@@ -1,7 +1,7 @@
import log4js, { Configuration } from 'log4js';
import winston, { format, transports } from 'winston';
import { truncateString } from '@/common/helper';
import path from 'node:path';
import chalk from 'chalk';
import fs from 'node:fs';
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
export enum LogLevel {
@@ -27,97 +27,137 @@ function getFormattedTimestamp() {
export class LogWrapper {
fileLogEnabled = true;
consoleLogEnabled = true;
logConfig: Configuration;
loggerConsole: log4js.Logger;
loggerFile: log4js.Logger;
loggerDefault: log4js.Logger;
// eslint-disable-next-line no-control-regex
colorEscape = /\x1B[@-_][0-?]*[ -/]*[@-~]/g;
logger: winston.Logger;
constructor(logDir: string) {
const filename = `${getFormattedTimestamp()}.log`;
const logPath = path.join(logDir, filename);
this.logConfig = {
appenders: {
FileAppender: { // 输出到文件的appender
type: 'file',
filename: logPath, // 指定日志文件的位置和文件名
maxLogSize: 10485760, // 日志文件的最大大小单位字节这里设置为10MB
layout: {
type: 'pattern',
pattern: '%d{yyyy-MM-dd hh:mm:ss} [%p] %X{userInfo} | %m',
},
},
ConsoleAppender: { // 输出到控制台的appender
type: 'console',
layout: {
type: 'pattern',
pattern: `%d{yyyy-MM-dd hh:mm:ss} [%[%p%]] ${chalk.magenta('%X{userInfo}')} | %m`,
},
},
},
categories: {
default: { appenders: ['FileAppender', 'ConsoleAppender'], level: 'debug' }, // 默认情况下同时输出到文件和控制台
file: { appenders: ['FileAppender'], level: 'debug' },
console: { appenders: ['ConsoleAppender'], level: 'debug' },
},
};
log4js.configure(this.logConfig);
this.loggerConsole = log4js.getLogger('console');
this.loggerFile = log4js.getLogger('file');
this.loggerDefault = log4js.getLogger('default');
this.logger = winston.createLogger({
level: 'debug',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
),
transports: [
new transports.File({
filename: logPath,
level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5
}),
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
})
)
})
]
});
this.setLogSelfInfo({ nick: '', uin: '', uid: '' });
this.cleanOldLogs(logDir);
}
cleanOldLogs(logDir: string) {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
fs.readdir(logDir, (err, files) => {
if (err) {
this.logger.error('Failed to read log directory', err);
return;
}
files.forEach(file => {
const filePath = path.join(logDir, file);
this.deleteOldLogFile(filePath, oneWeekAgo);
});
});
}
private deleteOldLogFile(filePath: string, oneWeekAgo: number) {
fs.stat(filePath, (err, stats) => {
if (err) {
this.logger.error('Failed to get file stats', err);
return;
}
if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath, err => {
if (err) {
if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${filePath}`);
} else {
this.logger.error('Failed to delete old log file', err);
}
} else {
this.logger.info(`Deleted old log file: ${filePath}`);
}
});
}
});
}
setFileAndConsoleLogLevel(fileLogLevel: LogLevel, consoleLogLevel: LogLevel) {
this.logConfig.categories.file.level = fileLogLevel;
this.logConfig.categories.console.level = consoleLogLevel;
log4js.configure(this.logConfig);
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.level = fileLogLevel;
} else if (transport instanceof transports.Console) {
transport.level = consoleLogLevel;
}
});
}
setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) {
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`;
this.loggerConsole.addContext('userInfo', userInfo);
this.loggerFile.addContext('userInfo', userInfo);
this.loggerDefault.addContext('userInfo', userInfo);
this.logger.defaultMeta = { userInfo };
}
setFileLogEnabled(isEnabled: boolean) {
this.fileLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.File) {
transport.silent = !isEnabled;
}
});
}
setConsoleLogEnabled(isEnabled: boolean) {
this.consoleLogEnabled = isEnabled;
this.logger.transports.forEach((transport) => {
if (transport instanceof transports.Console) {
transport.silent = !isEnabled;
}
});
}
formatMsg(msg: any[]) {
let logMsg = '';
for (const msgItem of msg) {
if (msgItem instanceof Error) { // 判断是否是错误
logMsg += msgItem.stack + ' ';
continue;
} else if (typeof msgItem === 'object') { // 判断是否是对象
const obj = JSON.parse(JSON.stringify(msgItem, null, 2));
logMsg += JSON.stringify(truncateString(obj)) + ' ';
continue;
return msg.map(msgItem => {
if (msgItem instanceof Error) {
return msgItem.stack;
} else if (typeof msgItem === 'object') {
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
}
logMsg += msgItem + ' ';
}
return logMsg;
return msgItem;
}).join(' ');
}
_log(level: LogLevel, ...args: any[]) {
if (this.consoleLogEnabled) {
this.loggerConsole[level](this.formatMsg(args));
}
if (this.fileLogEnabled) {
this.loggerFile[level](this.formatMsg(args).replace(this.colorEscape, ''));
const message = this.formatMsg(args);
if (this.consoleLogEnabled && this.fileLogEnabled) {
this.logger.log(level, message);
} else if (this.consoleLogEnabled) {
this.logger.log(level, message);
} else if (this.fileLogEnabled) {
// eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
}
}
log(...args: any[]) {
// info 等级
this._log(LogLevel.INFO, ...args);
}
@@ -140,12 +180,11 @@ export class LogWrapper {
logMessage(msg: RawMessage, selfInfo: SelfInfo) {
const isSelfSent = msg.senderUin === selfInfo.uin;
// Intercept grey tip
if (msg.elements[0]?.elementType === ElementType.GreyTip) {
return;
}
this.log(`${isSelfSent ? '发送 ->' : '接收 <-' } ${rawMessageToText(msg)}`);
this.log(`${isSelfSent ? '发送 ->' : '接收 <-'} ${rawMessageToText(msg)}`);
}
}
@@ -163,86 +202,93 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
} else /* temp */ {
} else {
tokens.push(`临时消息 (${msg.peerUin})`);
}
// message content
function msgElementToText(element: MessageElement) {
if (element.textElement) {
if (element.textElement.atType === AtType.notAt) {
const originalContentLines = element.textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (element.textElement.atType === AtType.atAll) {
return `@全体成员`;
} else if (element.textElement.atType === AtType.atUser) {
return `${element.textElement.content} (${element.textElement.atUid})`;
}
}
if (element.replyElement) {
const recordMsgOrNull = msg.records.find(
record => element.replyElement!.sourceMsgIdInRecords === record.msgId,
);
return `[回复消息 ${recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'// 非转发消息; 否则定位不到
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${element.replyElement.sourceMsgIdInRecords})`
}]`;
}
if (element.picElement) {
return '[图片]';
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
}
if (element.videoElement) {
return '[视频]';
}
if (element.pttElement) {
return `[语音 ${element.pttElement.duration}s]`;
}
if (element.arkElement) {
return '[卡片消息]';
}
if (element.faceElement) {
return `[表情 ${element.faceElement.faceText ?? ''}]`;
}
if (element.marketFaceElement) {
return element.marketFaceElement.faceName;
}
if (element.markdownElement) {
return '[Markdown 消息]';
}
if (element.multiForwardMsgElement) {
return '[转发消息]';
}
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
}
return `[未实现 (ElementType = ${element.elementType})]`;
}
for (const element of msg.elements) {
tokens.push(msgElementToText(element));
tokens.push(msgElementToText(element, msg, recursiveLevel));
}
return tokens.join(' ');
}
function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLevel: number): string {
if (element.textElement) {
return textElementToText(element.textElement);
}
if (element.replyElement) {
return replyElementToText(element.replyElement, msg, recursiveLevel);
}
if (element.picElement) {
return '[图片]';
}
if (element.fileElement) {
return `[文件 ${element.fileElement.fileName}]`;
}
if (element.videoElement) {
return '[视频]';
}
if (element.pttElement) {
return `[语音 ${element.pttElement.duration}s]`;
}
if (element.arkElement) {
return '[卡片消息]';
}
if (element.faceElement) {
return `[表情 ${element.faceElement.faceText ?? ''}]`;
}
if (element.marketFaceElement) {
return element.marketFaceElement.faceName;
}
if (element.markdownElement) {
return '[Markdown 消息]';
}
if (element.multiForwardMsgElement) {
return '[转发消息]';
}
if (element.elementType === ElementType.GreyTip) {
return '[灰条消息]';
}
return `[未实现 (ElementType = ${element.elementType})]`;
}
function textElementToText(textElement: any): string {
if (textElement.atType === AtType.notAt) {
const originalContentLines = textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (textElement.atType === AtType.atAll) {
return `@全体成员`;
} else if (textElement.atType === AtType.atUser) {
return `${textElement.content} (${textElement.atUid})`;
}
return '';
}
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
const recordMsgOrNull = msg.records.find(
record => replyElement.sourceMsgIdInRecords === record.msgId,
);
return `[回复消息 ${recordMsgOrNull &&
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
}]`;
}

View File

@@ -1,6 +1,6 @@
export class LRUCache<K, V> {
private capacity: number;
private cache: Map<K, V>;
public cache: Map<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
@@ -30,4 +30,13 @@ export class LRUCache<K, V> {
}
this.cache.set(key, value);
}
public resetCapacity(newCapacity: number): void {
this.capacity = newCapacity;
while (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
}
}

View File

@@ -2,8 +2,8 @@ import { Peer } from '@/core';
import crypto from 'crypto';
export class LimitedHashTable<K, V> {
private keyToValue: Map<K, V> = new Map();
private valueToKey: Map<V, K> = new Map();
private readonly keyToValue: Map<K, V> = new Map();
private readonly valueToKey: Map<V, K> = new Map();
private maxSize: number;
constructor(maxSize: number) {
@@ -23,10 +23,10 @@ export class LimitedHashTable<K, V> {
}
while (this.keyToValue.size > this.maxSize || this.valueToKey.size > this.maxSize) {
const oldestKey = this.keyToValue.keys().next().value;
// @ts-ignore
this.valueToKey.delete(this.keyToValue.get(oldestKey)!);
// @ts-ignore
this.keyToValue.delete(oldestKey);
if (oldestKey !== undefined) {
this.valueToKey.delete(this.keyToValue.get(oldestKey) as V);
this.keyToValue.delete(oldestKey);
}
}
}
@@ -75,8 +75,8 @@ export class LimitedHashTable<K, V> {
}
class MessageUniqueWrapper {
private msgDataMap: LimitedHashTable<string, number>;
private msgIdMap: LimitedHashTable<string, number>;
private readonly msgDataMap: LimitedHashTable<string, number>;
private readonly msgIdMap: LimitedHashTable<string, number>;
constructor(maxMap: number = 1000) {
this.msgIdMap = new LimitedHashTable<string, number>(maxMap);

View File

@@ -1,8 +1,9 @@
import fs from 'node:fs';
import { systemPlatform } from '@/common/system';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath } from './helper';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
import AppidTable from '@/core/external/appid.json';
import { LogWrapper } from './log';
import { getMajorPath } from '@/core';
export class QQBasicInfoWrapper {
QQMainPath: string | undefined;
@@ -72,6 +73,7 @@ export class QQBasicInfoWrapper {
}
getAppidV2(): { appid: string; qua: string } {
// 通过已有表 性能好
const appidTbale = AppidTable as unknown as QQAppidTableType;
const fullVersion = this.getFullQQVesion();
if (fullVersion) {
@@ -80,10 +82,25 @@ export class QQBasicInfoWrapper {
return data;
}
}
// else
// 通过Major拉取 性能差
try {
const majorAppid = this.getAppidV2ByMajor(fullVersion);
if (majorAppid) {
this.context.logger.log(`[QQ版本兼容性检测] 当前版本Appid未内置 通过Major获取 为了更好的性能请尝试更新NapCat`);
return { appid: majorAppid, qua: this.getQUAFallback() };
}
} catch (error) {
this.context.logger.log(`[QQ版本兼容性检测] 通过Major 获取Appid异常 请检测NapCat/QQNT是否正常`);
}
// 最终兜底为老版本
this.context.logger.log(`[QQ版本兼容性检测] 获取Appid异常 请检测NapCat/QQNT是否正常`);
this.context.logger.log(`[QQ版本兼容性检测] ${fullVersion} 版本兼容性不佳,可能会导致一些功能无法正常使用`,);
return { appid: this.getAppIdFallback(), qua: this.getQUAFallback() };
}
getAppidV2ByMajor(QQVersion: string) {
const majorPath = getMajorPath(QQVersion);
const appid = parseAppidFromMajor(majorPath);
return appid;
}
}

View File

@@ -8,49 +8,48 @@ export class RequestUtil {
const client = url.startsWith('https') ? https : http;
return new Promise((resolve, reject) => {
const req = client.get(url, (res) => {
let cookies: { [key: string]: string } = {};
const handleRedirect = (res: http.IncomingMessage) => {
//console.log(res.headers.location);
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
RequestUtil.HttpsGetCookies(redirectUrl.href).then((redirectCookies) => {
// 合并重定向过程中的cookies
cookies = { ...cookies, ...redirectCookies };
resolve(cookies);
}).catch((err) => {
reject(err);
});
} else {
resolve(cookies);
}
} else {
resolve(cookies);
}
};
res.on('data', () => {
}); // Necessary to consume the stream
const cookies: { [key: string]: string } = {};
res.on('data', () => { }); // Necessary to consume the stream
res.on('end', () => {
handleRedirect(res);
this.handleRedirect(res, url, cookies)
.then(resolve)
.catch(reject);
});
if (res.headers['set-cookie']) {
//console.log(res.headers['set-cookie']);
res.headers['set-cookie'].forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
});
this.extractCookies(res.headers['set-cookie'], cookies);
}
});
req.on('error', (error: any) => {
req.on('error', (error: Error) => {
reject(error);
});
});
}
private static async handleRedirect(res: http.IncomingMessage, url: string, cookies: { [key: string]: string }): Promise<{ [key: string]: string }> {
if (res.statusCode === 301 || res.statusCode === 302) {
if (res.headers.location) {
const redirectUrl = new URL(res.headers.location, url);
const redirectCookies = await this.HttpsGetCookies(redirectUrl.href);
// 合并重定向过程中的cookies
return { ...cookies, ...redirectCookies };
}
}
return cookies;
}
private static extractCookies(setCookieHeaders: string[], cookies: { [key: string]: string }) {
setCookieHeaders.forEach((cookie) => {
const parts = cookie.split(';')[0].split('=');
const key = parts[0];
const value = parts[1];
if (key && value && key.length > 0 && value.length > 0) {
cookies[key] = value;
}
});
}
// 请求和回复都是JSON data传原始内容 自动编码json
static async HttpGetJson<T>(url: string, method: string = 'GET', data?: any, headers: {
@@ -88,13 +87,13 @@ export class RequestUtil {
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError) {
reject(parseError);
} catch (parseError: unknown) {
reject(new Error((parseError as Error).message));
}
});
});
req.on('error', (error: any) => {
req.on('error', (error: Error) => {
reject(error);
});
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
@@ -133,62 +132,4 @@ export class RequestUtil {
Buffer.from(footer, 'utf8'),
]);
}
static async uploadImageForOpenPlatform(filePath: string, cookies: string): Promise<string> {
return new Promise(async (resolve, reject) => {
type retType = { retcode: number, result?: { url: string } };
try {
const options = {
hostname: 'cgi.connect.qq.com',
port: 443,
path: '/qqconnectopen/upload_share_image',
method: 'POST',
headers: {
'Referer': 'https://cgi.connect.qq.com',
'Cookie': cookies,
'Accept': '*/*',
'Connection': 'keep-alive',
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
},
};
const req = https.request(options, async (res) => {
let responseBody = '';
res.on('data', (chunk: string | Buffer) => {
responseBody += chunk.toString();
});
res.on('end', () => {
try {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
const responseJson = JSON.parse(responseBody) as retType;
resolve(responseJson.result!.url!);
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
} catch (parseError) {
reject(parseError);
}
});
});
req.on('error', (error) => {
reject(error);
console.log('Error during upload:', error);
});
const body = await RequestUtil.createFormData('WebKitFormBoundary7MA4YWxkTrZu0gW', filePath);
// req.setHeader('Content-Length', Buffer.byteLength(body));
// console.log(`Prepared data size: ${Buffer.byteLength(body)} bytes`);
req.write(body);
req.end();
return;
} catch (error) {
reject(error);
}
return undefined;
});
}
}

View File

@@ -1 +1 @@
export const napCatVersion = '3.0.6';
export const napCatVersion = '4.1.2';

View File

@@ -20,7 +20,7 @@ export async function getVideoInfo(filePath: string, logger: LogWrapper) {
ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg(filePath).ffprobe((err: any, metadata: ffmpeg.FfprobeData) => {
if (err) {
reject(err);
reject(new Error('无法获取视频信息。'));
} else {
const videoStream = metadata.streams.find((s: FfprobeStream) => s.codec_type === 'video');
if (videoStream) {

View File

@@ -6,8 +6,10 @@ export class NodeIDependsAdapter {
}
onMSFSsoError(args: unknown) {
}
getGroupCode(args: unknown) {
}
}

View File

@@ -6,6 +6,7 @@ import {
Peer,
PicElement,
PicType,
RawMessage,
SendFileElement,
SendPicElement,
SendPttElement,
@@ -30,7 +31,7 @@ export class NTQQFileApi {
context: InstanceContext;
core: NapCatCore;
rkeyManager: RkeyManager;
packetRkey: Array<{ rkey: string; time: number; type: number; }> | undefined;
packetRkey: Array<{ rkey: string; time: number; type: number; ttl: bigint }> | undefined;
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
@@ -174,14 +175,18 @@ export class NTQQFileApi {
const thumbPath = pathLib.join(thumb, thumbFileName);
ffmpeg(filePath)
.on('error', (err) => {
logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
try {
logger.logDebug('获取视频封面失败,使用默认封面', err);
if (diyThumbPath) {
fsPromises.copyFile(diyThumbPath, thumbPath).then(() => {
resolve(thumbPath);
}).catch(reject);
} else {
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
resolve(thumbPath);
}).catch(reject);
} else {
fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
resolve(thumbPath);
}
} catch (error) {
logger.logError.bind(logger)('获取视频封面失败,使用默认封面失败', error);
}
})
.screenshots({
@@ -238,7 +243,7 @@ export class NTQQFileApi {
fileName: fileName,
filePath: path,
md5HexStr: md5,
fileSize: fileSize,
fileSize: fileSize.toString(),
duration: duration ?? 1,
formatType: 1,
voiceType: 1,
@@ -267,6 +272,53 @@ export class NTQQFileApi {
return fileTransNotifyInfo.filePath;
}
async downloadRawMsgMedia(msg: RawMessage[]) {
const res = await Promise.all(
msg.map(m =>
Promise.all(
m.elements
.filter(element =>
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
)
.map(element =>
this.downloadMedia(m.msgId, m.chatType, m.peerUid, element.elementId, '', '', 1000 * 60 * 2, true)
)
)
)
);
msg.forEach((m, msgIndex) => {
const elementResults = res[msgIndex];
let elementIndex = 0;
m.elements.forEach(element => {
if (
element.elementType === ElementType.PIC ||
element.elementType === ElementType.VIDEO ||
element.elementType === ElementType.PTT ||
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
}
elementIndex++;
}
});
});
}
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
// 用于下载收到的消息中的图片等
if (sourcePath && fs.existsSync(sourcePath)) {
@@ -296,7 +348,7 @@ export class NTQQFileApi {
filePath: thumbPath,
}],
() => true,
(arg) => arg.msgId === msgId,
(arg) => arg.msgElementId === elementId && arg.msgId === msgId,
1,
timeout,
);
@@ -305,15 +357,13 @@ export class NTQQFileApi {
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
return new Promise((resolve, reject) => {
imageSize(filePath, (err, dimensions) => {
imageSize(filePath, (err: Error | null, dimensions) => {
if (err) {
reject(err);
reject(new Error(err.message));
} else if (!dimensions) {
reject(new Error('获取图片尺寸失败'));
} else {
if (!dimensions) {
reject(new Error('获取图片尺寸失败'));
} else {
resolve(dimensions);
}
resolve(dimensions);
}
});
});
@@ -356,68 +406,83 @@ export class NTQQFileApi {
return fileData.filePath!;
}
async getImageUrl(element: PicElement) {
async getImageUrl(element: PicElement): Promise<string> {
if (!element) {
return '';
}
const url: string = element.originImageUrl ?? '';
const md5HexStr = element.md5HexStr;
const fileMd5 = element.md5HexStr;
if (url) {
const parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const urlRkey = parsedUrl.searchParams.get('rkey');
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
const imageFileId = parsedUrl.searchParams.get('fileid');
const rkeyData = await this.getRkeyData();
return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData);
}
const rkeyData = {
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
online_rkey: false
};
return this.getImageUrlFromMd5(fileMd5, md5HexStr);
}
private async getRkeyData() {
const rkeyData = {
private_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qEc3Rbib9LP4',
group_rkey: 'CAQSKAB6JWENi5LM_xp9vumLbuThJSaYf-yzMrbZsuq7Uz2qffcqm614gds',
online_rkey: false
};
try {
if (this.core.apis.PacketApi.available) {
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000;
if (rkey_expired_private || rkey_expired_group) {
this.packetRkey = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
}
if (this.packetRkey && this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
rkeyData.online_rkey = true;
}
}
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
}
if (!rkeyData.online_rkey) {
try {
if (this.core.apis.PacketApi.available) {
if ((!this.packetRkey || this.packetRkey[0].time > Date.now() / 1000)) {
this.packetRkey = await this.core.apis.PacketApi.sendRkeyPacket();
}
if (this.packetRkey.length > 0) {
rkeyData.group_rkey = this.packetRkey[1].rkey.slice(6);
rkeyData.private_rkey = this.packetRkey[0].rkey.slice(6);
rkeyData.online_rkey = true;
}
}
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
const tempRkeyData = await this.rkeyManager.getRkey();
rkeyData.group_rkey = tempRkeyData.group_rkey;
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
}
if (!rkeyData.online_rkey) {
try {
const tempRkeyData = await this.rkeyManager.getRkey();
rkeyData.group_rkey = tempRkeyData.group_rkey;
rkeyData.private_rkey = tempRkeyData.private_rkey;
rkeyData.online_rkey = tempRkeyData.expired_time > Date.now() / 1000;
} catch (e) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
}
}
if (isNTV2 && urlRkey) {
return IMAGE_HTTP_HOST_NT + urlRkey;
} else if (isNTV2 && rkeyData.online_rkey) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + url + `&rkey=${rkey}`;
} else if (isNTV2 && imageFileId) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
}
}
//到这里说明可能是旧客户端
return rkeyData;
}
private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string {
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
const imageFileId = parsedUrl.searchParams.get('fileid');
if (isNTV2 && rkeyData.online_rkey) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST_NT + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
} else if (isNTV2 && imageFileId) {
const rkey = imageAppid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
return IMAGE_HTTP_HOST + `/download?appid=${imageAppid}&fileid=${imageFileId}&rkey=${rkey}`;
}
return '';
}
private getImageUrlFromMd5(fileMd5: string | undefined, md5HexStr: string | undefined): string {
if (fileMd5 || md5HexStr) {
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr)!.toUpperCase()}/0`;
return `${IMAGE_HTTP_HOST}/gchatpic_new/0/0-0-${(fileMd5 ?? md5HexStr ?? '').toUpperCase()}/0`;
}
this.context.logger.logDebug('图片url获取失败', element);
this.context.logger.logDebug('图片url获取失败', { fileMd5, md5HexStr });
return '';
}
}

View File

@@ -15,7 +15,7 @@ export class NTQQFriendApi {
}
async getBuddyV2SimpleInfoMap(refresh = false) {
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL) : await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
const buddyListV2 = await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL);
const uids = buddyListV2.data.flatMap(item => item.buddyUids);
return await this.core.eventWrapper.callNoListenerEvent(
'NodeIKernelProfileService/getCoreAndBaseInfo',
@@ -34,15 +34,17 @@ export class NTQQFriendApi {
data.forEach((value) => retMap.set(value.uin!, value.uid!));
return retMap;
}
async getBuddyV2ExWithCate(refresh = false) {
const categoryMap: Map<string, any> = new Map();
async delBuudy(uid: string, tempBlock = false, tempBothDel = false) {
return this.context.session.getBuddyService().delBuddy({
friendUid: uid,
tempBlock: tempBlock,
tempBothDel: tempBothDel
});
}
async getBuddyV2ExWithCate() {
const buddyService = this.context.session.getBuddyService();
const buddyListV2 = refresh ? (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data : (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
const buddyListV2 = (await buddyService.getBuddyListV2('0', BuddyListReqType.KNOMAL)).data;
const uids = buddyListV2.flatMap(item => {
item.buddyUids.forEach(uid => {
categoryMap.set(uid, { categoryId: item.categoryId, categoryName: item.categroyName });
});
return item.buddyUids;
});
const data = await this.core.eventWrapper.callNoListenerEvent(

View File

@@ -25,9 +25,10 @@ export class NTQQGroupApi {
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.initCache().then().catch(context.logger.logError.bind(context.logger));
}
async initApi() {
this.initCache().then().catch(this.context.logger.logError.bind(this.context.logger));
}
async initCache() {
this.groups = await this.getGroups();
for (const group of this.groups) {
@@ -54,7 +55,9 @@ export class NTQQGroupApi {
}, pskey);
}
async getGroupShutUpMemberList(groupCode: string) {
return this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
const data = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onShutUpMemberListChanged', (group_id) => group_id === groupCode, 1, 1000);
this.context.session.getGroupService().getGroupShutUpMemberList(groupCode);
return (await data)[1];
}
async clearGroupNotifiesUnreadCount(uk: boolean) {
return this.context.session.getGroupService().clearGroupNotifiesUnreadCount(uk);
@@ -256,9 +259,9 @@ export class NTQQGroupApi {
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,
(params, _, members) => params === GroupCode && members.size > 0,
);
const retData = await (
this.core.eventWrapper
@@ -316,24 +319,83 @@ export class NTQQGroupApi {
return undefined;
}
async getGroupMembersV2(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
let once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', 1, 2000, (params) => params.sceneId === sceneId)
.catch();
const result = await this.context.session.getGroupService().getNextMemberList(sceneId!, undefined, num);
async tryGetGroupMembersV2(groupQQ: string, modeListener = false, num = 30, timeout = 100): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
if (result.result.infos.size === 0) {
return (await once)[0].infos;
let resMode2;
if (modeListener) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
return result.result.infos;
this.context.session.getGroupService().destroyMemberListScene(sceneId);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
};
}
async GetGroupMembersV3(groupQQ: string, num = 3000, timeout = 2500): Promise<{
infos: Map<string, GroupMember>;
finish: boolean;
hasNext: boolean | undefined;
listenerMode: boolean;
}> {
const sceneId = this.context.session.getGroupService().createMemberListScene(groupQQ, 'groupMemberList_MainWindow_1');
const once = this.core.eventWrapper.registerListen('NodeIKernelGroupListener/onMemberListChange', (params) => params.sceneId === sceneId, 0, timeout)
.catch(() => { });
const result = await this.context.session.getGroupService().getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
let resMode2;
if (result.result.finish && result.result.infos.size === 0) {
const ret = (await once)?.[0];
if (ret) {
resMode2 = ret;
}
}
this.context.session.getGroupService().destroyMemberListScene(sceneId);
//console.log('GetGroupMembersV3 len :', result.result.infos.size, resMode2?.infos.size, groupQQ);
return {
infos: new Map([...(resMode2?.infos ?? []), ...result.result.infos]),
finish: result.result.finish,
hasNext: resMode2?.hasNext,
listenerMode: resMode2?.hasNext !== undefined
};
}
async getGroupMembersV2(groupQQ: string, num = 3000, no_cache: boolean = false): Promise<Map<string, GroupMember>> {
if (no_cache) {
return (await this.getGroupMemberAll(groupQQ, true)).result.infos;
}
let res = await this.GetGroupMembersV3(groupQQ, num);
let ret = res.infos;
if (res.infos.size === 0 && !res.listenerMode) {
res = await this.GetGroupMembersV3(groupQQ, num);
ret = res.infos;
}
if (res.infos.size === 0) {
ret = (await this.getGroupMemberAll(groupQQ)).result.infos;
}
return ret;
}
async getGroupMembers(groupQQ: string, num = 3000): Promise<Map<string, GroupMember>> {
const groupService = this.context.session.getGroupService();
const sceneId = groupService.createMemberListScene(groupQQ, 'groupMemberList_MainWindow');
const result = await groupService.getNextMemberList(sceneId!, undefined, num);
const result = await groupService.getNextMemberList(sceneId, undefined, num);
if (result.errCode !== 0) {
throw new Error('获取群成员列表出错,' + result.errMsg);
}
@@ -341,8 +403,8 @@ export class NTQQGroupApi {
return result.result.infos;
}
async getGroupFileCount(Gids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(Gids);
async getGroupFileCount(group_ids: Array<string>) {
return this.context.session.getRichMediaService().batchGetGroupFileCount(group_ids);
}
async getArkJsonGroupShare(GroupCode: string) {
@@ -425,7 +487,7 @@ export class NTQQGroupApi {
}
async getGroupRemainAtTimes(GroupCode: string) {
this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
return this.context.session.getGroupService().getGroupRemainAtTimes(GroupCode);
}
async getMemberExtInfo(groupCode: string, uin: string) {

View File

@@ -4,5 +4,4 @@ export * from './group';
export * from './msg';
export * from './user';
export * from './webapi';
export * from './sign';
export * from './system';

View File

@@ -3,6 +3,9 @@ import { InstanceContext, NapCatCore } from '@/core';
import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi {
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
}
// nt_qq//global//nt_data//Emoji//emoji-resource//sysface_res/apng/ 下可以看到所有QQ表情预览
// nt_qq\global\nt_data\Emoji\emoji-resource\face_config.json 里面有所有表情的id, 自带表情id是QSid, 标准emoji表情id是QCid
// 其实以官方文档为准是最好的https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType
@@ -22,7 +25,9 @@ export class NTQQMsgApi {
async sendShowInputStatusReq(peer: Peer, eventType: number) {
return this.context.session.getMsgService().sendShowInputStatusReq(peer.chatType, eventType, peer.peerUid);
}
async getSourceOfReplyMsgV2(peer: Peer, clientSeq: string, time: string) {
return this.context.session.getMsgService().getSourceOfReplyMsgV2(peer, clientSeq, time);
}
async getMsgEmojiLikesList(peer: Peer, msgSeq: string, emojiId: string, emojiType: string, count: number = 20) {
//注意此处emojiType 可选值一般为1-2 2好像是unicode表情dec值 大部分情况 Taged Mlikiowa
return this.context.session.getMsgService().getMsgEmojiLikesList(peer, msgSeq, emojiId, emojiType, '', false, count);
@@ -82,6 +87,18 @@ export class NTQQMsgApi {
pageLimit: 1,
});
}
async queryMsgsWithFilterExWithSeqV3(peer: Peer, msgSeq: string, SendersUid: string[]) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
isIncludeCurrent: true,
pageLimit: 1,
});
}
async queryFirstMsgBySeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
@@ -94,9 +111,9 @@ export class NTQQMsgApi {
pageLimit: 1,
});
}
//@deprecated
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, z: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, z);
// 客户端还在用别慌
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
}
async getMsgExBySeq(peer: Peer, msgSeq: string) {
const DateNow = Math.floor(Date.now() / 1000);
@@ -127,7 +144,7 @@ export class NTQQMsgApi {
params,
],
() => true,
() => true, // Todo: 应当通过 groupFileListResult 判断
() => true, // 应当通过 groupFileListResult 判断
1,
5000,
);
@@ -177,7 +194,7 @@ export class NTQQMsgApi {
async sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = true, timeout = 10000) {
//唉?!我有个想法
if (peer.chatType === ChatType.KCHATTYPETEMPC2CFROMGROUP && peer.guildId && peer.guildId !== '') {
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid!);
const member = await this.core.apis.GroupApi.getGroupMember(peer.guildId, peer.peerUid);
if (member) {
await this.PrepareTempChat(peer.peerUid, peer.guildId, member.nick);
}

View File

@@ -1,19 +1,9 @@
import * as os from 'os';
import { ChatType, InstanceContext, NapCatCore } from '..';
import offset from '@/core/external/offset.json';
import { PacketClient, RecvPacketData } from '@/core/packet/client';
import { PacketSession } from "@/core/packet/session";
import { PacketHexStr } from "@/core/packet/packer";
import { NapProtoMsg } from '@/core/packet/proto/NapProto';
import { OidbSvcTrpcTcp0X9067_202_Rsp_Body } from '@/core/packet/proto/oidb/Oidb.0x9067_202';
import { OidbSvcTrpcTcpBase, OidbSvcTrpcTcpBaseRsp } from '@/core/packet/proto/oidb/OidbBase';
import { OidbSvcTrpcTcp0XFE1_2RSP } from '@/core/packet/proto/oidb/Oidb.0XFE1_2';
import { InstanceContext, NapCatCore } from "@/core";
import { LogWrapper } from "@/common/log";
import { SendLongMsgResp } from "@/core/packet/proto/message/action";
import { PacketMsg } from "@/core/packet/msg/message";
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
import { PacketMsgPicElement } from "@/core/packet/msg/element";
import { c } from 'vite/dist/node/types.d-aGj9QkWt';
import { PacketClientSession } from "@/core/packet/clientSession";
import { napCatVersion } from "@/common/version";
interface OffsetType {
[key: string]: {
@@ -27,122 +17,50 @@ const typedOffset: OffsetType = offset;
export class NTQQPacketApi {
context: InstanceContext;
core: NapCatCore;
logger: LogWrapper
serverUrl: string | undefined;
logger: LogWrapper;
qqVersion: string | undefined;
packetSession: PacketSession | undefined;
pkt!: PacketClientSession;
errStack: string[] = [];
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.logger = core.context.logger;
this.packetSession = undefined;
const config = this.core.configLoader.configData;
if (config && config.packetServer && config.packetServer.length > 0) {
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
} else {
this.core.context.logger.logWarn('PacketServer is not set, will not init NapCat.Packet!');
}
}
async initApi() {
await this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
.then()
.catch((err) => {
this.logger.logError.bind(this.core.context.logger);
this.errStack.push(err);
});
}
get available(): boolean {
return this.packetSession?.client.available ?? false;
return this.pkt?.available ?? false;
}
async InitSendPacket(serverUrl: string, qqversion: string) {
this.serverUrl = serverUrl;
this.qqVersion = qqversion;
const offsetTable: OffsetType = offset;
const table = offsetTable[qqversion + '-' + os.arch()];
if (!table) return false;
const url = 'ws://' + this.serverUrl + '/ws';
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
const cb = () => {
if (this.packetSession && this.packetSession.client) {
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
}
get clientLogStack() {
return this.pkt?.clientLogStack + '\n' + this.errStack.join('\n');
}
async InitSendPacket(qqVer: string) {
this.qqVersion = qqVer;
const table = typedOffset[qqVer + '-' + os.arch()];
if (!table) {
const err = `[Core] [Packet] PacketBackend 不支持当前QQ版本架构${qqVer}-${os.arch()}
请参照 https://github.com/NapNeko/NapCatQQ/releases/tag/v${napCatVersion} 配置正确的QQ版本`;
this.logger.logError(err);
this.errStack.push(err);
return false;
}
await this.packetSession.client.connect(cb);
if (this.core.configLoader.configData.packetBackend === 'disable') {
const err = '[Core] [Packet] 已禁用PacketBackendNapCat.Packet将不会加载';
this.logger.logError(err);
this.errStack.push(err);
return false;
}
this.pkt = new PacketClientSession(this.core);
await this.pkt.init(process.pid, table.recv, table.send);
return true;
}
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
return this.packetSession!.client.sendPacket(cmd, data, rsp);
}
async sendPokePacket(peer: number, group?: number) {
const data = this.packetSession?.packer.packPokePacket(peer, group);
await this.sendPacket('OidbSvcTrpcTcp.0xed3_1', data!, false);
}
async sendRkeyPacket() {
const packet = this.packetSession?.packer.packRkeyPacket();
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x9067_202', packet!, true);
if (!ret?.hex_data) return [];
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const retData = new NapProtoMsg(OidbSvcTrpcTcp0X9067_202_Rsp_Body).decode(body);
return retData.data.rkeyList;
}
async sendStatusPacket(uin: number): Promise<{ status: number; ext_status: number; } | undefined> {
let status = 0;
try {
const packet = this.packetSession?.packer.packStatusPacket(uin);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0xfe1_2', packet!, true);
const data = Buffer.from(ret.hex_data, 'hex');
const ext = new NapProtoMsg(OidbSvcTrpcTcp0XFE1_2RSP).decode(new NapProtoMsg(OidbSvcTrpcTcpBase).decode(data).body).data.status.value;
// ext & 0xff00 + ext >> 16 & 0xff
const extBigInt = BigInt(ext); // 转换为 BigInt
if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 };
}
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); // 使用 BigInt 操作符
return { status: 10, ext_status: status };
} catch (error) {
return undefined;
}
}
async sendSetSpecialTittlePacket(groupCode: string, uid: string, tittle: string) {
const data = this.packetSession?.packer.packSetSpecialTittlePacket(groupCode, uid, tittle);
await this.sendPacket('OidbSvcTrpcTcp.0x8fc_2', data!, true);
}
private async uploadResources(msg: PacketMsg[], groupUin: number = 0) {
const reqList = []
for (const m of msg) {
for (const e of m.msg) {
if (e instanceof PacketMsgPicElement) {
reqList.push(this.packetSession?.highwaySession.uploadImage({
chatType: groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid: String(groupUin) ? String(groupUin) : this.core.selfInfo.uid
}, e));
}
}
}
return Promise.all(reqList);
}
async sendUploadForwardMsg(msg: PacketMsg[], groupUin: number = 0) {
await this.uploadResources(msg, groupUin);
const data = await this.packetSession?.packer.packUploadForwardMsg(this.core.selfInfo.uid, msg, groupUin);
const ret = await this.sendPacket('trpc.group.long_msg_interface.MsgService.SsoSendLongMsg', data!, true);
this.logger.logDebug('sendUploadForwardMsg', ret);
const resp = new NapProtoMsg(SendLongMsgResp).decode(Buffer.from(ret.hex_data, 'hex'));
return resp.result.resId;
}
async sendGroupFileDownloadReq(groupUin: number, fileUUID: string) {
const data = this.packetSession?.packer.packGroupFileDownloadReq(groupUin, fileUUID);
const ret = await this.sendPacket('OidbSvcTrpcTcp.0x6d6_2', data!, true);
const body = new NapProtoMsg(OidbSvcTrpcTcpBaseRsp).decode(Buffer.from(ret.hex_data, 'hex')).body;
const resp = new NapProtoMsg(OidbSvcTrpcTcp0x6D6Response).decode(body);
if (resp.download.retCode !== 0) {
throw new Error(`sendGroupFileDownloadReq error: ${resp.download.clientWording}`);
}
return `https://${resp.download.downloadDns}/ftn_handler/${Buffer.from(resp.download.downloadUrl).toString('hex')}/?fname=`
}
}

View File

@@ -1,5 +1,3 @@
import { RequestUtil } from '@/common/request';
import { MiniAppLuaJsonType } from '@/core';
import { InstanceContext, NapCatCore } from '..';
export class NTQQMusicSignApi {
@@ -10,210 +8,6 @@ export class NTQQMusicSignApi {
this.context = context;
this.core = core;
}
async signMiniApp(CardData: MiniAppLuaJsonType) {
// {
// "app": "com.tencent.miniapp.lua",
// "bizsrc": "tianxuan.imgJumpArk",
// "view": "miniapp",
// "prompt": "hi! 这里有我的日常故事,只想讲给你听",
// "config": {
// "type": "normal",
// "forward": 1,
// "autosize": 0
// },
// "meta": {
// "miniapp": {
// "title": "hi! 这里有我的日常故事,只想讲给你听",
// "preview": "https:\/\/tianquan.gtimg.cn\/qqAIAgent\/item\/7\/square.png",
// "jumpUrl": "https:\/\/club.vip.qq.com\/transfer?open_kuikly_info=%7B%22version%22%3A%20%221%22%2C%22src_type%22%3A%20%22web%22%2C%22kr_turbo_display%22%3A%20%221%22%2C%22page_name%22%3A%20%22vas_ai_persona_moments%22%2C%22bundle_name%22%3A%20%22vas_ai_persona_moments%22%7D&page_name=vas_ai_persona_moments&enteranceId=share&robot_uin=3889008584",
// "tag": "QQ智能体",
// "tagIcon": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png",
// "source": "QQ智能体",
// "sourcelogo": "https:\/\/tianquan.gtimg.cn\/shoal\/qqAIAgent\/3e9d70c9-d98c-45b8-80b4-79d82971b514.png"
// }
// }
// }
// token : function(url,skey){
// var str = skey || cookie('skey') || cookie('rv2') || '',
// hash = 5381;
// if(url){
// var hostname = uri(url).hostname;
// if(hostname.indexOf('qun.qq.com') > -1 || (hostname.indexOf('qzone.qq.com') > -1 && hostname.indexOf('qun.qzone.qq.com') === -1)){
// str = cookie('p_skey') || str;
// }
// }
// for(var i = 0, len = str.length; i < len; ++i){
// hash += (hash << 5) + str.charAt(i).charCodeAt();
// }
// return hash & 0x7fffffff;
// },
//
// function signToken(skey: string) {
// let hash = 5381;
// for (let i = 0, len = skey.length; i < len; ++i) {
// hash += (hash << 5) + skey.charCodeAt(i);
// }
// return hash & 0x7fffffff;
// }
const signCard = {
'app': 'com.tencent.miniapp.lua',
'bizsrc': 'tianxuan.imgJumpArk',
'view': 'miniapp',
'prompt': CardData.prompt,
'config': {
'type': 'normal',
'forward': 1,
'autosize': 0,
},
'meta': {
'miniapp': {
'title': CardData.title,
'preview': (CardData.preview as string).replace(/\\/g, '\\/\\/'),
'jumpUrl': (CardData.jumpUrl as string).replace(/\\/g, '\\/\\/'),
'tag': CardData.tag,
'tagIcon': (CardData.tagIcon as string).replace(/\\/g, '\\/\\/'),
'source': CardData.source,
'sourcelogo': (CardData.sourcelogo as string).replace(/\\/g, '\\/\\/'),
},
},
};
// let signCard = {
// "app": "com.tencent.eventshare.lua",
// "prompt": "Bot Test",
// "bizsrc": "tianxuan.business",
// "meta": {
// "eventshare": {
// "button1URL": "https://www.bilibili.com",
// "button1disable": false,
// "button1title": "点我前往",
// "button2URL": "",
// "button2disable": false,
// "button2title": "",
// "buttonNum": 1,
// "jumpURL": "https://www.bilibili.com",
// "preview": "https://tianquan.gtimg.cn/shoal/card/9930bc4e-4a92-4da3-814f-8094a2421d9c.png",
// "tag": "QQ集卡",
// "tagIcon": "https://tianquan.gtimg.cn/shoal/card/c034854b-102d-40be-a545-5ca90a7c49c9.png",
// "title": "Bot Test"
// }
// },
// "config": {
// "autosize": 0,
// "collect": 0,
// "ctime": 1716568575,
// "forward": 1,
// "height": 336,
// "reply": 0,
// "round": 1,
// "type": "normal",
// "width": 263
// },
// "view": "eventshare",
// "ver": "0.0.0.1"
// };
const data = (await this.core.apis.UserApi.getQzoneCookies());
const Bkn = this.core.apis.WebApi.getBknFromCookie(data.p_skey);
const CookieValue = 'p_skey=' + data.p_skey + '; skey=' + data.skey + '; p_uin=o' + this.core.selfInfo.uin + '; uin=o' + this.core.selfInfo.uin;
const signurl = 'https://h5.qzone.qq.com/v2/vip/tx/trpc/ark-share/GenNewSignedArk?g_tk=' + Bkn + '&ark=' + encodeURIComponent(JSON.stringify(signCard));
let signed_ark = '';
try {
const retData = await RequestUtil.HttpGetJson<{
code: number,
data: { signed_ark: string }
}>(signurl, 'GET', undefined, { Cookie: CookieValue });
//logDebug('MiniApp JSON 消息生成成功', retData);
signed_ark = retData.data.signed_ark;
} catch (error) {
this.context.logger.logDebug('MiniApp JSON 消息生成失败', error);
}
return signed_ark;
}
async signInternal(songname: string, singer: string, cover: string, songmid: string, songmusic: string) {
//curl -X POST 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003' -H 'Content-Type: application/json' -H 'Cookie: uin=o10086' -d '{"app":"com.tencent.qqreader.share","config":{"ctime":1718634110,"forward":1,"token":"9a63343c32d5a16bcde653eb97faa25d","type":"normal"},"extra":{"app_type":1,"appid":100497308,"msg_seq":14386738075403815000.0,"uin":1733139081},"meta":{"music":{"action":"","android_pkg_name":"","app_type":1,"appid":100497308,"ctime":1718634110,"desc":"周杰伦","jumpUrl":"https://i.y.qq.com/v8/playsong.html?songmid=0039MnYb0qxYhV&type=0","musicUrl":"http://ws.stream.qqmusic.qq.com/http://isure6.stream.qqmusic.qq.com/M800002202B43Cq4V4.mp3?fromtag=810033622&guid=br_xzg&trace=23fe7bcbe2336bbf&uin=553&vkey=CF0F5CE8B0FA16F3001F8A88D877A217EB5E4F00BDCEF1021EB6C48969CA33C6303987AEECE9CC840122DD2F917A59D6130D8A8CA4577C87","preview":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","cover":"https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg","sourceMsgId":"0","source_icon":"https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0","source_url":"","tag":"QQ音乐","title":"晴天","uin":10086}},"prompt":"[分享]晴天","ver":"0.0.0.1","view":"music"}'
const signurl = 'https://mqq.reader.qq.com/api/mqq/share/card?accessToken&_csrfToken&source=c0003';
//let = "https://y.qq.com/music/photo_new/T002R800x800M000000MkMni19ClKG.jpg";
const signCard = {
app: 'com.tencent.qqreader.share',
config: {
ctime: 1718634110,
forward: 1,
token: '9a63343c32d5a16bcde653eb97faa25d',
type: 'normal',
},
extra: {
app_type: 1,
appid: 100497308,
msg_seq: 14386738075403815000,
uin: 1733139081,
},
meta: {
music: {
action: '',
android_pkg_name: '',
app_type: 1,
appid: 100497308,
ctime: 1718634110,
desc: singer,
jumpUrl: 'https://i.y.qq.com/v8/playsong.html?songmid=' + songmid + '&type=0',
musicUrl: songmusic,
preview: cover,
cover: cover,
sourceMsgId: '0',
source_icon: 'https://p.qpic.cn/qqconnect/0/app_100497308_1626060999/100?max-age=2592000&t=0',
source_url: '',
tag: 'QQ音乐',
title: songname,
uin: 10086,
},
},
prompt: '[分享]' + songname,
ver: '0.0.0.1',
view: 'music',
};
//console.log(JSON.stringify(signCard, null, 2));
const data = await RequestUtil.HttpGetJson<{ code: number, data: { arkResult: string } }>
(signurl, 'POST', signCard, { 'Cookie': 'uin=o10086', 'Content-Type': 'application/json' });
return data;
}
//注意处理错误
async signWay03(id: string = '', mid: string = '') {
let signedMid;
if (mid == '') {
const MusicInfo = await RequestUtil.HttpGetJson<{
songinfo?: {
data?: {
track_info: {
mid: string
}
}
}
}>(
'https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"songinfo":{"method":"get_song_detail_yqq","param":{"song_type":0,"song_mid":"","song_id":' + id + '},"module":"music.pf_song_detail_svr"}}',
'GET',
undefined,
);
signedMid = MusicInfo.songinfo?.data?.track_info.mid;
}
//第三方接口 存在速率限制 现在勉强用
const MusicReal = await RequestUtil.HttpGetJson<{
code: number,
data?: {
name: string,
singer: string,
url: string,
cover: string
}
}>('https://api.leafone.cn/api/qqmusic?id=' + signedMid + '&type=8', 'GET');
//console.log(MusicReal);
return { ...MusicReal.data, mid: signedMid };
}
//转换外域名为 https://qq.ugcimg.cn/v1/cpqcbu4b8870i61bde6k7cbmjgejq8mr3in82qir4qi7ielffv5slv8ck8g42novtmev26i233ujtuab6tvu2l2sjgtupfr389191v00s1j5oh5325j5eqi40774jv1i/khovifoh7jrqd6eahoiv7koh8o
//https://cgi.connect.qq.com/qqconnectopen/openapi/change_image_url?url=https://th.bing.com/th?id=OSK.b8ed36f1fb1889de6dc84fd81c187773&w=46&h=46&c=11&rs=1&qlt=80&o=6&dpr=2&pid=SANGAM
@@ -227,10 +21,5 @@ export class NTQQMusicSignApi {
//https://y.gtimg.cn/music/photo_new/T002R800x800M000000y5gq7449K9I.jpg?max_age=2592000
//还有一处公告上传可以上传高质量图片 持久为qq域名
async SignMusicWrapper(id: string = '') {
const MusicInfo = await this.signWay03(id)!;
return await this.signInternal(MusicInfo.name!, MusicInfo.singer!, MusicInfo.cover!, MusicInfo.mid!, 'https://ws.stream.qqmusic.qq.com/' + MusicInfo.url!);
}
}

View File

@@ -18,7 +18,7 @@ export class NTQQUserApi {
async getStatusByUid(uid: string) {
return this.context.session.getProfileService().getStatus(uid);
}
async getProfileLike(uid: string) {
async getProfileLike(uid: string, start: number, count: number) {
return this.context.session.getProfileLikeService().getBuddyProfileLike({
friendUids: [uid],
basic: 1,
@@ -26,8 +26,8 @@ export class NTQQUserApi {
favorite: 0,
userProfile: 1,
type: 2,
start: 0,
limit: 20,
start: start,
limit: count,
});
}
async fetchOtherProfileLike(uid: string) {

View File

@@ -212,108 +212,65 @@ export class NTQQWebApi {
}
}
private async getDataInternal(cookieObject: any, groupCode: string, type: number) {
let resJson;
try {
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: groupCode,
type: type.toString(),
}).toString()}`,
'GET',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match) {
resJson = JSON.parse(match[1].trim());
}
return type === 1 ? resJson?.talkativeList : resJson?.actorList;
} catch (e) {
this.context.logger.logDebug('获取当前群荣耀失败', e);
return undefined;
}
}
private async getHonorList(cookieObject: any, groupCode: string, type: number) {
const data = await this.getDataInternal(cookieObject, groupCode, type);
if (!data) {
this.context.logger.logError(`获取类型 ${type} 的荣誉信息失败`);
return [];
}
return data.map((item: any) => ({
user_id: item?.uin,
nickname: item?.name,
avatar: item?.avatar,
description: item?.desc,
}));
}
async getGroupHonorInfo(groupCode: string, getType: WebHonorType) {
const cookieObject = await this.core.apis.UserApi.getCookies('qun.qq.com');
const getDataInternal = async (Internal_groupCode: string, Internal_type: number) => {
let resJson;
try {
const res = await RequestUtil.HttpGetText(
`https://qun.qq.com/interactive/honorlist?${new URLSearchParams({
gc: Internal_groupCode,
type: Internal_type.toString(),
}).toString()}`,
'GET',
'',
{ 'Cookie': this.cookieToString(cookieObject) }
);
const match = /window\.__INITIAL_STATE__=(.*?);/.exec(res);
if (match) {
resJson = JSON.parse(match[1].trim());
}
if (Internal_type === 1) {
return resJson?.talkativeList;
} else {
return resJson?.actorList;
}
} catch (e) {
this.context.logger.logDebug('获取当前群荣耀失败', e);
}
return undefined;
};
const HonorInfo: any = { group_id: groupCode };
if (getType === WebHonorType.TALKATIVE || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 1);
if (RetInternal) {
HonorInfo.current_talkative = {
user_id: RetInternal[0]?.uin,
avatar: RetInternal[0]?.avatar,
nickname: RetInternal[0]?.name,
day_count: 0,
description: RetInternal[0]?.desc,
};
HonorInfo.talkative_list = [];
for (const talkative_ele of RetInternal) {
HonorInfo.talkative_list.push({
user_id: talkative_ele?.uin,
avatar: talkative_ele?.avatar,
description: talkative_ele?.desc,
day_count: 0,
nickname: talkative_ele?.name,
});
}
} else {
this.context.logger.logError.bind(this.context.logger)('获取龙王信息失败');
const talkativeList = await this.getHonorList(cookieObject, groupCode, 1);
if (talkativeList.length > 0) {
HonorInfo.current_talkative = talkativeList[0];
HonorInfo.talkative_list = talkativeList;
}
}
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 2);
if (RetInternal) {
HonorInfo.performer_list = [];
for (const performer_ele of RetInternal) {
HonorInfo.performer_list.push({
user_id: performer_ele?.uin,
nickname: performer_ele?.name,
avatar: performer_ele?.avatar,
description: performer_ele?.desc,
});
}
} else {
this.context.logger.logError.bind(this.context.logger)('获取群聊之火失败');
}
HonorInfo.performer_list = await this.getHonorList(cookieObject, groupCode, 2);
}
if (getType === WebHonorType.PERFORMER || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 3);
if (RetInternal) {
HonorInfo.legend_list = [];
for (const legend_ele of RetInternal) {
HonorInfo.legend_list.push({
user_id: legend_ele?.uin,
nickname: legend_ele?.name,
avatar: legend_ele?.avatar,
desc: legend_ele?.description,
});
}
} else {
this.context.logger.logError.bind(this.context.logger)('获取群聊炽焰失败');
}
if (getType === WebHonorType.LEGEND || getType === WebHonorType.ALL) {
HonorInfo.legend_list = await this.getHonorList(cookieObject, groupCode, 3);
}
if (getType === WebHonorType.EMOTION || getType === WebHonorType.ALL) {
const RetInternal = await getDataInternal(groupCode, 6);
if (RetInternal) {
HonorInfo.emotion_list = [];
for (const emotion_ele of RetInternal) {
HonorInfo.emotion_list.push({
user_id: emotion_ele.uin,
nickname: emotion_ele.name,
avatar: emotion_ele.avatar,
desc: emotion_ele.description,
});
}
} else {
this.context.logger.logError.bind(this.context.logger)('获取快乐源泉失败');
}
HonorInfo.emotion_list = await this.getHonorList(cookieObject, groupCode, 6);
}
// 冒尖小春笋好像已经被tx扬了 R.I.P.
@@ -338,4 +295,12 @@ export class NTQQWebApi {
}
return (hash & 0x7FFFFFFF).toString();
}
public getBknFromSKey(sKey: string) {
let hash = 5381;
for (let i = 0; i < sKey.length; i++) {
const code = sKey.charCodeAt(i);
hash = hash + (hash << 5) + code;
}
return (hash & 0x7FFFFFFF).toString();
}
}

View File

@@ -23,7 +23,7 @@ export interface ChatCacheList {
export interface ChatCacheListItem {
chatType: ChatType;
basicChatCacheInfo: ChatCacheListItemBasic;
guildChatCacheInfo: unknown[]; // TODO: 没用过频道所以不知道这里边的详细内容
guildChatCacheInfo: unknown[]; // work: 没用过频道所以不知道这里边的详细内容
}
export interface ChatCacheListItemBasic {

View File

@@ -117,7 +117,7 @@ export enum GroupMemberRole {
}
export interface GroupMember {
memberRealLevel: string | undefined;
memberRealLevel: number | undefined;
memberSpecialTitle?: string;
avatarPath: string;
cardName: string;

View File

@@ -27,94 +27,70 @@ export interface GetFileListParam {
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
VIDEO = 5,
FACE = 6,
REPLY = 7,
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
WALLET = 9,
/**
* “小灰条”,包括拍一拍 (Poke)、撤回提示等
*/
GreyTip = 8,
ARK = 10,
MFACE = 11,
LIVEGIFT = 12,
STRUCTLONGMSG = 13,
MARKDOWN = 14,
GIPHY = 15,
MULTIFORWARD = 16,
INLINEKEYBOARD = 17,
INTEXTGIFT = 18,
CALENDAR = 19,
YOLOGAMERESULT = 20,
AVRECORD = 21,
FEED = 22,
TOFURECORD = 23,
ACEBUBBLE = 24,
ACTIVITY = 25,
TOFU = 26,
FACEBUBBLE = 27,
SHARELOCATION = 28,
TASKTOPMSG = 29,
RECOMMENDEDMSG = 43,
ACTIONBAR = 44
}
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
type ElementBase<
K extends keyof ElementFullBase,
S extends Partial<{ [P in K]: keyof NonNullable<ElementFullBase[P]> | Array<keyof NonNullable<ElementFullBase[P]>> }> = object
> = {
[P in K]:
S[P] extends Array<infer U>
? Pick<NonNullable<ElementFullBase[P]>, U & keyof NonNullable<ElementFullBase[P]>>
: S[P] extends keyof NonNullable<ElementFullBase[P]>
? Pick<NonNullable<ElementFullBase[P]>, S[P]>
: NonNullable<ElementFullBase[P]>;
};
export interface SendElementBase<ET extends ElementType> {
elementType: ET;
elementId: string;
extBufForUI?: string;
}
export interface ActionBarElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface SendActionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: ActionBarElement;
}
export interface RecommendedMsgElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface SendRecommendedMsgElement {
elementType: ElementType.RECOMMENDEDMSG;
elementId: string;
recommendedMsgElement: RecommendedMsgElement;
}
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
export interface InlineKeyboardButton {
id: string;
@@ -171,11 +147,7 @@ export enum NTMsgType {
KMSGTYPEWALLET = 10
}
export interface SendTaskTopMsgElement {
elementType: ElementType.TASKTOPMSG;
elementId: string;
taskTopMsgElement: TaskTopMsgElement;
}
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
export interface TofuRecordElement {
type: number;
@@ -194,11 +166,7 @@ export interface TofuRecordElement {
onscreennotify: boolean;
}
export interface SendTofuRecordElement {
elementType: ElementType.TOFURECORD;
elementId: string;
tofuRecordElement: TofuRecordElement;
}
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
export interface FaceBubbleElement {
faceCount: number;
@@ -216,12 +184,7 @@ export interface FaceBubbleElement {
};
}
export interface SendFaceBubbleElement {
elementType: ElementType.FACEBUBBLE;
elementId: string;
faceBubbleElement: FaceBubbleElement;
}
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
export interface AvRecordElement {
type: number;
@@ -232,11 +195,7 @@ export interface AvRecordElement {
extraType: number;
}
export interface SendavRecordElement {
elementType: ElementType.AVRECORD;
elementId: string;
avRecordElement: AvRecordElement;
}
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
export interface YoloUserInfo {
uid: string;
@@ -245,24 +204,13 @@ export interface YoloUserInfo {
bizId: string;
}
export interface SendInlineKeyboardElement {
elementType: ElementType.INLINEKEYBOARD;
elementId: string;
inlineKeyboardElement: {
rows: number;
botAppid: string;
};
}
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
export interface YoloGameResultElement {
UserInfo: YoloUserInfo[];
}
export interface SendYoloGameResultElement {
elementType: ElementType.YOLOGAMERESULT;
yoloGameResultElement: YoloGameResultElement;
}
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
export interface GiphyElement {
id: string;
@@ -271,17 +219,9 @@ export interface GiphyElement {
height: number;
}
export interface SendGiphyElement {
elementType: ElementType.GIPHY;
elementId: string;
giphyElement: GiphyElement;
}
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
export interface SendWalletElement {
elementType: ElementType.UNKNOWN;//不做 设置位置
elementId: string;
walletElement: Record<string, never>;
}
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
export interface CalendarElement {
summary: string;
@@ -291,49 +231,16 @@ export interface CalendarElement {
schema: string;
}
export interface SendCalendarElement {
elementType: ElementType.CALENDAR;
elementId: string;
calendarElement: CalendarElement;
}
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
export interface SendliveGiftElement {
elementType: ElementType.LIVEGIFT;
elementId: string;
liveGiftElement: Record<string, never>;
}
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
export interface SendTextElement {
elementType: ElementType.TEXT;
elementId: string;
textElement: {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
};
}
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
export interface SendPttElement {
elementType: ElementType.PTT;
elementId: string;
pttElement: {
fileName: string;
filePath: string;
md5HexStr: string;
fileSize: number;
duration: number; // 单位是秒
formatType: number;
voiceType: number;
voiceChangeType: number;
canConvert2Text: boolean;
waveAmplitudes: number[];
fileSubId: string;
playState: number;
autoConvertText: number;
};
}
export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
}>;
export enum PicType {
gif = 2000,
@@ -359,11 +266,7 @@ export enum NTMsgAtType {
ATTYPEUNKNOWN = 0
}
export interface SendPicElement {
elementType: ElementType.PIC;
elementId: string;
picElement: PicElement;
}
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
export interface ReplyElement {
sourceMsgIdInRecords?: string;
@@ -375,53 +278,27 @@ export interface ReplyElement {
replyMsgClientSeq?: string;
}
export interface SendReplyElement {
elementType: ElementType.REPLY;
elementId: string;
replyElement: ReplyElement;
}
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
export interface SendFaceElement {
elementType: ElementType.FACE;
elementId: string;
faceElement: FaceElement;
}
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
export interface SendMarketFaceElement {
elementType: ElementType.MFACE;
marketFaceElement: MarketFaceElement;
}
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
export interface SendStructLongMsgElement {
elementType: ElementType.STRUCTLONGMSG;
elementId: string;
structLongMsgElement: StructLongMsgElement;
}
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
export interface StructLongMsgElement {
xmlContent: string;
resId: string;
}
export interface SendactionBarElement {
elementType: ElementType.ACTIONBAR;
elementId: string;
actionBarElement: {
rows: number;
botAppid: string;
};
}
export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
export interface ShareLocationElement {
text: string;
ext: string;
}
export interface SendShareLocationElement {
elementType: ElementType.SHARELOCATION;
elementId: string;
shareLocationElement?: ShareLocationElement;
}
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export interface FileElement {
fileMd5?: string;
@@ -441,29 +318,13 @@ export interface FileElement {
fileBizId?: number;
}
export interface SendFileElement {
elementType: ElementType.FILE;
elementId: string;
fileElement: FileElement;
}
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
export interface SendVideoElement {
elementType: ElementType.VIDEO;
elementId: string;
videoElement: VideoElement;
}
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
export interface SendArkElement {
elementType: ElementType.ARK;
elementId: string;
arkElement: ArkElement;
}
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
export interface SendMarkdownElement {
elementType: ElementType.MARKDOWN;
elementId: string;
markdownElement: MarkdownElement;
}
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
@@ -480,7 +341,7 @@ export interface TextElement {
export interface MessageElement {
elementType: ElementType,
elementId: string,
extBufForUI: string,//"0x",
extBufForUI?: string, //"0x",
textElement?: TextElement;
faceElement?: FaceElement,
marketFaceElement?: MarketFaceElement,
@@ -509,7 +370,6 @@ export interface MessageElement {
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement
}
export enum AtType {
@@ -578,7 +438,7 @@ export interface PttElement {
fileSize: string; // "4261"
fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: string; // 1
formatType: number; // 1
invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0
@@ -589,6 +449,7 @@ export interface PttElement {
voiceChangeType: number; // 0
voiceType: number; // 0
waveAmplitudes: number[];
autoConvertText: number;
}
export interface ArkElement {
@@ -794,7 +655,8 @@ export interface InlineKeyboardElementRowButton {
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}];
}],
botAppid: string;
}
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
@@ -960,6 +822,8 @@ export interface RawMessage {
elements: MessageElement[];
sourceType: MsgSourceType;
isOnlineMsg: boolean;
}
export interface QueryMsgsParams {
chatInfo: Peer;

View File

@@ -43,6 +43,50 @@ export enum GroupInviteType {
BYGROUPMEMBER,
BYDISCUSSMEMBER
}
export interface ShutUpGroupHonor {
[key: string]: number;
}
export interface ShutUpGroupMember {
uid: string;
qid: string;
uin: string;
nick: string;
remark: string;
cardType: number;
cardName: string;
role: number;
avatarPath: string;
shutUpTime: number;
isDelete: boolean;
isSpecialConcerned: boolean;
isSpecialShield: boolean;
isRobot: boolean;
groupHonor: ShutUpGroupHonor;
memberRealLevel: number;
memberLevel: number;
globalGroupLevel: number;
globalGroupPoint: number;
memberTitleId: number;
memberSpecialTitle: string;
specialTitleExpireTime: string;
userShowFlag: number;
userShowFlagNew: number;
richFlag: number;
mssVipType: number;
bigClubLevel: number;
bigClubFlag: number;
autoRemark: string;
creditLevel: number;
joinTime: number;
lastSpeakTime: number;
memberFlag: number;
memberFlagExt: number;
memberMobileFlag: number;
memberFlagExt2: number;
isSpecialShielded: boolean;
cardNameId: number;
}
export interface GroupNotify {
seq: string; // 通知序列号

View File

@@ -175,8 +175,8 @@ export interface SimpleInfo {
status: UserStatus | null;
vasInfo: VasInfo | null;
relationFlags: RelationFlags | null;
otherFlags: any | null;
intimate: any | null;
otherFlags: any;
intimate: any;
}
export type FriendV2 = SimpleInfo;

View File

@@ -50,5 +50,41 @@
"9.9.16-28788": {
"appid": 537249739,
"qua": "V1_WIN_NQ_9.9.16_28788_GW_B"
},
"9.9.16-28971": {
"appid": 537249775,
"qua": "V1_WIN_NQ_9.9.16_28971_GW_B"
},
"3.2.13-28971": {
"appid": 537249848,
"qua": "V1_LNX_NQ_3.2.13_28971_GW_B"
},
"6.9.58-28971": {
"appid": 537249826,
"qua": "V1_MAC_NQ_6.9.58_28971_GW_B"
},
"9.9.16-29271": {
"appid": 537249813,
"qua": "V1_WIN_NQ_9.9.16_29271_GW_B"
},
"3.2.13-29271": {
"appid": 537249913,
"qua": "V1_LNX_NQ_3.2.13_29271_GW_B"
},
"6.9.59-29271": {
"appid": 537249863,
"qua": "V1_MAC_NQ_6.9.59_29271_GW_B"
},
"9.9.16-29456": {
"appid": 537249875,
"qua": "V1_WIN_NQ_9.9.16_29456_GW_B"
},
"3.2.13-29456": {
"appid": 537249996,
"qua": "V1_LNX_NQ_3.2.13_29456_GW_B"
},
"6.9.59-29456": {
"appid": 537249961,
"qua": "V1_MAC_NQ_6.9.59_29456_GW_B"
}
}
}

View File

@@ -1,7 +1,8 @@
{
"fileLog": true,
"fileLog": false,
"consoleLog": true,
"fileLogLevel": "debug",
"consoleLogLevel": "info",
"packetBackend": "auto",
"packetServer": ""
}
}

View File

@@ -7,6 +7,14 @@
"recv": "37A9004",
"send": "37A4BD0"
},
"6.9.56-28418-x64": {
"send": "4471360",
"recv": "4473BCC"
},
"6.9.56-28418-arm64": {
"send": "3FBDBF8",
"recv": "3FC0410"
},
"9.9.15-28498-x64": {
"recv": "37A9004",
"send": "37A4BD0"
@@ -18,5 +26,61 @@
"3.2.13-28788-x64": {
"send": "A0CEC20",
"recv": "A0D2520"
},
"3.2.13-28788-arm64": {
"send": "6E91018",
"recv": "6E94850"
},
"9.9.16-28971-x64": {
"send": "38079F0",
"recv": "380BE24"
},
"3.2.13-28971-x64": {
"send": "A0CEF60",
"recv": "A0D2860"
},
"3.2.12-28971-arm64": {
"send": "6E91318",
"recv": "6E94B50"
},
"6.9.58-28971-x64": {
"send": "449ACA0",
"recv": "449D50C"
},
"6.9.58-28971-arm64": {
"send": "3FE0DB0",
"recv": "3FE35C8"
},
"9.9.16-29271-x64": {
"send": "3833510",
"recv": "3837944"
},
"3.2.13-29271-x64": {
"send": "A11E680",
"recv": "A121F80"
},
"3.2.13-29271-arm64": {
"send": "6ECA098",
"recv": "6ECD8D0"
},
"9.9.16-29456-x64": {
"send": "3835CD0",
"recv": "383A104"
},
"3.2.13-29456-x64": {
"send": "A11E820",
"recv": "A122120"
},
"3.2.13-29456-arm64": {
"send": "6ECA130",
"recv": "6ECD968"
},
"6.9.59-29456-x64": {
"send": "44C57A0",
"recv": "44C800C"
},
"6.9.59-29456-arm64": {
"send": "4005FE8",
"recv": "4008800"
}
}
}

View File

@@ -1,31 +0,0 @@
syntax = 'proto3';
package SysMessage;
message EmojiLikeToOthersWrapper1 {
EmojiLikeToOthersWrapper2 wrapper = 1;
}
message EmojiLikeToOthersWrapper2 {
EmojiLikeToOthersWrapper3 body = 1;
}
message EmojiLikeToOthersWrapper3 {
EmojiLikeToOthersMsgSpec msgSpec = 2;
EmojiLikeToOthersAttributes attributes = 3;
}
message EmojiLikeToOthersMsgSpec {
uint32 msgSeq = 1;
}
message EmojiLikeToOthersAttributes {
enum Operation {
FALLBACK = 0;
LIKE = 1;
UNLIKE = 2;
}
string emojiId = 1;
string senderUid = 4;
Operation operation = 5;
}

View File

@@ -1,9 +0,0 @@
syntax = 'proto3';
package SysMessage;
message GreyTipWrapper {
uint32 subTypeId = 1;
uint32 groupCode = 4;
uint32 subTypeIdMinusOne = 13;
bytes rest = 44;
}

View File

@@ -1,18 +0,0 @@
syntax = "proto3";
package SysMessage;
message likeDetail {
string txt = 1;
int64 uin = 3;
string nickname = 5;
}
message likeMsg {
int32 times = 1;
int32 time = 2;
likeDetail detail = 3;
}
message profileLikeTip {
likeMsg msg = 14;
}

View File

@@ -1,36 +0,0 @@
syntax = 'proto3';
package SysMessage;
message SysMessage {
repeated SysMessageHeader header = 1;
repeated SysMessageMsgSpec msgSpec = 2;
SysMessageBodyWrapper bodyWrapper = 3;
}
message SysMessageHeader {
uint32 PeerNumber = 1;
string PeerString = 2;
uint32 Uin = 5;
optional string Uid = 6;
}
message SysMessageMsgSpec {
uint32 msgType = 1;
uint32 subType = 2;
uint32 subSubType = 3;
uint32 msgSeq = 5;
uint32 time = 6;
uint64 msgId = 12;
uint32 other = 13;
}
message SysMessageBodyWrapper {
bytes wrappedBody = 2;
// Find the first [08], or ignore the first 7 bytes?
// And it becomes another ProtoBuf message.
}
message KeyValuePair {
string key = 1;
string value = 2;
}

View File

@@ -0,0 +1,61 @@
// work:further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const LikeDetail = {
txt: ProtoField(1, ScalarType.STRING),
uin: ProtoField(3, ScalarType.INT64),
nickname: ProtoField(5, ScalarType.STRING)
};
const LikeMsg = {
times: ProtoField(1, ScalarType.INT32),
time: ProtoField(2, ScalarType.INT32),
detail: ProtoField(3, () => LikeDetail)
};
const ProfileLikeSubTip = {
msg: ProtoField(14, () => LikeMsg)
};
const ProfileLikeTip = {
msgType: ProtoField(1, ScalarType.INT32),
subType: ProtoField(2, ScalarType.INT32),
content: ProtoField(203, () => ProfileLikeSubTip)
};
const SysMessageHeader = {
PeerNumber: ProtoField(1, ScalarType.UINT32),
PeerString: ProtoField(2, ScalarType.STRING),
Uin: ProtoField(5, ScalarType.UINT32),
Uid: ProtoField(6, ScalarType.STRING, true)
};
const SysMessageMsgSpec = {
msgType: ProtoField(1, ScalarType.UINT32),
subType: ProtoField(2, ScalarType.UINT32),
subSubType: ProtoField(3, ScalarType.UINT32),
msgSeq: ProtoField(5, ScalarType.UINT32),
time: ProtoField(6, ScalarType.UINT32),
msgId: ProtoField(12, ScalarType.UINT64),
other: ProtoField(13, ScalarType.UINT32)
};
const SysMessageBodyWrapper = {
wrappedBody: ProtoField(2, ScalarType.BYTES)
};
const SysMessage = {
header: ProtoField(1, () => SysMessageHeader, false, true),
msgSpec: ProtoField(2, () => SysMessageMsgSpec, false, true),
bodyWrapper: ProtoField(3, () => SysMessageBodyWrapper)
};
export function decodeProfileLikeTip(buffer: Uint8Array) {
const msg = new NapProtoMsg(ProfileLikeTip);
return msg.decode(buffer);
}
export function decodeSysMessage(buffer: Uint8Array) {
const msg = new NapProtoMsg(SysMessage);
return msg.decode(buffer);
}

View File

@@ -0,0 +1,49 @@
// work: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

@@ -62,14 +62,32 @@ export function loadQQWrapper(QQVersion: string): WrapperNodeApi {
process.dlopen(nativemodule, wrapperNodePath);
return nativemodule.exports;
}
export function getMajorPath(QQVersion: string): string {
// major.node
let appPath;
if (os.platform() === 'darwin') {
appPath = path.resolve(path.dirname(process.execPath), '../Resources/app');
} else if (os.platform() === 'linux') {
appPath = path.resolve(path.dirname(process.execPath), './resources/app');
} else {
appPath = path.resolve(path.dirname(process.execPath), `./versions/${QQVersion}/`);
}
let majorPath = path.resolve(appPath, 'major.node');
if (!fs.existsSync(majorPath)) {
majorPath = path.join(appPath, `./resources/app/major.node`);
}
//老版本兼容 未来去掉
if (!fs.existsSync(majorPath)) {
majorPath = path.join(path.dirname(process.execPath), `./resources/app/versions/${QQVersion}/major.node`);
}
return majorPath;
}
export class NapCatCore {
readonly context: InstanceContext;
readonly apis: StableNTApiWrapper;
readonly eventWrapper: NTEventWrapper;
// readonly eventChannel: NTEventChannel;
NapCatDataPath: string;
NapCatTempPath: string;
NapCatDataPath: string = '';
NapCatTempPath: string = '';
apis: StableNTApiWrapper;
// runtime info, not readonly
selfInfo: SelfInfo;
util: NodeQQNTWrapperUtil;
@@ -93,6 +111,8 @@ export class NapCatCore {
UserApi: new NTQQUserApi(this.context, this),
GroupApi: new NTQQGroupApi(this.context, this),
};
}
async initCore() {
this.NapCatDataPath = path.join(this.dataPath, 'NapCat');
fs.mkdirSync(this.NapCatDataPath, { recursive: true });
this.NapCatTempPath = path.join(this.NapCatDataPath, 'temp');
@@ -100,7 +120,13 @@ export class NapCatCore {
if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
}
//遍历this.apis[i].initApi 如果存在该函数进行async 调用
for (const apiKey in this.apis) {
const api = this.apis[apiKey as keyof StableNTApiWrapper];
if ('initApi' in api && typeof api.initApi === 'function') {
await api.initApi();
}
}
this.initNapCatCoreListeners().then().catch(this.context.logger.logError.bind(this.context.logger));
this.context.logger.setFileLogEnabled(
@@ -114,7 +140,6 @@ export class NapCatCore {
this.configLoader.configData.consoleLogLevel as LogLevel,
);
}
get dataPath(): string {
let result = this.context.wrapper.NodeQQNTWrapperUtil.getNTUserDataInfoConfig();
if (!result) {
@@ -140,7 +165,7 @@ export class NapCatCore {
};
//await sleep(2500);
this.context.session.getMsgService().addKernelMsgListener(
proxiedListenerOf(msgListener, this.context.logger) as any,
proxiedListenerOf(msgListener, this.context.logger),
);
const profileListener = new NodeIKernelProfileListener();
@@ -185,7 +210,7 @@ export class NapCatCore {
});
};
groupListener.onMemberListChange = (arg) => {
// todo: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
// work:应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;
@@ -195,7 +220,7 @@ export class NapCatCore {
if (existMember) {
Object.assign(existMember, member);
} else {
existMembers!.set(uid, member);
existMembers.set(uid, member);
}
//移除成员
if (member.isDelete) {
@@ -236,7 +261,7 @@ export class NapCatCore {
}
};
this.context.session.getGroupService().addKernelGroupListener(
proxiedListenerOf(groupListener, this.context.logger) as any,
proxiedListenerOf(groupListener, this.context.logger),
);
}
@@ -276,7 +301,7 @@ export async function genSessionConfig(
d2: '',
d2Key: '',
machineId: '',
platform: systemPlatform, // 3是Windows?
platform: systemPlatform, // 3是Windows?
platVer: systemVersion, // 系统版本号, 应该可以固定
appid: QQVersionAppid,
rdeliveryConfig: {

View File

@@ -3,57 +3,57 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/entities';
export type OnBuddyChangeParams = BuddyCategoryType[];
export class NodeIKernelBuddyListener {
onBuddyListChangedV2(arg: unknown): void {
onBuddyListChangedV2(arg: unknown): any {
}
onAddBuddyNeedVerify(arg: unknown) {
onAddBuddyNeedVerify(arg: unknown): any {
}
onAddMeSettingChanged(arg: unknown) {
onAddMeSettingChanged(arg: unknown): any {
}
onAvatarUrlUpdated(arg: unknown) {
onAvatarUrlUpdated(arg: unknown): any {
}
onBlockChanged(arg: unknown) {
onBlockChanged(arg: unknown): any {
}
onBuddyDetailInfoChange(arg: unknown) {
onBuddyDetailInfoChange(arg: unknown): any {
}
onBuddyInfoChange(arg: unknown) {
onBuddyInfoChange(arg: unknown): any {
}
onBuddyListChange(arg: OnBuddyChangeParams): void {
onBuddyListChange(arg: OnBuddyChangeParams): any {
}
onBuddyRemarkUpdated(arg: unknown): void {
onBuddyRemarkUpdated(arg: unknown): any {
}
onBuddyReqChange(arg: FriendRequestNotify): void {
onBuddyReqChange(arg: FriendRequestNotify): any {
}
onBuddyReqUnreadCntChange(arg: unknown): void {
onBuddyReqUnreadCntChange(arg: unknown): any {
}
onCheckBuddySettingResult(arg: unknown): void {
onCheckBuddySettingResult(arg: unknown): any {
}
onDelBatchBuddyInfos(arg: unknown): void {
onDelBatchBuddyInfos(arg: unknown): any {
}
onDoubtBuddyReqChange(arg: unknown): void {
onDoubtBuddyReqChange(arg: unknown): any {
}
onDoubtBuddyReqUnreadNumChange(arg: unknown): void {
onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
}
onNickUpdated(arg: unknown): void {
onNickUpdated(arg: unknown): any {
}
onSmartInfos(arg: unknown): void {
onSmartInfos(arg: unknown): any {
}
onSpacePermissionInfos(arg: unknown): void {
onSpacePermissionInfos(arg: unknown): any {
}
}

View File

@@ -7,19 +7,19 @@ export class NodeIKernelFileAssistantListener {
fileSpeed: number,
thumbPath: string | null,
filePath: string | null,
}) {
}): any {
}
onSessionListChanged(...args: unknown[]) {
onSessionListChanged(...args: unknown[]): any {
}
onSessionChanged(...args: unknown[]) {
onSessionChanged(...args: unknown[]): any {
}
onFileListChanged(...args: unknown[]) {
onFileListChanged(...args: unknown[]): any {
}
onFileSearch(searchResult: SearchResultWrapper) {
onFileSearch(searchResult: SearchResultWrapper): any {
}
}

View File

@@ -1,84 +1,85 @@
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify } from '@/core/entities';
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): void { }
onGroupListInited(listEmpty: boolean): any { }
// 发现于Win 9.9.9 23159
onGroupMemberLevelInfoChange(...args: unknown[]): void {
onGroupMemberLevelInfoChange(...args: unknown[]): any {
}
onGetGroupBulletinListResult(...args: unknown[]) {
onGetGroupBulletinListResult(...args: unknown[]): any {
}
onGroupAllInfoChange(...args: unknown[]) {
onGroupAllInfoChange(...args: unknown[]): any {
}
onGroupBulletinChange(...args: unknown[]) {
onGroupBulletinChange(...args: unknown[]): any {
}
onGroupBulletinRemindNotify(...args: unknown[]) {
onGroupBulletinRemindNotify(...args: unknown[]): any {
}
onGroupArkInviteStateResult(...args: unknown[]) {
onGroupArkInviteStateResult(...args: unknown[]): any {
}
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]) {
onGroupBulletinRichMediaDownloadComplete(...args: unknown[]): any {
}
onGroupConfMemberChange(...args: unknown[]) {
onGroupConfMemberChange(...args: unknown[]): any {
}
onGroupDetailInfoChange(...args: unknown[]) {
onGroupDetailInfoChange(...args: unknown[]): any {
}
onGroupExtListUpdate(...args: unknown[]) {
onGroupExtListUpdate(...args: unknown[]): any {
}
onGroupFirstBulletinNotify(...args: unknown[]) {
onGroupFirstBulletinNotify(...args: unknown[]): any {
}
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]) {
onGroupListUpdate(updateType: GroupListUpdateType, groupList: Group[]): any {
}
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]) {
onGroupNotifiesUpdated(dboubt: boolean, notifies: GroupNotify[]): any {
}
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]) {
onGroupBulletinRichMediaProgressUpdate(...args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
}
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]) {
onGroupSingleScreenNotifies(doubt: boolean, seq: string, notifies: GroupNotify[]): any {
}
onGroupsMsgMaskResult(...args: unknown[]) {
onGroupsMsgMaskResult(...args: unknown[]): any {
}
onGroupStatisticInfoChange(...args: unknown[]) {
onGroupStatisticInfoChange(...args: unknown[]): any {
}
onJoinGroupNotify(...args: unknown[]) {
onJoinGroupNotify(...args: unknown[]): any {
}
onJoinGroupNoVerifyFlag(...args: unknown[]) {
onJoinGroupNoVerifyFlag(...args: unknown[]): any {
}
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>) {
onMemberInfoChange(groupCode: string, dateSource: DataSource, members: Map<string, GroupMember>): any {
}
onMemberListChange(arg: {
sceneId: string,
ids: string[],
infos: Map<string, GroupMember>, // uid -> GroupMember
finish: boolean,
hasPrev: boolean,
hasNext: boolean,
hasRobot: boolean
}) {
}): any {
}
onSearchMemberChange(...args: unknown[]) {
onSearchMemberChange(...args: unknown[]): any {
}
onShutUpMemberListChanged(...args: unknown[]) {
onShutUpMemberListChanged(groupCode: string, members: Array<ShutUpGroupMember>): any {
}
}

View File

@@ -1,57 +1,57 @@
export class NodeIKernelLoginListener {
onLoginConnected(...args: any[]): void {
onLoginConnected(...args: any[]): any {
}
onLoginDisConnected(...args: any[]): void {
onLoginDisConnected(...args: any[]): any {
}
onLoginConnecting(...args: any[]): void {
onLoginConnecting(...args: any[]): any {
}
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): void {
onQRCodeGetPicture(arg: { pngBase64QrcodeData: string, qrcodeUrl: string }): any {
// let base64Data: string = arg.pngBase64QrcodeData
// base64Data = base64Data.split("data:image/png;base64,")[1]
// let buffer = Buffer.from(base64Data, 'base64')
// console.log("onQRCodeGetPicture", arg);
}
onQRCodeLoginPollingStarted(...args: any[]): void {
onQRCodeLoginPollingStarted(...args: any[]): any {
}
onQRCodeSessionUserScaned(...args: any[]): void {
onQRCodeSessionUserScaned(...args: any[]): any {
}
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): void {
onQRCodeLoginSucceed(arg: QRCodeLoginSucceedResult): any {
}
onQRCodeSessionFailed(...args: any[]): void {
onQRCodeSessionFailed(...args: any[]): any {
}
onLoginFailed(...args: any[]): void {
onLoginFailed(...args: any[]): any {
}
onLogoutSucceed(...args: any[]): void {
onLogoutSucceed(...args: any[]): any {
}
onLogoutFailed(...args: any[]): void {
onLogoutFailed(...args: any[]): any {
}
onUserLoggedIn(...args: any[]): void {
onUserLoggedIn(...args: any[]): any {
}
onQRCodeSessionQuickLoginFailed(...args: any[]): void {
onQRCodeSessionQuickLoginFailed(...args: any[]): any {
}
onPasswordLoginFailed(...args: any[]): void {
onPasswordLoginFailed(...args: any[]): any {
}
OnConfirmUnusualDeviceFailed(...args: any[]): void {
OnConfirmUnusualDeviceFailed(...args: any[]): any {
}
onQQLoginNumLimited(...args: any[]): void {
onQQLoginNumLimited(...args: any[]): any {
}
onLoginState(...args: any[]): void {
onLoginState(...args: any[]): any {
}
}

View File

@@ -1,4 +1,4 @@
import { ChatType, RawMessage } from '@/core/entities';
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities';
import { CommonFileInfo } from '@/core';
export interface OnRichMediaDownloadCompleteParams {
@@ -20,8 +20,8 @@ export interface OnRichMediaDownloadCompleteParams {
fileSrvErrCode: string,
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown | null,
userUsedSpacePerDay: unknown | null
userTotalSpacePerDay: unknown,
userUsedSpacePerDay: unknown
}
export interface GroupFileInfoUpdateParamType {
@@ -94,108 +94,108 @@ export interface TempOnRecvParams {
}
export class NodeIKernelMsgListener {
onAddSendMsg(msgRecord: RawMessage) {
onAddSendMsg(msgRecord: RawMessage): any {
}
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown) {
onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): any {
}
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown) {
onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): any {
}
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown) {
onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): any {
}
onContactUnreadCntUpdate(hashMap: unknown) {
onContactUnreadCntUpdate(hashMap: unknown): any {
}
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown) {
onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): any {
}
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown) {
onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): any {
}
onEmojiDownloadComplete(emojiNotifyInfo: unknown) {
onEmojiDownloadComplete(emojiNotifyInfo: unknown): any {
}
onEmojiResourceUpdate(emojiResourceInfo: unknown) {
onEmojiResourceUpdate(emojiResourceInfo: unknown): any {
}
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onFileMsgCome(arrayList: unknown) {
onFileMsgCome(arrayList: unknown): any {
}
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown) {
onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onFirstViewGroupGuildMapping(arrayList: unknown) {
onFirstViewGroupGuildMapping(arrayList: unknown): any {
}
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown) {
onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): any {
}
onGroupFileInfoAdd(groupItem: unknown) {
onGroupFileInfoAdd(groupItem: unknown): any {
}
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType) {
onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType): any {
}
onGroupGuildUpdate(groupGuildNotifyInfo: unknown) {
onGroupGuildUpdate(groupGuildNotifyInfo: unknown): any {
}
onGroupTransferInfoAdd(groupItem: unknown) {
onGroupTransferInfoAdd(groupItem: unknown): any {
}
onGroupTransferInfoUpdate(groupFileListResult: unknown) {
onGroupTransferInfoUpdate(groupFileListResult: unknown): any {
}
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown) {
onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): any {
}
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown) {
onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): any {
}
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown) {
onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): any {
}
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown) {
onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): any {
}
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown) {
onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): any {
}
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown) {
onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): any {
}
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown) {
onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): any {
}
@@ -208,176 +208,176 @@ export class NodeIKernelMsgListener {
statusText: string;
timestamp: string;
toUin: string;
}) {
}): any {
}
onKickedOffLine(kickedInfo: unknown) {
onKickedOffLine(kickedInfo: KickedOffLineInfo): any {
}
onLineDev(arrayList: unknown) {
onLineDev(arrayList: unknown): any {
}
onLogLevelChanged(j2: unknown) {
onLogLevelChanged(j2: unknown): any {
}
onMsgAbstractUpdate(arrayList: unknown) {
onMsgAbstractUpdate(arrayList: unknown): any {
}
onMsgBoxChanged(arrayList: unknown) {
onMsgBoxChanged(arrayList: unknown): any {
}
onMsgDelete(contact: unknown, arrayList: unknown) {
onMsgDelete(contact: unknown, arrayList: unknown): any {
}
onMsgEventListUpdate(hashMap: unknown) {
onMsgEventListUpdate(hashMap: unknown): any {
}
onMsgInfoListAdd(arrayList: unknown) {
onMsgInfoListAdd(arrayList: unknown): any {
}
onMsgInfoListUpdate(msgList: RawMessage[]) {
onMsgInfoListUpdate(msgList: RawMessage[]): any {
}
onMsgQRCodeStatusChanged(i2: unknown) {
onMsgQRCodeStatusChanged(i2: unknown): any {
}
onMsgRecall(i2: unknown, str: unknown, j2: unknown) {
onMsgRecall(i2: unknown, str: unknown, j2: unknown): any {
}
onMsgSecurityNotify(msgRecord: unknown) {
onMsgSecurityNotify(msgRecord: unknown): any {
}
onMsgSettingUpdate(msgSetting: unknown) {
onMsgSettingUpdate(msgSetting: unknown): any {
}
onNtFirstViewMsgSyncEnd() {
onNtFirstViewMsgSyncEnd(): any {
}
onNtMsgSyncEnd() {
onNtMsgSyncEnd(): any {
}
onNtMsgSyncStart() {
onNtMsgSyncStart(): any {
}
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown) {
onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
}
onRecvGroupGuildFlag(i2: unknown) {
onRecvGroupGuildFlag(i2: unknown): any {
}
onRecvMsg(arrayList: RawMessage[]) {
onRecvMsg(arrayList: RawMessage[]): any {
}
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown) {
onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): any {
}
onRecvOnlineFileMsg(arrayList: unknown) {
onRecvOnlineFileMsg(arrayList: unknown): any {
}
onRecvS2CMsg(arrayList: unknown) {
onRecvS2CMsg(arrayList: unknown): any {
}
onRecvSysMsg(arrayList: Array<number>) {
onRecvSysMsg(arrayList: Array<number>): any {
}
onRecvUDCFlag(i2: unknown) {
onRecvUDCFlag(i2: unknown): any {
}
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams) {
onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
}
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown) {
onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): any {
}
onRichMediaUploadComplete(fileTransNotifyInfo: unknown) {
onRichMediaUploadComplete(fileTransNotifyInfo: unknown): any {
}
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown) {
onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): any {
}
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown) {
onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): any {
}
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown) {
onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): any {
}
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams) {
onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): any {
}
onUnreadCntAfterFirstView(hashMap: unknown) {
onUnreadCntAfterFirstView(hashMap: unknown): any {
}
onUnreadCntUpdate(hashMap: unknown) {
onUnreadCntUpdate(hashMap: unknown): any {
}
onUserChannelTabStatusChanged(z: unknown) {
onUserChannelTabStatusChanged(z: unknown): any {
}
onUserOnlineStatusChanged(z: unknown) {
onUserOnlineStatusChanged(z: unknown): any {
}
onUserTabStatusChanged(arrayList: unknown) {
onUserTabStatusChanged(arrayList: unknown): any {
}
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
}
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown) {
onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
}
// 第一次发现于Linux
onUserSecQualityChanged(...args: unknown[]) {
onUserSecQualityChanged(...args: unknown[]): any {
}
onMsgWithRichLinkInfoUpdate(...args: unknown[]) {
onMsgWithRichLinkInfoUpdate(...args: unknown[]): any {
}
onRedTouchChanged(...args: unknown[]) {
onRedTouchChanged(...args: unknown[]): any {
}
// 第一次发现于Win 9.9.9-23159
onBroadcastHelperProgerssUpdate(...args: unknown[]) {
onBroadcastHelperProgerssUpdate(...args: unknown[]): any {
}
}

View File

@@ -5,67 +5,67 @@ export class NodeIKernelProfileListener {
}
onProfileSimpleChanged(...args: unknown[]) {
onProfileSimpleChanged(...args: unknown[]): any {
}
onProfileDetailInfoChanged(profile: User) {
onProfileDetailInfoChanged(profile: User): any {
}
onStatusUpdate(...args: unknown[]) {
onStatusUpdate(...args: unknown[]): any {
}
onSelfStatusChanged(...args: unknown[]) {
onSelfStatusChanged(...args: unknown[]): any {
}
onStrangerRemarkChanged(...args: unknown[]) {
onStrangerRemarkChanged(...args: unknown[]): any {
}
onMemberListChange(...args: unknown[]) {
onMemberListChange(...args: unknown[]): any {
}
onMemberInfoChange(...args: unknown[]) {
onMemberInfoChange(...args: unknown[]): any {
}
onGroupListUpdate(...args: unknown[]) {
onGroupListUpdate(...args: unknown[]): any {
}
onGroupAllInfoChange(...args: unknown[]) {
onGroupAllInfoChange(...args: unknown[]): any {
}
onGroupDetailInfoChange(...args: unknown[]) {
onGroupDetailInfoChange(...args: unknown[]): any {
}
onGroupConfMemberChange(...args: unknown[]) {
onGroupConfMemberChange(...args: unknown[]): any {
}
onGroupExtListUpdate(...args: unknown[]) {
onGroupExtListUpdate(...args: unknown[]): any {
}
onGroupNotifiesUpdated(...args: unknown[]) {
onGroupNotifiesUpdated(...args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]) {
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
}
onGroupMemberLevelInfoChange(...args: unknown[]) {
onGroupMemberLevelInfoChange(...args: unknown[]): any {
}
onGroupBulletinChange(...args: unknown[]) {
onGroupBulletinChange(...args: unknown[]): any {
}
}

View File

@@ -1,25 +1,25 @@
export class NodeIKernelRecentContactListener {
onDeletedContactsNotify(...args: unknown[]) {
onDeletedContactsNotify(...args: unknown[]): any {
}
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number) {
onRecentContactNotification(msgList: any, arg0: { msgListUnreadCnt: string }, arg1: number): any {
}
onMsgUnreadCountUpdate(...args: unknown[]) {
onMsgUnreadCountUpdate(...args: unknown[]): any {
}
onGuildDisplayRecentContactListChanged(...args: unknown[]) {
onGuildDisplayRecentContactListChanged(...args: unknown[]): any {
}
onRecentContactListChanged(...args: unknown[]) {
onRecentContactListChanged(...args: unknown[]): any {
}
onRecentContactListChangedVer2(...args: unknown[]) {
onRecentContactListChangedVer2(...args: unknown[]): any {
}
}

View File

@@ -1,13 +1,13 @@
export class NodeIKernelRobotListener {
onRobotFriendListChanged(...args: unknown[]) {
onRobotFriendListChanged(...args: unknown[]): any {
}
onRobotListChanged(...args: unknown[]) {
onRobotListChanged(...args: unknown[]): any {
}
onRobotProfileChanged(...args: unknown[]) {
onRobotProfileChanged(...args: unknown[]): any {
}
}

View File

@@ -57,7 +57,7 @@ export interface GroupSearchResult {
}
export interface NodeIKernelSearchListener {
onSearchGroupResult(params: GroupSearchResult): void;
onSearchGroupResult(params: GroupSearchResult): any;
onSearchFileKeywordsResult(params: {
searchId: string,
@@ -93,5 +93,5 @@ export interface NodeIKernelSearchListener {
end: number
}[]
}[]
}): void;
}): any;
}

View File

@@ -1,25 +1,25 @@
export class NodeIKernelSessionListener {
onNTSessionCreate(args: unknown) {
onNTSessionCreate(args: unknown): any {
}
onGProSessionCreate(args: unknown) {
onGProSessionCreate(args: unknown): any {
}
onSessionInitComplete(args: unknown) {
onSessionInitComplete(args: unknown): any {
}
onOpentelemetryInit(args: unknown) {
onOpentelemetryInit(args: unknown): any {
}
onUserOnlineResult(args: unknown) {
onUserOnlineResult(args: unknown): any {
}
onGetSelfTinyId(args: unknown) {
onGetSelfTinyId(args: unknown): any {
}
}

View File

@@ -1,21 +1,21 @@
export class NodeIKernelStorageCleanListener {
onCleanCacheProgressChanged(args: unknown) {
onCleanCacheProgressChanged(args: unknown): any {
}
onScanCacheProgressChanged(args: unknown) {
onScanCacheProgressChanged(args: unknown): any {
}
onCleanCacheStorageChanged(args: unknown) {
onCleanCacheStorageChanged(args: unknown): any {
}
onFinishScan(args: unknown) {
onFinishScan(args: unknown): any {
}
onChatCleanDone(args: unknown) {
onChatCleanDone(args: unknown): any {
}
}

View File

@@ -1,2 +1,5 @@
export class NodeIKernelTicketListener {
listener(): any {
}
}

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