Compare commits

...

161 Commits

Author SHA1 Message Date
手瓜一十雪
902fe907bd refactor: 持续重构project结构与定义 2024-11-21 14:36:06 +08:00
手瓜一十雪
bbb4ad7d95 rename: project 2024-11-21 14:30:21 +08:00
手瓜一十雪
24bc9f35b2 fix: NTGroupRequestOperateTypes 2024-11-21 14:28:04 +08:00
手瓜一十雪
52c68a3bfb fix: NTGroupRequestOperateTypes 2024-11-21 14:27:11 +08:00
手瓜一十雪
d982bcdad5 fix: typo 2024-11-21 14:21:46 +08:00
手瓜一十雪
b8165242f0 feat: NTSex 2024-11-21 14:21:14 +08:00
手瓜一十雪
7ce95bca04 feat: NTGroupMemberRole 2024-11-21 14:15:22 +08:00
手瓜一十雪
cd212abd5f fix: TipGroupElementType 2024-11-21 14:10:52 +08:00
手瓜一十雪
e5b063accb feat: MemberAddShowType 2024-11-21 13:44:21 +08:00
手瓜一十雪
eeef5409dc fix: PicSubType 2024-11-21 13:10:49 +08:00
手瓜一十雪
2bf8d8f791 fix 标准类型 2024-11-21 13:06:13 +08:00
手瓜一十雪
56e62392a6 chore: webui backend readme 2024-11-21 12:24:53 +08:00
手瓜一十雪
2ecf04c78c chore: LICENSE 2024-11-21 12:23:51 +08:00
手瓜一十雪
a19358da5b refactor: 项目结构 2024-11-21 11:52:50 +08:00
手瓜一十雪
a5d4998933 refactor: 通过@实现定位 2024-11-21 11:39:44 +08:00
手瓜一十雪
8edbe54456 fix: index 2024-11-21 11:25:06 +08:00
手瓜一十雪
e898915d01 rename: 使用@搜寻 2024-11-21 11:16:25 +08:00
手瓜一十雪
b2075130d9 style: lint 2024-11-21 11:09:23 +08:00
手瓜一十雪
02e39b5714 fix: typo 2024-11-21 11:06:03 +08:00
手瓜一十雪
de64b03054 refactor: 旧代码移除 2024-11-21 11:01:35 +08:00
手瓜一十雪
fa70eec3d8 refactor: core类型refactor结束 2024-11-21 10:47:12 +08:00
手瓜一十雪
583ec10c7c fix: 规范化类型 2024-11-21 10:43:05 +08:00
手瓜一十雪
38a098c77d refactor: type 2024-11-21 10:36:08 +08:00
pk5ls20
d17674d06e fix: #553
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 3m49s
Build Action / Build-Shell (push) Failing after 3m21s
2024-11-20 20:58:36 +08:00
pk5ls20
0b839258aa fix: ci 2024-11-20 19:51:29 +08:00
pk5ls20
50e207cf6f chore: remove useless log 2024-11-20 17:30:50 +08:00
pk5ls20
5d2d8c7123 fix: #551
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 3m37s
Build Action / Build-Shell (push) Failing after 2m58s
2024-11-20 17:00:01 +08:00
pk5ls20
23702f412c chore: link 2024-11-20 16:23:04 +08:00
pk5ls20
31e94792c4 Merge pull request #548 from huankong233/main 2024-11-20 14:45:28 +08:00
huankong233
249afdce81 revent: 对手动拆分chunk进行回滚 2024-11-20 14:15:03 +08:00
huankong233
ee8f381341 feat: 增强webui对老设备的支持 2024-11-20 14:02:34 +08:00
Mlikiowa
83f3df76cd release: v4.1.12 2024-11-20 04:22:12 +00:00
手瓜一十雪
16195ca52b fix: old version handle 2024-11-20 12:21:45 +08:00
Mlikiowa
d5f492775e release: v4.1.11 2024-11-20 02:20:35 +00:00
手瓜一十雪
1f273a8799 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2024-11-20 10:20:00 +08:00
手瓜一十雪
f44f6fd1e9 fix: 文件后清除 2024-11-20 10:19:43 +08:00
Mlikiowa
21ca13789e release: v4.1.9 2024-11-20 02:18:14 +00:00
手瓜一十雪
648faedca6 fix: #544 2024-11-20 10:16:41 +08:00
Mlikiowa
3a6748ae37 release: v4.1.8
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 13s
Build Action / Build-Shell (push) Failing after 3m22s
2024-11-19 10:45:26 +00:00
手瓜一十雪
4d4b1ad26c fix: #543 2024-11-19 18:44:56 +08:00
手瓜一十雪
e42fbea918 feat: 扩展rkey支持 2024-11-19 17:58:43 +08:00
Mlikiowa
48b648b0fb release: v4.17
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 2m22s
Build Action / Build-Shell (push) Failing after 2m23s
2024-11-19 08:30:06 +00:00
手瓜一十雪
68e86b07c7 fix: #543 2024-11-19 16:29:33 +08:00
手瓜一十雪
12cb500818 refactor: rename OB11BaseEvent 2024-11-19 12:55:42 +08:00
手瓜一十雪
9ffaab178a refactor: Action 2024-11-19 12:49:51 +08:00
pk5ls20
d4fbbd6711 fix: #539
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 2m33s
Build Action / Build-Shell (push) Failing after 2m17s
2024-11-19 01:12:49 +08:00
pk5ls20
ded53cd348 fix: link
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 3m48s
Build Action / Build-Shell (push) Failing after 2m49s
2024-11-18 23:22:25 +08:00
手瓜一十雪
be9e80c87b fix: 清除无效链接 2024-11-18 20:40:43 +08:00
Mlikiowa
e9fe6f28cc release: v4.1.6 2024-11-18 11:51:39 +00:00
手瓜一十雪
0b8bf739e9 fix: event 2024-11-18 19:51:08 +08:00
手瓜一十雪
0222664db8 fix: type
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 14m48s
Build Action / Build-Shell (push) Failing after 14m43s
2024-11-17 16:02:25 +08:00
手瓜一十雪
a88792e452 docs: 没有参考故移除 2024-11-17 16:01:44 +08:00
手瓜一十雪
ad45400742 docs: 调整更新速度与Packet重构 2024-11-17 15:57:21 +08:00
手瓜一十雪
53e5ba03be fix 2024-11-17 15:56:48 +08:00
手瓜一十雪
b587d6b91d fix: (SetGroupSign) BaseAction-->GetPacketStatusDepends 2024-11-17 15:37:10 +08:00
手瓜一十雪
5e750d4ee9 feat: uploadQunAlbum未测试
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 2m40s
Build Action / Build-Shell (push) Failing after 2m38s
2024-11-17 13:39:57 +08:00
Mlikiowa
50fb32f81c release: v4.1.5 2024-11-17 03:39:17 +00:00
手瓜一十雪
6c46cdd947 fix: error 2024-11-17 11:33:01 +08:00
手瓜一十雪
372452fbee fix: 消息上报 2024-11-17 11:29:27 +08:00
手瓜一十雪
417ef5d335 Revert "fix"
This reverts commit 9c534f8afd.
2024-11-17 11:21:48 +08:00
手瓜一十雪
9c534f8afd fix 2024-11-17 11:12:14 +08:00
pk5ls20
ecd426bb80 refactor: webui network 2024-11-17 08:17:09 +08:00
pk5ls20
f74ef273de fix: workflow
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 3m25s
Build Action / Build-Shell (push) Failing after 2m52s
2024-11-17 06:24:58 +08:00
pk5ls20
f913e0b027 chore: workflow 2024-11-17 06:23:33 +08:00
pk5ls20
f7268c30ca chore: revert todo 2024-11-17 05:28:46 +08:00
pk5ls20
0f5ef03d63 chore: try todo x2 2024-11-17 05:21:35 +08:00
pk5ls20
745276d0f0 chore: try todo 2024-11-17 05:16:18 +08:00
pk5ls20
2e108a4bd6 feat: error stack 2024-11-17 04:43:29 +08:00
pk5ls20
666da80ef5 feat: version display 2024-11-17 03:43:09 +08:00
pk5ls20
cc73104d62 chore: eslint 2024-11-17 03:35:20 +08:00
手瓜一十雪
3c10b82bab Merge branch 'main' of https://github.com/NapNeko/NapCatQQ
Some checks failed
Build Action / Build-LiteLoader (push) Failing after 11s
Build Action / Build-Shell (push) Failing after 14s
2024-11-16 20:35:31 +08:00
手瓜一十雪
9a65dae6a2 fix: #531 2024-11-16 20:32:52 +08:00
Mlikiowa
f26cd8cdc9 release: v4.1.3 2024-11-16 12:22:06 +00:00
手瓜一十雪
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
手瓜一十雪
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
手瓜一十雪
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
260 changed files with 5271 additions and 5953 deletions

View File

@@ -1,8 +1,7 @@
name: "Build Action"
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions: write-all
@@ -11,54 +10,38 @@ jobs:
Build-LiteLoader:
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
with:
repository: 'NapNeko/NapCatQQ'
submodules: true
ref: main
token: ${{ secrets.NAPCAT_BUILD }}
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NuCat Framework
run: |
npm i
npm run build:framework
cd dist
npm i --omit=dev
- name: Clone Main Repository
uses: actions/checkout@v4
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NapCat.Framework
run: |
npm i && cd napcat.webui && npm i && cd .. || exit 1
npm run build:framework && npm run depend || exit 1
rm package-lock.json
cd ..
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Framework
path: dist
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Framework
path: dist
Build-Shell:
runs-on: ubuntu-latest
steps:
- name: Clone Main Repository
uses: actions/checkout@v4
with:
repository: 'NapNeko/NapCatQQ'
submodules: true
ref: main
token: ${{ secrets.NAPCAT_BUILD }}
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NuCat LiteLoader
run: |
npm i
npm run build:shell
cd dist
npm i --omit=dev
rm package-lock.json
cd ..
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: dist
- name: Clone Main Repository
uses: actions/checkout@v4
- name: Use Node.js 20.X
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Build NapCat.Shell
run: |
npm i && cd napcat.webui && npm i && cd .. || exit 1
npm run build:shell && npm run depend || exit 1
rm package-lock.json
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: NapCat.Shell
path: dist

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

480
LICENSE
View File

@@ -1,343 +1,201 @@
GNU GENERAL PUBLIC Without Social media promotion LICENSE
Version 2, June 1991
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Preamble
1. Definitions.
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
The precise terms and conditions for copying, distribution and
modification follow.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
dYou may use this software in accordance with the above terms,
but you are not allowed to promote this project or your projects
based on this project on any public social media.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
END OF TERMS AND CONDITIONS
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
APPENDIX: How to apply the Apache License to your work.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
Copyright [yyyy] [name of copyright owner]
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
http://www.apache.org/licenses/LICENSE-2.0
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -8,7 +8,7 @@
## 欢迎回家
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 碎碎叨叨
## 特性介绍
- [x] **安装简单**:就算是笨蛋也能使用
- [x] **性能友好**:就算是低内存也能使用
- [x] **接口丰富**:就算是没有也能使用
@@ -26,25 +26,32 @@ NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
[Cloudflare.HKServer](https://napcat.napneko.icu/)
[Cloudflare.Pages](https://napneko.pages.dev/)
[Github.IO](https://napneko.github.io/)
[Server.China](https://napneko.com/)
[Cloudflare.Pages](https://napneko.pages.dev/)
[Server.Other](https://napcat.cyou/)
[Github.IO](https://napneko.github.io/)
## 回家旅途
[QQ Group](https://qm.qq.com/q/VfjAq5HIMS)
[QQ Group](https://qm.qq.com/q/NWP25OeV0c)
## 感谢他们
感谢 [LLOneBot](https://github.com/LLOneBot/LLOneBot)
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
感谢 Tencent Tdesign / Vue3 强力驱动 NapCat.WebUi
不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 延缓Native模块与NapCat对新版QQ适配
为未来持续与高效的使用Native模块 模块代码转为完全非Git仓库的本地保存源码 并进行相关重构
同时为了保证稳定 NapCat 本体通常会在3 Week+的周期进行新版本适配
因此此时推荐使用release指定版本
## 开源附加
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**此外,禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。**

View File

@@ -4,7 +4,7 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
"version": "4.0.2",
"version": "4.1.12",
"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>

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

@@ -0,0 +1,34 @@
{
"name": "napcat.webui",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"webui:lint": "eslint --fix src/**/*.{js,ts,vue}",
"webui:dev": "vite",
"webui:build": "vite build",
"webui:preview": "vite preview"
},
"dependencies": {
"eslint-plugin-prettier": "^5.2.1",
"qrcode": "^1.5.4",
"tdesign-icons-vue-next": "^0.3.3",
"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-legacy": "^5.4.3",
"@vitejs/plugin-vue": "^5.1.4",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"terser": "^5.36.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,66 @@
<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: {{ pkg.version }} </t-tag>
<t-tag class="tag-item" theme="success"> NapCat: {{ napCatVersion }} </t-tag>
<t-tag class="tag-item" theme="success">
TDesign: {{ pkg.dependencies['tdesign-vue-next'] }}
</t-tag>
</span>
</t-list-item>
</t-list>
</div>
</div>
</template>
<script setup lang="ts">
import pkg from '../../package.json';
import { napCatVersion } from '../../../src/common/version';
</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,249 @@
<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 } 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;
type ComponentUnion =
| typeof HttpServerComponent
| typeof HttpClientComponent
| typeof WebsocketServerComponent
| typeof WebsocketClientComponent;
const componentMap: Record<ConfigKey, ComponentUnion> = {
httpServers: HttpServerComponent,
httpClients: HttpClientComponent,
websocketServers: WebsocketServerComponent,
websocketClients: WebsocketClientComponent,
};
const defaultConfigMap: Record<ConfigKey, ConfigUnion> = {
httpServers: httpServerDefaultConfigs,
httpClients: httpClientDefaultConfigs,
websocketServers: websocketServerDefaultConfigs,
websocketClients: websocketClientDefaultConfigs,
};
interface ConfigMap {
httpServers: HttpServerConfig;
httpClients: HttpClientConfig;
websocketServers: WebsocketServerConfig;
websocketClients: WebsocketClientConfig;
}
interface ClientPanel<K extends ConfigKey = ConfigKey> {
name: string;
key: K;
data: ConfigMap[K];
}
const activeTab = ref<number>(0);
const isDialogVisible = ref(false);
const newTab = ref<{ name: string; type: ConfigKey }>({ name: '', type: 'httpServers' });
const clientPanelData: Ref<ClientPanel[]> = ref([]);
const getComponent = (type: ConfigKey) => {
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 = <K extends ConfigKey>(configs: ConfigMap[K][], key: K) => {
configs.forEach((config) => clientPanelData.value.push({ name: config.name, data: config, key }));
};
const addConfigDataToPanel = (data: NetworkConfig) => {
(Object.keys(data) as ConfigKey[]).forEach((key) => {
addToPanel(data[key], key);
});
};
const parsePanelData = (): NetworkConfig => {
const result: NetworkConfig = {
httpServers: [],
httpClients: [],
websocketServers: [],
websocketClients: [],
};
clientPanelData.value.forEach((panel) => {
(result[panel.key] as Array<typeof panel.data>).push(panel.data);
});
return result;
};
const 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);
}
};
const saveConfig = async () => {
const config = parsePanelData();
const userConfig = await getOB11Config();
if (!userConfig) {
await MessagePlugin.error('无法获取配置!');
return;
}
userConfig.network = config;
const success = await setOB11Config(userConfig);
if (success) {
await MessagePlugin.success('配置保存成功');
} else {
await MessagePlugin.error('配置保存失败');
}
};
const showAddTabDialog = () => {
newTab.value = { name: '', type: 'httpServers' };
isDialogVisible.value = true;
};
const addTab = async () => {
const { name, type } = newTab.value;
if (clientPanelData.value.some((panel) => panel.name === name)) {
await MessagePlugin.error('选项卡名称已存在');
return;
}
const defaultConfig = structuredClone(defaultConfigMap[type]);
defaultConfig.name = name;
clientPanelData.value.push({ name, data: defaultConfig, key: type });
isDialogVisible.value = false;
await nextTick();
activeTab.value = clientPanelData.value.length - 1;
await MessagePlugin.success('选项卡添加成功');
};
const removeTab = async (payload: { value: string; index: number; e: PointerEvent }) => {
clientPanelData.value.splice(payload.index, 1);
activeTab.value = Math.max(0, activeTab.value - 1);
await saveConfig();
};
onMounted(() => {
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,68 @@
<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,74 @@
<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,71 @@
<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,77 @@
<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,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,41 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
import path from 'path';
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
legacy({
targets: ['defaults', 'not IE 11'],
modernPolyfills: ['web.structured-clone'],
}),
],
base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
proxy: {
'/api': 'http://localhost:6099',
},
},
build: {
chunkSizeWarningLimit: 4000,
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
manualChunks(id: string) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
},
},
},
});

View File

@@ -2,12 +2,15 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.0.2",
"version": "4.1.12",
"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 || exit 1",
"build:shell": "npm run build:webui && vite build --mode shell || exit 1",
"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": {
@@ -48,9 +51,10 @@
},
"dependencies": {
"express": "^5.0.0",
"fluent-ffmpeg": "^2.1.2",
"qrcode-terminal": "^0.12.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0",
"qrcode-terminal": "^0.12.0",
"fluent-ffmpeg": "^2.1.2"
"piscina": "^4.7.0"
}
}
}

View File

@@ -45,7 +45,6 @@ try {
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
sed -i "s/SettingButton(\\"V.*\\", \\"napcat-update-button\\", \\"secondary\\")/SettingButton(\\"V${targetVersion}\\", \\"napcat-update-button\\", \\"secondary\\")/g" ./static/assets/renderer.js
git add .
git commit -m "release: v${targetVersion}"
git push -u origin main`;

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 { LogWrapper } from './log';
import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-wasm';
import { LogWrapper } from '@/common/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

@@ -234,25 +234,33 @@ export class NTEventWrapper {
this.EventTask.get(ListenerMainName)?.get(ListenerSubName)?.set(id, eventCallback);
this.createListenerFunction(ListenerMainName);
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',
),
);
}
const eventResult = this.createEventFunction(serviceAndMethod)!(...(args));
const eventRetHandle = (eventData: any) => {
retEvent = eventData;
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',
),
);
}
};
if (eventResult instanceof Promise) {
eventResult.then((eventResult: any) => {
eventRetHandle(eventResult);
})
.catch(reject);
.catch(reject);
} else {
eventRetHandle(eventResult);
}
},
);
}

View File

@@ -4,7 +4,20 @@ import crypto, { randomUUID } from 'crypto';
import util from 'util';
import path from 'node:path';
import * as fileType from 'file-type';
import { solveProblem } from './helper';
import { solveProblem } from '@/common/helper';
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
}
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string
}
export function isGIF(path: string) {
const buffer = Buffer.alloc(4);
@@ -15,7 +28,7 @@ export function isGIF(path: string) {
}
// 定义一个异步函数来检查文件是否存在
export function checkFileReceived(path: string, timeout: number = 3000): Promise<void> {
export function checkFileExist(path: string, timeout: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
@@ -34,7 +47,7 @@ export function checkFileReceived(path: string, timeout: number = 3000): Promise
}
// 定义一个异步函数来检查文件是否存在
export async function checkFileReceived2(path: string, timeout: number = 3000): Promise<void> {
export async function checkFileExistV2(path: string, timeout: number = 3000): Promise<void> {
// 使用 Promise.race 来同时进行文件状态检查和超时计时
// Promise.race 会返回第一个解决resolve或拒绝reject的 Promise
await Promise.race([
@@ -75,18 +88,13 @@ export async function file2base64(path: string) {
data: '',
};
try {
// 读取文件内容
// if (!fs.existsSync(path)){
// path = path.replace("\\Ori\\", "\\Thumb\\");
// }
try {
await checkFileReceived(path, 5000);
await checkFileExist(path, 5000);
} catch (e: any) {
result.err = e.toString();
return result;
}
const data = await readFile(path);
// 转换为Base64编码
result.data = data.toString('base64');
} catch (err: any) {
result.err = err.toString();
@@ -118,13 +126,7 @@ export function calculateFileMD5(filePath: string): Promise<string> {
});
}
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
}
async function tryDownload(options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
// const chunks: Buffer[] = [];
let url: string;
let headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
@@ -166,14 +168,6 @@ export async function httpDownload(options: string | HttpDownloadOptions): Promi
return Buffer.from(buffer);
}
type Uri2LocalRes = {
success: boolean,
errMsg: string,
fileName: string,
ext: string,
path: string
}
export async function checkFileV2(filePath: string) {
try {
const ext: string | undefined = (await fileType.fileTypeFromFile(filePath))?.ext;
@@ -242,7 +236,7 @@ export async function uri2local(dir: string, uri: string, filename: string | und
//解析Http和Https协议
if (UriType == FileUriType.Unknown) {
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}
//解析File协议和本地文件
if (UriType == FileUriType.Local) {
@@ -289,5 +283,5 @@ export async function uri2local(dir: string, uri: string, filename: string | und
}
return { success: true, errMsg: '', fileName: filename, ext: fileExt, path: filePath };
}
return { success: false, errMsg: '未知文件类型', fileName: '', ext: '', path: '' };
return { success: false, errMsg: `未知文件类型, uri= ${uri}`, fileName: '', ext: '', path: '' };
}

View File

@@ -2,7 +2,7 @@ import winston, { format, transports } from 'winston';
import { truncateString } from '@/common/helper';
import path from 'node:path';
import fs from 'node:fs';
import { AtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
export enum LogLevel {
DEBUG = 'debug',
@@ -36,7 +36,7 @@ export class LogWrapper {
this.logger = winston.createLogger({
level: 'debug',
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.timestamp({ format: 'MM-DD HH:mm:ss' }),
format.printf(({ timestamp, level, message, ...meta }) => {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`;
@@ -61,7 +61,7 @@ export class LogWrapper {
]
});
this.setLogSelfInfo({ nick: '', uin: '', uid: '' });
this.setLogSelfInfo({ nick: '', uid: '' });
this.cleanOldLogs(logDir);
}
@@ -111,8 +111,8 @@ export class LogWrapper {
});
}
setLogSelfInfo(selfInfo: { nick: string, uin: string, uid: string }) {
const userInfo = `${selfInfo.nick}(${selfInfo.uin})`;
setLogSelfInfo(selfInfo: { nick: string, uid: string }) {
const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo };
}
@@ -270,12 +270,12 @@ function msgElementToText(element: MessageElement, msg: RawMessage, recursiveLev
}
function textElementToText(textElement: any): string {
if (textElement.atType === AtType.notAt) {
if (textElement.atType === NTMsgAtType.ATTYPEUNKNOWN) {
const originalContentLines = textElement.content.split('\n');
return `${originalContentLines[0]}${originalContentLines.length > 1 ? ' ...' : ''}`;
} else if (textElement.atType === AtType.atAll) {
} else if (textElement.atType === NTMsgAtType.ATTYPEALL) {
return `@全体成员`;
} else if (textElement.atType === AtType.atUser) {
} else if (textElement.atType === NTMsgAtType.ATTYPEONE) {
return `${textElement.content} (${textElement.atUid})`;
}
return '';

View File

@@ -1,4 +1,4 @@
import { LogWrapper } from './log';
import { LogWrapper } from '@/common/log';
export function proxyHandlerOf(logger: LogWrapper) {
return {

View File

@@ -2,7 +2,7 @@ import fs from 'node:fs';
import { systemPlatform } from '@/common/system';
import { getDefaultQQVersionConfigInfo, getQQPackageInfoPath, getQQVersionConfigPath, parseAppidFromMajor } from './helper';
import AppidTable from '@/core/external/appid.json';
import { LogWrapper } from './log';
import { LogWrapper } from '@/common/log';
import { getMajorPath } from '@/core';
export class QQBasicInfoWrapper {

View File

@@ -1 +1 @@
export const napCatVersion = '4.0.2';
export const napCatVersion = '4.1.12';

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import { MsfChangeReasonType, MsfStatusType } from "../entities/adapter";
import { MsfChangeReasonType, MsfStatusType } from "../types/adapter";
export class NodeIDependsAdapter {
onMSFStatusChange(statusType: MsfStatusType, changeReasonType: MsfChangeReasonType) {

View File

@@ -1,4 +1,4 @@
import { InstanceContext, NapCatCore } from '..';
import { InstanceContext, NapCatCore } from '@/core';
export class NTQQCollectionApi {
context: InstanceContext;

View File

@@ -5,13 +5,14 @@ import {
IMAGE_HTTP_HOST_NT,
Peer,
PicElement,
PicSubType,
PicType,
RawMessage,
SendFileElement,
SendPicElement,
SendPttElement,
SendVideoElement,
} from '@/core/entities';
} from '@/core/types';
import path from 'path';
import fs from 'fs';
import fsPromises from 'fs/promises';
@@ -36,7 +37,11 @@ export class NTQQFileApi {
constructor(context: InstanceContext, core: NapCatCore) {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager(['https://llob.linyuchen.net/rkey', 'http://napcat-sign.wumiao.wang:2082/rkey'], this.context.logger);
this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys'
],
this.context.logger
);
}
async copyFile(filePath: string, destPath: string) {
@@ -102,12 +107,12 @@ export class NTQQFileApi {
fileName: fileName || _fileName,
folderId: folderId,
filePath: path,
fileSize: (fileSize).toString(),
fileSize: fileSize.toString(),
},
};
}
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: 0 | 1 = 0,): Promise<SendPicElement> {
async createValidSendPicElement(context: MessageContext, picPath: string, summary: string = '', subType: PicSubType = 0): Promise<SendPicElement> {
const { md5, fileName, path, fileSize } = await this.core.apis.FileApi.uploadFile(picPath, ElementType.PIC, subType);
if (fileSize === 0) {
throw new Error('文件异常大小为0');
@@ -125,7 +130,7 @@ export class NTQQFileApi {
fileName: fileName,
sourcePath: path,
original: true,
picType: isGIF(picPath) ? PicType.gif : PicType.jpg,
picType: isGIF(picPath) ? PicType.NEWPIC_GIF : PicType.NEWPIC_JPEG,
picSubType: subType,
fileUuid: '',
fileSubId: '',
@@ -138,7 +143,8 @@ export class NTQQFileApi {
async createValidSendVideoElement(context: MessageContext, filePath: string, fileName: string = '', diyThumbPath: string = ''): Promise<SendVideoElement> {
const logger = this.core.context.logger;
let videoInfo = {
width: 1920, height: 1080,
width: 1920,
height: 1080,
time: 15,
format: 'mp4',
size: 0,
@@ -300,18 +306,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
case ElementType.PIC:
case ElementType.PIC:
element.picElement!.sourcePath = elementResults[elementIndex];
break;
case ElementType.VIDEO:
break;
case ElementType.VIDEO:
element.videoElement!.filePath = elementResults[elementIndex];
break;
case ElementType.PTT:
break;
case ElementType.PTT:
element.pttElement!.filePath = elementResults[elementIndex];
break;
case ElementType.FILE:
break;
case ElementType.FILE:
element.fileElement!.filePath = elementResults[elementIndex];
break;
break;
}
elementIndex++;
}
@@ -320,7 +326,7 @@ export class NTQQFileApi {
}
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)) {
if (force) {
try {
@@ -410,27 +416,29 @@ export class NTQQFileApi {
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 parsedUrl = new URL(IMAGE_HTTP_HOST + url);
const imageAppid = parsedUrl.searchParams.get('appid');
const isNTV2 = imageAppid && ['1406', '1407'].includes(imageAppid);
const imageFileId = parsedUrl.searchParams.get('fileid');
if (url && isNTV2 && imageFileId) {
const rkeyData = await this.getRkeyData();
return this.getImageUrlFromParsedUrl(parsedUrl, rkeyData);
return this.getImageUrlFromParsedUrl(imageFileId, imageAppid, rkeyData);
}
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;
@@ -447,7 +455,7 @@ export class NTQQFileApi {
} catch (error: any) {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败', error.message);
}
if (!rkeyData.online_rkey) {
try {
const tempRkeyData = await this.rkeyManager.getRkey();
@@ -458,34 +466,23 @@ export class NTQQFileApi {
this.context.logger.logError.bind(this.context.logger)('获取rkey失败 Fallback Old Mode', e);
}
}
return rkeyData;
}
private getImageUrlFromParsedUrl(parsedUrl: URL, rkeyData: any): string {
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');
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 + `/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}`;
private getImageUrlFromParsedUrl(imageFileId: string, appid: string, rkeyData: any): string {
const rkey = appid === '1406' ? rkeyData.private_rkey : rkeyData.group_rkey;
if (rkeyData.online_rkey) {
return IMAGE_HTTP_HOST_NT + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
}
return '';
return IMAGE_HTTP_HOST + `/download?appid=${appid}&fileid=${imageFileId}&rkey=${rkey}`;
}
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获取失败', { fileMd5, md5HexStr });
return '';
}

View File

@@ -1,4 +1,4 @@
import { FriendV2 } from '@/core/entities';
import { FriendV2 } from '@/core/types';
import { BuddyListReqType, InstanceContext, NapCatCore } from '@/core';
import { LimitedHashTable } from '@/common/message-unique';
@@ -24,7 +24,7 @@ export class NTQQFriendApi {
);
}
async getBuddyV2(refresh = false): Promise<FriendV2[]> {
async getBuddy(refresh = false): Promise<FriendV2[]> {
return Array.from((await this.getBuddyV2SimpleInfoMap(refresh)).values());
}
@@ -58,7 +58,7 @@ export class NTQQFriendApi {
categoryName: category.categroyName,
categoryMbCount: category.categroyMbCount,
onlineCount: category.onlineCount,
buddyList: category.buddyUids.map(uid => data.get(uid)!).filter(value => value),
buddyList: category.buddyUids.map(uid => data.get(uid)).filter(value => !!value),
}));
}

View File

@@ -2,8 +2,8 @@ import {
GeneralCallResult,
Group,
GroupMember,
GroupMemberRole,
GroupRequestOperateTypes,
NTGroupMemberRole,
NTGroupRequestOperateTypes,
InstanceContext,
KickMemberV2Req,
MemberExtSourceType,
@@ -147,14 +147,11 @@ export class NTQQGroupApi {
if (!members) {
try {
members = await this.getGroupMembers(groupCodeStr);
// 更新群成员列表
this.groupMemberCache.set(groupCodeStr, members);
} catch (e) {
return null;
}
}
// log('getGroupMember', members);
function getMember() {
let member: GroupMember | undefined;
if (isNumeric(memberUinOrUidStr)) {
@@ -367,7 +364,6 @@ export class NTQQGroupApi {
}
}
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,
@@ -421,7 +417,7 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().uploadGroupBulletinPic(GroupCode, _Pskey, imageurl);
}
async handleGroupRequest(flag: string, operateType: GroupRequestOperateTypes, reason?: string) {
async handleGroupRequest(flag: string, operateType: NTGroupRequestOperateTypes, reason?: string) {
const flagitem = flag.split('|');
const groupCode = flagitem[0];
const seq = flagitem[1];
@@ -430,7 +426,7 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().operateSysNotify(
false,
{
operateType: operateType, // 2 拒绝
operateType: operateType,
targetMsg: {
seq: seq, // 通知序列号
type: type,
@@ -461,7 +457,7 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().modifyMemberCardName(groupQQ, memberUid, cardName);
}
async setMemberRole(groupQQ: string, memberUid: string, role: GroupMemberRole) {
async setMemberRole(groupQQ: string, memberUid: string, role: NTGroupMemberRole) {
return this.context.session.getGroupService().modifyMemberRole(groupQQ, memberUid, role);
}

View File

@@ -1,14 +1,9 @@
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/entities';
import { InstanceContext, NapCatCore } from '@/core';
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types';
import { GroupFileInfoUpdateItem, 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
context: InstanceContext;
core: NapCatCore;
@@ -17,7 +12,10 @@ export class NTQQMsgApi {
this.context = context;
this.core = core;
}
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
}
async getAioFirstViewLatestMsgs(peer: Peer, MsgCount: number) {
return this.context.session.getMsgService().getAioFirstViewLatestMsgs(peer, MsgCount);
}
@@ -25,9 +23,11 @@ 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);
@@ -111,7 +111,7 @@ export class NTQQMsgApi {
pageLimit: 1,
});
}
// 客户端还在用别慌
// 客户端还在用别慌
async getMsgsBySeqAndCount(peer: Peer, seq: string, count: number, desc: boolean, isReverseOrder: boolean) {
return await this.context.session.getMsgService().getMsgsBySeqAndCount(peer, seq, count, desc, isReverseOrder);
}
@@ -136,19 +136,29 @@ export class NTQQMsgApi {
}
async getGroupFileList(GroupCode: string, params: GetFileListParam) {
const [, groupFileListResult] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelRichMediaService/getGroupFileList',
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
[
GroupCode,
params,
],
() => true,
() => true, // 应当通过 groupFileListResult 判断
1,
5000,
);
return groupFileListResult.item;
const item: GroupFileInfoUpdateItem[] = [];
let index = params.startIndex;
while (true) {
params.startIndex = index;
const [, groupFileListResult] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelRichMediaService/getGroupFileList',
'NodeIKernelMsgListener/onGroupFileInfoUpdate',
[
GroupCode,
params,
],
() => true,
() => true, // 应当通过 groupFileListResult 判断
1,
5000,
);
if (!groupFileListResult?.item?.length) break;
item.push(...groupFileListResult.item);
if (groupFileListResult.isEnd) break;
if (item.length === params.fileCount) break;
index = groupFileListResult.nextIndex;
}
return item;
}
async getMsgHistory(peer: Peer, msgId: string, count: number, isReverseOrder: boolean = false) {

View File

@@ -1,4 +1,4 @@
import { ModifyProfileParams, User, UserDetailSource } from '@/core/entities';
import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';

View File

@@ -8,6 +8,9 @@ import {
WebHonorType,
} from '@/core';
import { NapCatCore } from '..';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
export class NTQQWebApi {
context: InstanceContext;
@@ -303,4 +306,110 @@ export class NTQQWebApi {
}
return (hash & 0x7FFFFFFF).toString();
}
async createQunAlbumSession(gc: string, sAlbumID: string, sAlbumName: string, path: string, skey: string, pskey: string, uin: string) {
const img = readFileSync(path);
const img_md5 = createHash('md5').update(img).digest('hex');
const img_size = img.length;
const img_name = basename(path);
const time = Math.floor(Date.now() / 1000);
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const body = {
control_req: [{
uin: uin,
token: {
type: 4,
data: pskey,
appid: 5
},
appid: "qun",
checksum: img_md5,
check_type: 0,
file_len: img_size,
env: {
refer: "qzone",
deviceInfo: "h5"
},
model: 0,
biz_req: {
sPicTitle: img_name,
sPicDesc: "",
sAlbumName: sAlbumName,
sAlbumID: sAlbumID,
iAlbumTypeID: 0,
iBitmap: 0,
iUploadType: 0,
iUpPicType: 0,
iBatchID: time,
sPicPath: "",
iPicWidth: 0,
iPicHight: 0,
iWaterType: 0,
iDistinctUse: 0,
iNeedFeeds: 1,
iUploadTime: time,
mapExt: {
appid: "qun",
userid: gc
}
},
session: "",
asy_upload: 0,
cmd: "FileUpload"
}]
};
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileBatchControl/${img_md5}?g_tk=${GTK}`;
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
"Cookie": cookie,
"Content-Type": "application/json"
});
return post;
}
async uploadQunAlbumSlice(path: string, session: string, skey: string, pskey: string, uin: string, slice_size: number) {
const img_size = statSync(path).size;
const img_name = basename(path);
let seq = 0;
let offset = 0;
const GTK = this.getBknFromSKey(pskey);
const cookie = `p_uin=${uin}; p_skey=${pskey}; skey=${skey}; uin=${uin}`;
const stream = createReadStream(path, { highWaterMark: slice_size });
for await (const chunk of stream) {
const end = Math.min(offset + chunk.length, img_size);
const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`;
const formData = await RequestUtil.createFormData(boundary, path);
const api = `https://h5.qzone.qq.com/webapp/json/sliceUpload/FileUpload?seq=${seq}&retry=0&offset=${offset}&end=${end}&total=${img_size}&type=form&g_tk=${GTK}`;
const body = {
uin: uin,
appid: "qun",
session: session,
offset: offset,
data: formData,
checksum: "",
check_type: 0,
retry: 0,
seq: seq,
end: end,
cmd: "FileUpload",
slice_size: slice_size,
"biz_req.iUploadType": 0
};
const post = await RequestUtil.HttpGetJson(api, 'POST', body, {
"Cookie": cookie,
"Content-Type": `multipart/form-data; boundary=${boundary}`
});
offset += chunk.length;
seq++;
}
}
async uploadQunAlbum(path: string, albumId: string, group: string, skey: string, pskey: string, uin: string) {
const session = (await this.createQunAlbumSession(group, albumId, group, path, skey, pskey, uin) as { data: { session: string } }).data.session;
return await this.uploadQunAlbumSlice(path, session, skey, pskey, uin, 1024 * 1024);
}
}

View File

@@ -1,65 +0,0 @@
import { ChatType } from './msg';
export interface CacheScanResult {
result: number;
size: [ // 单位为字节
string, // 系统总存储空间
string, // 系统可用存储空间
string, // 系统已用存储空间
string, // QQ总大小
string, // 「聊天与文件」大小
string, // 未知
string, // 「缓存数据」大小
string, // 「其他数据」大小
string, // 未知
];
}
export interface ChatCacheList {
pageCount: number;
infos: ChatCacheListItem[];
}
export interface ChatCacheListItem {
chatType: ChatType;
basicChatCacheInfo: ChatCacheListItemBasic;
guildChatCacheInfo: unknown[]; // work: 没用过频道所以不知道这里边的详细内容
}
export interface ChatCacheListItemBasic {
chatSize: string;
chatTime: string;
uid: string;
uin: string;
remarkName: string;
nickName: string;
chatType?: ChatType;
isChecked?: boolean;
}
export enum CacheFileType {
IMAGE = 0,
VIDEO = 1,
AUDIO = 2,
DOCUMENT = 3,
OTHER = 4,
}
export interface CacheFileList {
infos: CacheFileListItem[],
}
export interface CacheFileListItem {
fileSize: string;
fileTime: string;
fileKey: string;
elementId: string;
elementIdStr: string;
fileType: CacheFileType;
path: string;
fileName: string;
senderId: string;
previewPath: string;
senderName: string;
isChecked?: boolean;
}

View File

@@ -1,867 +0,0 @@
import { GroupMemberRole } from '@/core';
export interface Peer {
chatType: ChatType;
peerUid: string; // 如果是群聊uid为群号私聊uid就是加密的字符串
guildId?: string;
}
export interface KickedOffLineInfo {
appId: number;
instanceId: number;
sameDevice: boolean;
tipsDesc: string;
tipsTitle: string;
kickedType: number;
securityKickedType: number;
}
export interface GetFileListParam {
sortType: number;
fileCount: number;
startIndex: number;
sortOrder: number;
showOnlinedocFolder: number;
folderId?: string;
}
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
VIDEO = 5,
FACE = 6,
REPLY = 7,
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
WALLET = 9,
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 RecommendedMsgElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
export interface InlineKeyboardButton {
id: string;
label: string;
visitedLabel: string;
unsupportTips: string;
data: string;
specifyRoleIds: string[];
specifyTinyids: string[];
style: number;
type: number;
clickLimit: number;
atBotShowChannelList: boolean;
permissionType: number;
}
export interface InlineKeyboardRow {
buttons: InlineKeyboardButton[];
}
export interface TofuElementContent {
color: string;
tittle: string;
}
export interface TaskTopMsgElement {
msgTitle: string;
msgSummary: string;
iconUrl: string;
topMsgType: number;
}
export enum NTMsgType {
KMSGTYPEARKSTRUCT = 11,
KMSGTYPEFACEBUBBLE = 24,
KMSGTYPEFILE = 3,
KMSGTYPEGIFT = 14,
KMSGTYPEGIPHY = 13,
KMSGTYPEGRAYTIPS = 5,
KMSGTYPEMIX = 2,
KMSGTYPEMULTIMSGFORWARD = 8,
KMSGTYPENULL = 1,
KMSGTYPEONLINEFILE = 21,
KMSGTYPEONLINEFOLDER = 27,
KMSGTYPEPROLOGUE = 29,
KMSGTYPEPTT = 6,
KMSGTYPEREPLY = 9,
KMSGTYPESHARELOCATION = 25,
KMSGTYPESTRUCT = 4,
KMSGTYPESTRUCTLONGMSG = 12,
KMSGTYPETEXTGIFT = 15,
KMSGTYPEUNKNOWN = 0,
KMSGTYPEVIDEO = 7,
KMSGTYPEWALLET = 10
}
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
export interface TofuRecordElement {
type: number;
busiid: string;
busiuuid: string;
descriptionContent: string;
contentlist: TofuElementContent[],
background: string;
icon: string;
uinlist: string[],
uidlist: string[],
busiExtra: string;
updateTime: string;
dependedmsgid: string;
msgtime: string;
onscreennotify: boolean;
}
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
export interface FaceBubbleElement {
faceCount: number;
faceSummary: string;
faceFlag: number;
content: string;
oldVersionStr: string;
faceType: number;
others: string;
yellowFaceInfo: {
index: number;
buf: string;
compatibleText: string;
text: string;
};
}
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
export interface AvRecordElement {
type: number;
time: string;
text: string;
mainType: number;
hasRead: boolean;
extraType: number;
}
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
export interface YoloUserInfo {
uid: string;
result: number;
rank: number;
bizId: string;
}
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
export interface YoloGameResultElement {
UserInfo: YoloUserInfo[];
}
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
export interface GiphyElement {
id: string;
isClip: boolean;
width: number;
height: number;
}
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
export interface CalendarElement {
summary: string;
msg: string;
expireTimeMs: string;
schemaType: number;
schema: string;
}
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
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,
jpg = 1000
}
export enum PicSubType {
normal = 0, // 普通图片,大图
face = 1 // 表情包小图
}
export enum NTMsgAtType {
ATTYPEALL = 1,
ATTYPECATEGORY = 512,
ATTYPECHANNEL = 16,
ATTYPEME = 4,
ATTYPEONE = 2,
ATTYPEONLINE = 64,
ATTYPEROLE = 8,
ATTYPESUMMON = 32,
ATTYPESUMMONONLINE = 128,
ATTYPESUMMONROLE = 256,
ATTYPEUNKNOWN = 0
}
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
export interface ReplyElement {
sourceMsgIdInRecords?: string;
replayMsgSeq: string;
replayMsgId: string;
senderUin: string;
senderUidStr?: string;
replyMsgTime?: string;
replyMsgClientSeq?: string;
}
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
export interface StructLongMsgElement {
xmlContent: string;
resId: string;
}
export type SendActionBarElement = SendElementBase<ElementType.ACTIONBAR> & ElementBase<'actionBarElement'>;
export interface ShareLocationElement {
text: string;
ext: string;
}
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export interface FileElement {
fileMd5?: string;
fileName: string;
filePath: string;
fileSize: string;
picHeight?: number;
picWidth?: number;
folderId?: string;
picThumbPath?: Map<number, string>;
file10MMd5?: string;
fileSha?: string;
fileSha3?: string;
fileUuid?: string;
fileSubId?: string;
thumbFileSize?: number;
fileBizId?: number;
}
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;
export interface TextElement {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
}
export interface MessageElement {
elementType: ElementType,
elementId: string,
extBufForUI?: string, //"0x",
textElement?: TextElement;
faceElement?: FaceElement,
marketFaceElement?: MarketFaceElement,
replyElement?: ReplyElement,
picElement?: PicElement,
pttElement?: PttElement,
videoElement?: VideoElement,
grayTipElement?: GrayTipElement,
arkElement?: ArkElement,
fileElement?: FileElement,
liveGiftElement?: null,
markdownElement?: MarkdownElement,
structLongMsgElement?: StructLongMsgElement,
multiForwardMsgElement?: MultiForwardMsgElement,
giphyElement?: GiphyElement,
walletElement?: null,
inlineKeyboardElement?: InlineKeyboardElement,
textGiftElement?: null,//????
calendarElement?: CalendarElement,
yoloGameResultElement?: YoloGameResultElement,
avRecordElement?: AvRecordElement,
structMsgElement?: null,
faceBubbleElement?: FaceBubbleElement,
shareLocationElement?: ShareLocationElement,
tofuRecordElement?: TofuRecordElement,
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement
}
export enum AtType {
notAt = 0,
atAll = 1,
atUser = 2
}
export enum MsgSourceType {
K_DOWN_SOURCETYPE_AIOINNER = 1,
K_DOWN_SOURCETYPE_BIGSCREEN = 2,
K_DOWN_SOURCETYPE_HISTORY = 3,
K_DOWN_SOURCETYPE_UNKNOWN = 0
}
// 来自Android分析
export enum ChatType {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
export interface PttElement {
canConvert2Text: boolean;
duration: number; // 秒数
fileBizId: null;
fileId: number; // 0
fileName: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
filePath: string; // "/Users//Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_a6b15c9820595d25a56c1633ce19ad40/nt_data/Ptt/2023-11/Ori/e4d09c784d5a2abcb2f9980bdc7acfe6.amr"
fileSize: string; // "4261"
fileSubId: string; // "0"
fileUuid: string; // "90j3z7rmRphDPrdVgP9udFBaYar#oK0TWZIV"
formatType: number; // 1
invalidState: number; // 0
md5HexStr: string; // "e4d09c784d5a2abcb2f9980bdc7acfe6"
playState: number; // 0
progress: number; // 0
text: string; // ""
transferStatus: number; // 0
translateStatus: number; // 0
voiceChangeType: number; // 0
voiceType: number; // 0
waveAmplitudes: number[];
autoConvertText: number;
}
export interface ArkElement {
bytesData: string;
linkInfo: null;
subElementType: null;
}
export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn';
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn';
export interface PicElement {
md5HexStr?: string;
filePath?: string;
fileSize: number | string;//number
picWidth: number;
picHeight: number;
fileName: string;
sourcePath: string;
original: boolean;
picType: PicType;
picSubType?: PicSubType;
fileUuid: string;
fileSubId: string;
thumbFileSize: number;
summary: string;
thumbPath: Map<number, string>;
originImageMd5?: string;
originImageUrl?: string; // http url, 没有hosthost是https://gchat.qpic.cn/, 带download参数的是https://multimedia.nt.qq.com.cn
}
export enum NTGrayTipElementSubTypeV2 {
GRAYTIP_ELEMENT_SUBTYPE_AIOOP = 15,
GRAYTIP_ELEMENT_SUBTYPE_BLOCK = 14,
GRAYTIP_ELEMENT_SUBTYPE_BUDDY = 5,
GRAYTIP_ELEMENT_SUBTYPE_BUDDYNOTIFY = 9,
GRAYTIP_ELEMENT_SUBTYPE_EMOJIREPLY = 3,
GRAYTIP_ELEMENT_SUBTYPE_ESSENCE = 7,
GRAYTIP_ELEMENT_SUBTYPE_FEED = 6,
GRAYTIP_ELEMENT_SUBTYPE_FEEDCHANNELMSG = 11,
GRAYTIP_ELEMENT_SUBTYPE_FILE = 10,
GRAYTIP_ELEMENT_SUBTYPE_GROUP = 4,
GRAYTIP_ELEMENT_SUBTYPE_GROUPNOTIFY = 8,
GRAYTIP_ELEMENT_SUBTYPE_JSON = 17,
GRAYTIP_ELEMENT_SUBTYPE_LOCALMSG = 13,
GRAYTIP_ELEMENT_SUBTYPE_PROCLAMATION = 2,
GRAYTIP_ELEMENT_SUBTYPE_REVOKE = 1,
GRAYTIP_ELEMENT_SUBTYPE_UNKNOWN = 0,
GRAYTIP_ELEMENT_SUBTYPE_WALLET = 16,
GRAYTIP_ELEMENT_SUBTYPE_XMLMSG = 12,
}
export interface GrayTipElement {
subElementType: NTGrayTipElementSubTypeV2;
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
};
aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement;
xmlElement: {
content: string;
templId: string;
};
jsonGrayTipElement: {
busiId?: number;
jsonStr: string;
};
}
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸, 从faceIndex 222开始
dice = 3, // 骰子
poke = 5 // 拍一拍
}
export enum FaceIndex {
dice = 358,
RPS = 359 // 石头剪刀布
}
export interface FaceElement {
faceIndex: number;
faceType: FaceType;
faceText?: string;
packId?: string;
stickerId?: string;
sourceType?: number;
stickerType?: number;
resultId?: string;
surpriseId?: string;
randomType?: number;
}
export interface MarketFaceElement {
emojiPackageId: number;
faceName: string;
emojiId: string;
key: string;
}
export interface VideoElement {
filePath: string;
fileName: string;
videoMd5?: string;
thumbMd5?: string;
fileTime?: number; // second
thumbSize?: number; // byte
fileFormat?: viedo_type; // 2表示mp4 参考下面条目
fileSize?: string; // byte
thumbWidth?: number;
thumbHeight?: number;
busiType?: 0; //
subBusiType?: 0; // 未知
thumbPath?: Map<number, any>;
transferStatus?: 0; // 未知
progress?: 0; // 下载进度?
invalidState?: 0; // 未知
fileUuid?: string; // 可以用于下载链接?
fileSubId?: string;
fileBizId?: null;
originVideoMd5?: string;
import_rich_media_context?: null;
sourceVideoCodecFormat?: number;
}
// export enum busiType{
// public static final int CREATOR_SHARE_ADV_XWORLD = 21;
// public static final int MINI_APP_MINI_GAME = 11;
// public static final int OFFICIAL_ACCOUNT_ADV = 4;
// public static final int OFFICIAL_ACCOUNT_ADV_GAME = 8;
// public static final int OFFICIAL_ACCOUNT_ADV_SHOP = 9;
// public static final int OFFICIAL_ACCOUNT_ADV_VIP = 7;
// public static final int OFFICIAL_ACCOUNT_LAYER_MASK_ADV = 14;
// public static final int OFFICIAL_ACCOUNT_SPORT = 13;
// public static final int OFFICIAL_ACCOUNT_TIAN_QI = 10;
// public static final int PC_QQTAB_ADV = 18;
// public static final int QIQIAOBAN_SDK = 15;
// public static final int QQ_CPS = 16;
// public static final int QQ_WALLET_CPS = 17;
// public static final int QZONE_FEEDS = 0;
// public static final int QZONE_PHOTO_TAIL = 2;
// public static final int QZONE_VIDEO_LAYER = 1;
// public static final int REWARD_GIFT_ADV = 6;
// public static final int REWARD_GROUPGIFT_ADV = 12;
// public static final int REWARD_PERSONAL_ADV = 5;
// public static final int WEISEE_OFFICIAL_ACCOUNT = 3;
// public static final int X_WORLD_CREATOR_ADV = 20;
// public static final int X_WORLD_QZONE_LAYER = 22;
// public static final int X_WORLD_VIDEO_ADV = 19;
// }
// export enum CategoryBusiType {
// _KCateBusiTypeDefault = 0,
// _kCateBusiTypeFaceCluster = 1,
// _kCateBusiTypeLabelCluster = 4,
// _kCateBusiTypeMonthCluster = 16,
// _kCateBusiTypePoiCluster = 2,
// _kCateBusiTypeYearCluster = 8,
// }
export enum viedo_type {
VIDEO_FORMAT_AFS = 7,
VIDEO_FORMAT_AVI = 1,
VIDEO_FORMAT_MKV = 4,
VIDEO_FORMAT_MOD = 9,
VIDEO_FORMAT_MOV = 8,
VIDEO_FORMAT_MP4 = 2,
VIDEO_FORMAT_MTS = 11,
VIDEO_FORMAT_RM = 6,
VIDEO_FORMAT_RMVB = 5,
VIDEO_FORMAT_TS = 10,
VIDEO_FORMAT_WMV = 3,
}
export interface MarkdownElement {
content: string;
}
export interface InlineKeyboardElementRowButton {
id: string;
label: string;
visitedLabel: string;
style: 1; // 未知
type: 2; // 未知
clickLimit: 0; // 未知
unsupportTips: string;
data: string;
atBotShowChannelList: boolean;
permissionType: number;
specifyRoleIds: [];
specifyTinyids: [];
isReply: false;
anchor: 0;
enter: false;
subscribeDataTemplateIds: [];
}
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}],
botAppid: string;
}
export interface TipAioOpGrayTipElement { // 这是什么提示来着?
operateType: number;
peerUid: string;
fromGrpCodeOfTmpChat: string;
}
export enum TipGroupElementType {
memberIncrease = 1,
kicked = 3, // 被移出群
ban = 8
}
// public final class MemberAddShowType {
// public static final int KOTHERADD = 0;
// public static final int KOTHERADDBYOTHERQRCODE = 2;
// public static final int KOTHERADDBYYOURQRCODE = 3;
// public static final int KOTHERINVITEOTHER = 5;
// public static final int KOTHERINVITEYOU = 6;
// public static final int KYOUADD = 1;
// public static final int KYOUADDBYOTHERQRCODE = 4;
// public static final int KYOUALREADYMEMBER = 8;
// public static final int KYOUINVITEOTHER = 7;
// }
export interface TipGroupElement {
type: TipGroupElementType; // 1是表示有人加入群; 自己加入群也会收到这个
role: 0; // 暂时不知
groupName: string; // 暂时获取不到
memberUid: string;
memberNick: string;
memberRemark: string;
adminUid: string;
adminNick: string;
adminRemark: string;
createGroup: null;
memberAdd?: {
showType: 1;
otherAdd: null;
otherAddByOtherQRCode: null;
otherAddByYourQRCode: null;
youAddByOtherQRCode: null;
otherInviteOther: null;
otherInviteYou: null;
youInviteOther: null
};
shutUp?: {
curTime: string;
duration: string; // 禁言时间,秒
admin: {
uid: string;
card: string;
name: string;
role: GroupMemberRole
};
member: {
uid: string
card: string;
name: string;
role: GroupMemberRole
}
};
}
export interface MultiForwardMsgElement {
xmlContent: string; // xml格式的消息内容
resId: string;
fileName: string;
}
export enum SendStatusType {
KSEND_STATUS_FAILED = 0,
KSEND_STATUS_SENDING = 1,
KSEND_STATUS_SUCCESS = 2,
KSEND_STATUS_SUCCESS_NOSEQ = 3
}
export interface RawMessage {
parentMsgPeer: Peer;
parentMsgIdList: string[];
/**
* 扩展字段,与 Ob11 msg ID 有关
*/
id?: number;
guildId: string;
msgRandom: string;
msgId: string;
/**
* 消息时间戳(秒)
*/
msgTime: string;
msgSeq: string;
msgType: NTMsgType;
subMsgType: number;
senderUid: string;
/**
* 发送者 QQ 号
*/
senderUin: string;
/**
* 群号 / 用户 UID
*/
peerUid: string;
/**
* 群号 / 用户 QQ 号
*/
peerUin: string;
/**
* 好友备注(如果是好友消息)
*/
remark?: string;
/**
* 群名(如果是群消息)
*/
peerName: string;
/**
* 发送者昵称(如果是好友消息)
*/
sendNickName: string;
/**
* 发送者好友备注(如果是群消息并且有发送者好友)
*/
sendRemarkName: string;
/**
* 发送者群名片(如果是群消息)
*/
sendMemberName?: string;
chatType: ChatType;
/**
* 消息状态,别人发的 2 是已撤回,自己发的 2 是已发送
*/
sendStatus?: SendStatusType;
/**
* 撤回时间,"0" 是没有撤回
*/
recallTime: string;
records: RawMessage[];
elements: MessageElement[];
sourceType: MsgSourceType;
isOnlineMsg: boolean;
}
export interface QueryMsgsParams {
chatInfo: Peer;
filterMsgType: [];
filterSendersUid: string[];
filterMsgFromTime: string;
filterMsgToTime: string;
pageLimit: number;
isReverseOrder: boolean;
isIncludeCurrent: boolean;
}
export interface TmpChatInfoApi {
errMsg: string;
result: number;
tmpChatInfo?: TmpChatInfo;
}
export interface TmpChatInfo {
chatType: number;
fromNick: string;
groupCode: string;
peerUid: string;
sessionType: number;
sig: string;
}
export interface MsgReqType {
peer: Peer,
byType: number,
msgId: string,
msgSeq: string,
msgTime: string,
clientSeq: string,
cnt: number,
queryOrder: boolean,
includeSelf: boolean,
includeDeleteMsg: boolean,
extraCnt: number
}
//getMsgsIncludeSelf Peer必须 byType 1
//getMsgsWithMsgTimeAndClientSeqForC2C Peer必须 byType 3

View File

@@ -1,5 +1,5 @@
{
"fileLog": true,
"fileLog": false,
"consoleLog": true,
"fileLogLevel": "debug",
"consoleLogLevel": "info",

View File

@@ -1,4 +1,4 @@
// work:further refactor in NapCat.Packet v2
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const LikeDetail = {

View File

@@ -1,4 +1,4 @@
// work:further refactor in NapCat.Packet v2
// TODO: further refactor in NapCat.Packet v2
import { NapProtoMsg, ProtoField, ScalarType } from "@napneko/nap-proto-core";
const BodyInner = {

View File

@@ -24,14 +24,14 @@ import path from 'node:path';
import fs from 'node:fs';
import { hostname, systemName, systemVersion } from '@/common/system';
import { NTEventWrapper } from '@/common/event';
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/entities';
import { DataSource, GroupMember, KickedOffLineInfo, SelfInfo, SelfStatusInfo } from '@/core/types';
import { NapCatConfigLoader } from '@/core/helper/config';
import os from 'node:os';
import { NodeIKernelGroupListener, NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
export * from './wrapper';
export * from './entities';
export * from './types';
export * from './services';
export * from './listeners';
@@ -120,7 +120,7 @@ export class NapCatCore {
if (!fs.existsSync(this.NapCatTempPath)) {
fs.mkdirSync(this.NapCatTempPath, { recursive: true });
}
//遍历this.apis[i].initApi 如果存在该函数进行async 调用
//遍历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') {
@@ -210,7 +210,7 @@ export class NapCatCore {
});
};
groupListener.onMemberListChange = (arg) => {
// work:应该加一个内部自己维护的成员变动callback用于判断成员变化通知
// TODO: 应该加一个内部自己维护的成员变动callback用于判断成员变化通知
const groupCode = arg.sceneId.split('_')[0];
if (this.apis.GroupApi.groupMemberCache.has(groupCode)) {
const existMembers = this.apis.GroupApi.groupMemberCache.get(groupCode)!;

View File

@@ -1,4 +1,4 @@
import { BuddyCategoryType, FriendRequestNotify } from '@/core/entities';
import { BuddyCategoryType, FriendRequestNotify } from '@/core/types';
export type OnBuddyChangeParams = BuddyCategoryType[];

View File

@@ -1,4 +1,4 @@
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/entities';
import { DataSource, Group, GroupListUpdateType, GroupMember, GroupNotify, ShutUpGroupMember } from '@/core/types';
export class NodeIKernelGroupListener {
onGroupListInited(listEmpty: boolean): any { }

View File

@@ -1,4 +1,4 @@
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/entities';
import { ChatType, KickedOffLineInfo, RawMessage } from '@/core/types';
import { CommonFileInfo } from '@/core';
export interface OnRichMediaDownloadCompleteParams {
@@ -29,50 +29,10 @@ export interface GroupFileInfoUpdateParamType {
retMsg: string;
clientWording: string;
isEnd: boolean;
item: Array<{
peerId: string;
type: number;
folderInfo?: {
folderId: string;
parentFolderId: string;
folderName: string;
createTime: number;
modifyTime: number;
createUin: string;
creatorName: string;
totalFileCount: number;
modifyUin: string;
modifyName: string;
usedSpace: string;
},
fileInfo?: {
fileModelId: string;
fileId: string;
fileName: string;
fileSize: string;
busId: number;
uploadedSize: string;
uploadTime: number;
deadTime: number;
modifyTime: number;
downloadTimes: number;
sha: string;
sha3: string;
md5: string;
uploaderLocalPath: string;
uploaderName: string;
uploaderUin: string;
parentFolderId: string;
localPath: string;
transStatus: number;
transType: number;
elementId: string;
isFolder: boolean;
},
}>;
allFileCount: string;
nextIndex: string;
reqId: string;
item: Array<GroupFileInfoUpdateItem>;
allFileCount: number;
nextIndex: number;
reqId: number;
}
// {
@@ -83,6 +43,49 @@ export interface GroupFileInfoUpdateParamType {
// fromNick: '拾xxxx,
// sig: '0x'
// }
export interface GroupFileInfoUpdateItem {
peerId: string;
type: number;
folderInfo?: {
folderId: string;
parentFolderId: string;
folderName: string;
createTime: number;
modifyTime: number;
createUin: string;
creatorName: string;
totalFileCount: number;
modifyUin: string;
modifyName: string;
usedSpace: string;
},
fileInfo?: {
fileModelId: string;
fileId: string;
fileName: string;
fileSize: string;
busId: number;
uploadedSize: string;
uploadTime: number;
deadTime: number;
modifyTime: number;
downloadTimes: number;
sha: string;
sha3: string;
md5: string;
uploaderLocalPath: string;
uploaderName: string;
uploaderUin: string;
parentFolderId: string;
localPath: string;
transStatus: number;
transType: number;
elementId: string;
isFolder: boolean;
},
}
export interface TempOnRecvParams {
sessionType: number,//1
chatType: ChatType,//100

View File

@@ -1,4 +1,4 @@
import { User, UserDetailInfoListenerArg } from '@/core/entities';
import { User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {

View File

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

View File

@@ -1,7 +1,7 @@
import { LogLevel, LogWrapper } from "@/common/log";
import { PacketContext } from "@/core/packet/context/packetContext";
// work: check bind?
// TODO: check bind?
export class PacketLogger {
private readonly napLogger: LogWrapper;

View File

@@ -78,8 +78,12 @@ export class PacketHighwayContext {
ip: int32ip2str(addr.ip),
port: addr.port
});
this.hwClient.changeServer(int32ip2str(addr.ip), addr.port);
}
}
if (this.sig.serverAddr.length === 0) {
this.logger.warn('[Highway PrepareUpload] server addr is empty!');
}
}
async uploadImage(peer: Peer, img: PacketMsgPicElement): Promise<void> {

View File

@@ -76,7 +76,7 @@ export type rawMsgWithSendMsg = {
msg: PacketSendMsgElement[]
}
// work:make it become adapter?
// TODO: make it become adapter?
export class PacketMsgConverter {
private isValidElementType(type: ElementType): type is keyof ElementToPacketMsgConverters {
return SupportedElementTypes.includes(type);
@@ -116,7 +116,7 @@ export class PacketMsgConverter {
[ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement);
},
// work:check this logic, move it in arkElement?
// TODO: check this logic, move it in arkElement?
[ElementType.STRUCTLONGMSG]: (element) => {
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
}

View File

@@ -14,7 +14,7 @@ import {
GroupFileExtra
} from "@/core/packet/transformer/proto";
import {
AtType,
NTMsgAtType,
PicType,
SendArkElement,
SendFaceElement,
@@ -32,7 +32,7 @@ import { ForwardMsgBuilder } from "@/common/forward-msg-builder";
import { PacketMsg, PacketSendMsgElement } from "@/core/packet/message/message";
// raw <-> packet
// work:SendStructLongMsgElement
// TODO: SendStructLongMsgElement
export abstract class IPacketMsgElement<T extends PacketSendMsgElement> {
protected constructor(rawElement: T) {
}
@@ -82,7 +82,7 @@ export class PacketMsgAtElement extends PacketMsgTextElement {
constructor(element: SendTextElement) {
super(element);
this.targetUid = element.textElement.atNtUid;
this.atAll = element.textElement.atType === AtType.atAll;
this.atAll = element.textElement.atType === NTMsgAtType.ATTYPEALL;
}
buildElement(): NapProtoEncodeStructType<typeof Elem>[] {
@@ -118,7 +118,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
this.targetUin = +(element.replyElement.senderUin ?? 0);
this.targetUid = element.replyElement.senderUidStr ?? '';
this.time = +(element.replyElement.replyMsgTime ?? 0);
this.elems = []; // work:in replyElement.sourceMsgTextElems
this.elems = []; // TODO: in replyElement.sourceMsgTextElems
}
get isGroupReply(): boolean {
@@ -131,7 +131,7 @@ export class PacketMsgReplyElement extends IPacketMsgElement<SendReplyElement> {
origSeqs: [this.isGroupReply ? this.messageClientSeq : this.messageSeq],
senderUin: BigInt(this.targetUin),
time: this.time,
elems: [], // work:in replyElement.sourceMsgTextElems
elems: [], // TODO: in replyElement.sourceMsgTextElems
pbReserve: {
messageId: this.messageId,
},
@@ -346,9 +346,9 @@ export class PacketMsgPttElement extends IPacketMsgElement<SendPttElement> {
constructor(element: SendPttElement) {
super(element);
this.filePath = element.pttElement.filePath;
this.fileSize = +element.pttElement.fileSize; // work:cc
this.fileSize = +element.pttElement.fileSize; // TODO: cc
this.fileMd5 = element.pttElement.md5HexStr;
this.fileDuration = Math.round(element.pttElement.duration); // work:cc
this.fileDuration = Math.round(element.pttElement.duration); // TODO: cc
}
get valid(): boolean {

View File

@@ -25,7 +25,7 @@ class DownloadOfflineFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0
return OidbBase.build(0xE37, 800, body, false, false);
}
// work:check
// TODO:check
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.OidbSvcTrpcTcp0XE37Response).decode(oidbBody);

View File

@@ -16,7 +16,7 @@ class FetchSessionKey extends PacketTransformer<typeof proto.HttpConn0x6ff_501Re
field4: 1,
field6: 3,
serviceTypes: [1, 5, 10, 21],
// tgt: "", // work:do we really need tgt? seems not
// tgt: "", // TODO: do we really need tgt? seems not
field9: 2,
field10: 9,
field11: 8,

View File

@@ -16,7 +16,7 @@ class UploadGroupFile extends PacketTransformer<typeof proto.OidbSvcTrpcTcp0x6D6
appId: 4,
busId: 102,
entrance: 6,
targetDirectory: '/', // work:
targetDirectory: '/', // TODO:
fileName: file.fileName,
localDirectory: `/${file.fileName}`,
fileSize: BigInt(file.fileSize),

View File

@@ -40,7 +40,7 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //work:extend NapCat imgType /cc @MliKiowa
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
@@ -59,7 +59,7 @@ class UploadGroupImage extends PacketTransformer<typeof proto.NTV2RichMediaResp>
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // work:
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -40,7 +40,7 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
fileName: img.name,
type: {
type: 1,
picFormat: img.picType, //work:extend NapCat imgType /cc @MliKiowa
picFormat: img.picType, //TODO: extend NapCat imgType /cc @MliKiowa
videoFormat: 0,
voiceFormat: 0,
},
@@ -59,7 +59,7 @@ class UploadPrivateImage extends PacketTransformer<typeof proto.NTV2RichMediaRes
extBizInfo: {
pic: {
bytesPbReserveTroop: Buffer.from("0800180020004200500062009201009a0100a2010c080012001800200028003a00", 'hex'),
textSummary: "Nya~", // work:
textSummary: "Nya~", // TODO:
},
video: {
bytesPbReserve: Buffer.alloc(0),

View File

@@ -1,6 +1,6 @@
import { GeneralCallResult } from '@/core/services/common';
import { NodeIKernelBuddyListener } from '@/core/listeners';
import { BuddyListReqType } from '../entities/user';
import { BuddyListReqType } from '../types/user';
export interface NodeIKernelBuddyService {
getBuddyListV2(callFrom: string, reqType: BuddyListReqType): Promise<GeneralCallResult & {

View File

@@ -4,11 +4,11 @@ import {
GroupExtParam,
GroupInfoSource,
GroupMember,
GroupMemberRole,
NTGroupMemberRole,
GroupNotifyMsgType,
GroupRequestOperateTypes,
NTGroupRequestOperateTypes,
KickMemberV2Req,
} from '@/core/entities';
} from '@/core/types';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelGroupService {
@@ -137,7 +137,7 @@ export interface NodeIKernelGroupService {
kickMember(groupCode: string, memberUids: string[], refuseForever: boolean, kickReason: string): Promise<void>;
modifyMemberRole(groupCode: string, uid: string, role: GroupMemberRole): void;
modifyMemberRole(groupCode: string, uid: string, role: NTGroupMemberRole): void;
modifyMemberCardName(groupCode: string, uid: string, cardName: string): void;
@@ -198,9 +198,9 @@ export interface NodeIKernelGroupService {
operateSysNotify(
doubt: boolean,
operateMsg: {
operateType: GroupRequestOperateTypes, // 2 拒绝
operateType: NTGroupRequestOperateTypes,
targetMsg: {
seq: string, // 通知序列号
seq: string,
type: GroupNotifyMsgType,
groupCode: string,
postscript: string

View File

@@ -1,7 +1,7 @@
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/entities';
import { ElementType, MessageElement, Peer, RawMessage, SendMessageElement } from '@/core/types';
import { NodeIKernelMsgListener } from '@/core/listeners/NodeIKernelMsgListener';
import { GeneralCallResult } from '@/core/services/common';
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '../entities/msg';
import { MsgReqType, QueryMsgsParams, TmpChatInfoApi } from '../types/msg';
export interface NodeIKernelMsgService {

View File

@@ -1,7 +1,7 @@
import { ChatType, Peer } from '../entities';
import { ChatType, Peer } from '@/core/types';
import { NodeIKernelRecentContactListener } from '../listeners/NodeIKernelRecentContactListener';
import { GeneralCallResult } from './common';
import { FSABRecentContactParams } from '../entities/contact';
import { FSABRecentContactParams } from '../types/contact';
export interface NodeIKernelRecentContactService {
setGuildDisplayStatus(...args: unknown[]): unknown; // 2 arguments

View File

@@ -1,4 +1,4 @@
import { GetFileListParam, MessageElement, Peer } from '../entities';
import { GetFileListParam, MessageElement, Peer } from '@/core/types';
import { GeneralCallResult } from './common';
export enum UrlFileDownloadType {

View File

@@ -1,4 +1,4 @@
import { ChatType } from '../entities';
import { ChatType } from '@/core/types';
import { GeneralCallResult } from './common';
export interface NodeIKernelSearchService {

View File

@@ -1,4 +1,4 @@
import { MessageElement, Peer } from '../entities';
import { MessageElement, Peer } from '@/core/types';
export interface NodeIkernelTestPerformanceService {

68
src/core/types/cache.ts Normal file
View File

@@ -0,0 +1,68 @@
import { ChatType } from './msg';
/**
* 聊天缓存列表
*/
export interface ChatCacheList {
pageCount: number; // 页数
infos: ChatCacheListItem[]; // 聊天缓存项列表
}
/**
* 聊天缓存列表项
*/
export interface ChatCacheListItem {
chatType: ChatType; // 聊天类型
basicChatCacheInfo: ChatCacheListItemBasic; // 基本聊天缓存信息
guildChatCacheInfo: unknown[]; // 公会聊天缓存信息
}
/**
* 基本聊天缓存信息
*/
export interface ChatCacheListItemBasic {
chatSize: string; // 聊天大小
chatTime: string; // 聊天时间
uid: string; // 用户ID
uin: string; // 用户号码
remarkName: string; // 备注名
nickName: string; // 昵称
chatType?: ChatType; // 聊天类型(可选)
isChecked?: boolean; // 是否已检查(可选)
}
/**
* 缓存文件类型枚举
*/
export enum CacheFileType {
IMAGE = 0, // 图片
VIDEO = 1, // 视频
AUDIO = 2, // 音频
DOCUMENT = 3, // 文档
OTHER = 4, // 其他
}
/**
* 缓存文件列表
*/
export interface CacheFileList {
infos: CacheFileListItem[]; // 缓存文件项列表
}
/**
* 缓存文件列表项
*/
export interface CacheFileListItem {
fileSize: string; // 文件大小
fileTime: string; // 文件时间
fileKey: string; // 文件键
elementId: string; // 元素ID
elementIdStr: string; // 元素ID字符串
fileType: CacheFileType; // 文件类型
path: string; // 路径
fileName: string; // 文件名
senderId: string; // 发送者ID
previewPath: string; // 预览路径
senderName: string; // 发送者名称
isChecked?: boolean; // 是否已检查(可选)
}

View File

@@ -0,0 +1,2 @@
export const IMAGE_HTTP_HOST = 'https://gchat.qpic.cn';
export const IMAGE_HTTP_HOST_NT = 'https://multimedia.nt.qq.com.cn';

View File

@@ -1,4 +1,3 @@
export interface FSABRecentContactParams {
anchorPointContact: {
contactId: string;

348
src/core/types/element.ts Normal file
View File

@@ -0,0 +1,348 @@
import { ElementType, FaceType, MessageElement, NTGrayTipElementSubTypeV2, PicSubType, PicType, TipAioOpGrayTipElement, TipGroupElement, NTVideoType } from "./msg";
type ElementFullBase = Omit<MessageElement, 'elementType' | 'elementId' | 'extBufForUI'>;
export interface SendElementBase<ET extends ElementType> {
elementType: ET;
elementId: string;
extBufForUI?: string;
}
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 TextElement {
content: string;
atType: number;
atUid: string;
atTinyId: string;
atNtUid: string;
}
export interface FaceElement {
faceIndex: number;
faceType: FaceType;
faceText?: string;
packId?: string;
stickerId?: string;
sourceType?: number;
stickerType?: number;
resultId?: string;
surpriseId?: string;
randomType?: number;
}
export interface GrayTipElement {
subElementType: NTGrayTipElementSubTypeV2;
revokeElement: {
operatorRole: string;
operatorUid: string;
operatorNick: string;
operatorRemark: string;
operatorMemRemark?: string;
wording: string; // 自定义的撤回提示语
};
aioOpGrayTipElement: TipAioOpGrayTipElement;
groupElement: TipGroupElement;
xmlElement: {
content: string;
templId: string;
};
jsonGrayTipElement: {
busiId?: number;
jsonStr: string;
};
}
export interface ArkElement {
bytesData: string;
linkInfo: null;
subElementType: null;
}
export interface MarketFaceElement {
emojiPackageId: number;
faceName: string;
emojiId: string;
key: string;
}
export interface VideoElement {
filePath: string;
fileName: string;
videoMd5?: string;
thumbMd5?: string;
fileTime?: number; // second
thumbSize?: number; // byte
fileFormat?: NTVideoType; // 2表示mp4 参考下面条目
fileSize?: string; // byte
thumbWidth?: number;
thumbHeight?: number;
busiType?: 0; //
subBusiType?: 0; // 未知
thumbPath?: Map<number, any>;
transferStatus?: 0; // 未知
progress?: 0; // 下载进度?
invalidState?: 0; // 未知
fileUuid?: string; // 可以用于下载链接?
fileSubId?: string;
fileBizId?: null;
originVideoMd5?: string;
import_rich_media_context?: null;
sourceVideoCodecFormat?: number;
}
export interface PicElement {
md5HexStr?: string;
filePath?: string;
fileSize: number | string;//number
picWidth: number;
picHeight: number;
fileName: string;
sourcePath: string;
original: boolean;
picType: PicType;
picSubType?: PicSubType;
fileUuid: string;
fileSubId: string;
thumbFileSize: number;
summary: string;
thumbPath: Map<number, string>;
originImageMd5?: string;
originImageUrl?: string;
}
export interface InlineKeyboardButton {
id: string;
label: string;
visitedLabel: string;
unsupportTips: string;
data: string;
specifyRoleIds: string[];
specifyTinyids: string[];
style: number;
type: number;
clickLimit: number;
atBotShowChannelList: boolean;
permissionType: number;
}
// 非element
interface InlineKeyboardRow {
buttons: InlineKeyboardButton[];
}
// 非element
interface TofuElementContent {
color: string;
tittle: string;
}
export interface ActionBarElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface RecommendedMsgElement {
rows: InlineKeyboardRow[];
botAppid: string;
}
export interface TofuRecordElement {
type: number;
busiid: string;
busiuuid: string;
descriptionContent: string;
contentlist: TofuElementContent[],
background: string;
icon: string;
uinlist: string[],
uidlist: string[],
busiExtra: string;
updateTime: string;
dependedmsgid: string;
msgtime: string;
onscreennotify: boolean;
}
export interface FileElement {
fileMd5?: string;
fileName: string;
filePath: string;
fileSize: string;
picHeight?: number;
picWidth?: number;
folderId?: string;
picThumbPath?: Map<number, string>;
file10MMd5?: string;
fileSha?: string;
fileSha3?: string;
fileUuid?: string;
fileSubId?: string;
thumbFileSize?: number;
fileBizId?: number;
}
export interface ShareLocationElement {
text: string;
ext: string;
}
export interface StructLongMsgElement {
xmlContent: string;
resId: string;
}
export interface ReplyElement {
sourceMsgIdInRecords?: string;
replayMsgSeq: string;
replayMsgId: string;
senderUin: string;
senderUidStr?: string;
replyMsgTime?: string;
replyMsgClientSeq?: string;
}
export interface CalendarElement {
summary: string;
msg: string;
expireTimeMs: string;
schemaType: number;
schema: string;
}
export interface GiphyElement {
id: string;
isClip: boolean;
width: number;
height: number;
}
export interface AvRecordElement {
type: number;
time: string;
text: string;
mainType: number;
hasRead: boolean;
extraType: number;
}
// 非element
interface YoloUserInfo {
uid: string;
result: number;
rank: number;
bizId: string;
}
export interface YoloGameResultElement {
UserInfo: YoloUserInfo[];
}
export interface FaceBubbleElement {
faceCount: number;
faceSummary: string;
faceFlag: number;
content: string;
oldVersionStr: string;
faceType: number;
others: string;
yellowFaceInfo: {
index: number;
buf: string;
compatibleText: string;
text: string;
};
}
export interface TaskTopMsgElement {
msgTitle: string;
msgSummary: string;
iconUrl: string;
topMsgType: number;
}
export interface PttElement {
canConvert2Text: boolean;
duration: number;
fileBizId: null;
fileId: number;
fileName: string;
filePath: string;
fileSize: string;
fileSubId: string;
fileUuid: string; // FileId
formatType: number; // Todo 已定义 但是未替换
invalidState: number;
md5HexStr: string;
playState: number;
progress: number; //进度
text: string;
transferStatus: number;
translateStatus: number;
voiceChangeType: number;
voiceType: number;
waveAmplitudes: number[];
autoConvertText: number;
}
export type SendRecommendedMsgElement = SendElementBase<ElementType.RECOMMENDEDMSG> & ElementBase<'recommendedMsgElement'>;
export type SendTaskTopMsgElement = SendElementBase<ElementType.TASKTOPMSG> & ElementBase<'taskTopMsgElement'>;
export type SendTofuRecordElement = SendElementBase<ElementType.TOFURECORD> & ElementBase<'tofuRecordElement'>;
export type SendFaceBubbleElement = SendElementBase<ElementType.FACEBUBBLE> & ElementBase<'faceBubbleElement'>;
export type SendAvRecordElement = SendElementBase<ElementType.AVRECORD> & ElementBase<'avRecordElement'>;
export type SendInlineKeyboardElement = SendElementBase<ElementType.INLINEKEYBOARD> & ElementBase<'inlineKeyboardElement'>;
export type SendYoloGameResultElement = SendElementBase<ElementType.YOLOGAMERESULT> & ElementBase<'yoloGameResultElement'>;
export type SendGiphyElement = SendElementBase<ElementType.GIPHY> & ElementBase<'giphyElement'>;
export type SendWalletElement = SendElementBase<ElementType.UNKNOWN> & ElementBase<'walletElement'>;
export type SendCalendarElement = SendElementBase<ElementType.CALENDAR> & ElementBase<'calendarElement'>;
export type SendLiveGiftElement = SendElementBase<ElementType.LIVEGIFT> & ElementBase<'liveGiftElement'>;
export type SendTextElement = SendElementBase<ElementType.TEXT> & ElementBase<'textElement'>;
export type SendReplyElement = SendElementBase<ElementType.REPLY> & ElementBase<'replyElement'>;
export type SendFaceElement = SendElementBase<ElementType.FACE> & ElementBase<'faceElement'>;
export type SendMarketFaceElement = SendElementBase<ElementType.MFACE> & ElementBase<'marketFaceElement'>;
export type SendStructLongMsgElement = SendElementBase<ElementType.STRUCTLONGMSG> & ElementBase<'structLongMsgElement'>;
export type SendPicElement = SendElementBase<ElementType.PIC> & ElementBase<'picElement'>;
export type SendPttElement = SendElementBase<ElementType.PTT> & ElementBase<'pttElement', {
pttElement: ['fileName', 'filePath', 'md5HexStr', 'fileSize', 'duration', 'formatType', 'voiceType',
'voiceChangeType', 'canConvert2Text', 'waveAmplitudes', 'fileSubId', 'playState', 'autoConvertText']
}>;
export type SendFileElement = SendElementBase<ElementType.FILE> & ElementBase<'fileElement'>;
export type SendVideoElement = SendElementBase<ElementType.VIDEO> & ElementBase<'videoElement'>;
export type SendArkElement = SendElementBase<ElementType.ARK> & ElementBase<'arkElement'>;
export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & ElementBase<'markdownElement'>;
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;

View File

@@ -1,12 +1,13 @@
import { QQLevel, Sex } from './user';
import { QQLevel, NTSex } from './user';
export interface KickMemberInfo {
optFlag: number,
optOperate: number,
optMemberUid: string,
optBytesMsg: string,
optFlag: number;
optOperate: number;
optMemberUid: string;
optBytesMsg: string;
}
//getGroupDetailInfo GroupCode,GroupInfoSource
// 获取群详细信息的来源类型
export enum GroupInfoSource {
KUNSPECIFIED,
KBIGDATACARD,
@@ -16,6 +17,7 @@ export enum GroupInfoSource {
KRECENTCONTACT,
KMOREPANEL
}
export interface GroupExt0xEF0InfoFilter {
bindGuildId: number;
blacklistExpireTime: number;
@@ -52,18 +54,20 @@ export interface GroupExt0xEF0InfoFilter {
}
export interface KickMemberV2Req {
groupCode: string,
kickFlag: number,
kickList: Array<KickMemberInfo>,
kickListUids: Array<string>,
kickMsg: string
groupCode: string;
kickFlag: number;
kickList: Array<KickMemberInfo>;
kickListUids: Array<string>;
kickMsg: string;
}
// 数据来源类型
export enum DataSource {
LOCAL,
REMOTE
}
// 群列表更新类型
export enum GroupListUpdateType {
REFRESHALL,
GETALL,
@@ -80,42 +84,42 @@ export interface GroupMemberCache {
}
export interface Group {
groupCode: string,
createTime?: string,//高版本才有
maxMember: number,
memberCount: number,
groupName: string,
groupStatus: number,
memberRole: number,
isTop: boolean,
toppedTimestamp: string,
privilegeFlag: number, //65760
isConf: boolean,
hasModifyConfGroupFace: boolean,
hasModifyConfGroupName: boolean,
remarkName: string,
hasMemo: boolean,
groupShutupExpireTime: string, //"0",
personShutupExpireTime: string, //"0",
discussToGroupUin: string, //"0",
discussToGroupMaxMsgSeq: number,
discussToGroupTime: number,
groupFlagExt: number, //1073938496,
authGroupType: number, //0,
groupCreditLevel: number, //0,
groupFlagExt3: number, //0,
groupCode: string;
createTime?: string;
maxMember: number;
memberCount: number;
groupName: string;
groupStatus: number;
memberRole: number;
isTop: boolean;
toppedTimestamp: string;
privilegeFlag: number;
isConf: boolean;
hasModifyConfGroupFace: boolean;
hasModifyConfGroupName: boolean;
remarkName: string;
hasMemo: boolean;
groupShutupExpireTime: string;
personShutupExpireTime: string;
discussToGroupUin: string;
discussToGroupMaxMsgSeq: number;
discussToGroupTime: number;
groupFlagExt: number;
authGroupType: number;
groupCreditLevel: number;
groupFlagExt3: number;
groupOwnerId: {
memberUin: string, //"0",
memberUid: string, //"u_fbf8N7aeuZEnUiJAbQ9R8Q"
}
memberUin: string;
memberUid: string;
};
}
export enum GroupMemberRole {
normal = 2,
admin = 3,
owner = 4
export enum NTGroupMemberRole {
KUNSPECIFIED = 0,
KSTRANGER = 1,
KMEMBER = 2,
KADMIN = 3,
KOWNER = 4
}
export interface GroupMember {
memberRealLevel: number | undefined;
memberSpecialTitle?: string;
@@ -126,15 +130,15 @@ export interface GroupMember {
nick: string;
qid: string;
remark: string;
role: GroupMemberRole; // 群主:4, 管理员:3群员:2
shutUpTime: number; // 禁言时间,单位是什么暂时不清楚
uid: string; // 加密的字符串
uin: string; // QQ号
role: NTGroupMemberRole;
shutUpTime: number; // 禁言时间(S)
uid: string;
uin: string;
isRobot: boolean;
sex?: Sex;
sex?: NTSex;
age?: number;
qqLevel?: QQLevel;
isChangeRole: boolean;
joinTime: string;
lastSpeakTime: string;
}
}

View File

@@ -5,4 +5,6 @@ export * from './notify';
export * from './cache';
export * from './system';
export * from './webapi';
export * from './sign';
export * from './sign';
export * from './element';
export * from './constant';

535
src/core/types/msg.ts Normal file
View File

@@ -0,0 +1,535 @@
import { NTGroupMemberRole } from '@/core';
import { ActionBarElement, ArkElement, AvRecordElement, CalendarElement, FaceBubbleElement, FaceElement, FileElement, GiphyElement, GrayTipElement, MarketFaceElement, PicElement, PttElement, RecommendedMsgElement, ReplyElement, ShareLocationElement, StructLongMsgElement, TaskTopMsgElement, TextElement, TofuRecordElement, VideoElement, YoloGameResultElement } from './element';
/**
* 表示对等方的信息
*/
export interface Peer {
chatType: ChatType; // 聊天类型
peerUid: string; // 对等方的唯一标识符
guildId?: string; // 可选的频道ID
}
/**
* 表示被踢下线的信息
*/
export interface KickedOffLineInfo {
appId: number; // 应用ID
instanceId: number; // 实例ID
sameDevice: boolean; // 是否为同一设备
tipsDesc: string; // 提示描述
tipsTitle: string; // 提示标题
kickedType: number; // 被踢类型
securityKickedType: number; // 安全踢出类型
}
/**
* 获取文件列表的参数
*/
export interface GetFileListParam {
sortType: number;
fileCount: number;
startIndex: number;
sortOrder: number;
showOnlinedocFolder: number;
folderId?: string;
}
/**
* 消息元素类型枚举
*/
export enum ElementType {
UNKNOWN = 0,
TEXT = 1,
PIC = 2,
FILE = 3,
PTT = 4,
VIDEO = 5,
FACE = 6,
REPLY = 7,
GreyTip = 8, // “小灰条”,包括拍一拍 (Poke)、撤回提示等
WALLET = 9,
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
}
/**
* 消息类型枚举
*/
export enum NTMsgType {
KMSGTYPEARKSTRUCT = 11,
KMSGTYPEFACEBUBBLE = 24,
KMSGTYPEFILE = 3,
KMSGTYPEGIFT = 14,
KMSGTYPEGIPHY = 13,
KMSGTYPEGRAYTIPS = 5,
KMSGTYPEMIX = 2,
KMSGTYPEMULTIMSGFORWARD = 8,
KMSGTYPENULL = 1,
KMSGTYPEONLINEFILE = 21,
KMSGTYPEONLINEFOLDER = 27,
KMSGTYPEPROLOGUE = 29,
KMSGTYPEPTT = 6,
KMSGTYPEREPLY = 9,
KMSGTYPESHARELOCATION = 25,
KMSGTYPESTRUCT = 4,
KMSGTYPESTRUCTLONGMSG = 12,
KMSGTYPETEXTGIFT = 15,
KMSGTYPEUNKNOWN = 0,
KMSGTYPEVIDEO = 7,
KMSGTYPEWALLET = 10
}
/**
* 图片类型枚举
*/
export enum PicType {
NEWPIC_APNG = 2001,
NEWPIC_BMP = 1005,
NEWPIC_GIF = 2000,
NEWPIC_JPEG = 1000,
NEWPIC_PNG = 1001,
NEWPIC_PROGERSSIV_JPEG = 1003,
NEWPIC_SHARPP = 1004,
NEWPIC_WEBP = 1002
}
/**
* 图片子类型枚举
*/
export enum PicSubType {
KNORMAL = 0,
KCUSTOM = 1,
KHOT = 2,
KDIPPERCHART = 3,
KSMART = 4,
KSPACE = 5,
KUNKNOW = 6,
KRELATED = 7
}
/**
* 消息@类型枚举
*/
export enum NTMsgAtType {
ATTYPEALL = 1,
ATTYPECATEGORY = 512,
ATTYPECHANNEL = 16,
ATTYPEME = 4,
ATTYPEONE = 2,
ATTYPEONLINE = 64,
ATTYPEROLE = 8,
ATTYPESUMMON = 32,
ATTYPESUMMONONLINE = 128,
ATTYPESUMMONROLE = 256,
ATTYPEUNKNOWN = 0
}
/**
* 消息元素接口
*/
export interface MessageElement {
elementType: ElementType,
elementId: string,
extBufForUI?: string, //"0x",
textElement?: TextElement;
faceElement?: FaceElement,
marketFaceElement?: MarketFaceElement,
replyElement?: ReplyElement,
picElement?: PicElement,
pttElement?: PttElement,
videoElement?: VideoElement,
grayTipElement?: GrayTipElement,
arkElement?: ArkElement,
fileElement?: FileElement,
liveGiftElement?: null,
markdownElement?: MarkdownElement,
structLongMsgElement?: StructLongMsgElement,
multiForwardMsgElement?: MultiForwardMsgElement,
giphyElement?: GiphyElement,
walletElement?: null,
inlineKeyboardElement?: InlineKeyboardElement,
textGiftElement?: null,//????
calendarElement?: CalendarElement,
yoloGameResultElement?: YoloGameResultElement,
avRecordElement?: AvRecordElement,
structMsgElement?: null,
faceBubbleElement?: FaceBubbleElement,
shareLocationElement?: ShareLocationElement,
tofuRecordElement?: TofuRecordElement,
taskTopMsgElement?: TaskTopMsgElement,
recommendedMsgElement?: RecommendedMsgElement,
actionBarElement?: ActionBarElement
}
/**
* 消息来源类型枚举
*/
export enum MsgSourceType {
K_DOWN_SOURCETYPE_AIOINNER = 1,
K_DOWN_SOURCETYPE_BIGSCREEN = 2,
K_DOWN_SOURCETYPE_HISTORY = 3,
K_DOWN_SOURCETYPE_UNKNOWN = 0
}
/**
* 聊天类型枚举
*/
export enum ChatType {
KCHATTYPEADELIE = 42,
KCHATTYPEBUDDYNOTIFY = 5,
KCHATTYPEC2C = 1,
KCHATTYPECIRCLE = 113,
KCHATTYPEDATALINE = 8,
KCHATTYPEDATALINEMQQ = 134,
KCHATTYPEDISC = 3,
KCHATTYPEFAV = 41,
KCHATTYPEGAMEMESSAGE = 105,
KCHATTYPEGAMEMESSAGEFOLDER = 116,
KCHATTYPEGROUP = 2,
KCHATTYPEGROUPBLESS = 133,
KCHATTYPEGROUPGUILD = 9,
KCHATTYPEGROUPHELPER = 7,
KCHATTYPEGROUPNOTIFY = 6,
KCHATTYPEGUILD = 4,
KCHATTYPEGUILDMETA = 16,
KCHATTYPEMATCHFRIEND = 104,
KCHATTYPEMATCHFRIENDFOLDER = 109,
KCHATTYPENEARBY = 106,
KCHATTYPENEARBYASSISTANT = 107,
KCHATTYPENEARBYFOLDER = 110,
KCHATTYPENEARBYHELLOFOLDER = 112,
KCHATTYPENEARBYINTERACT = 108,
KCHATTYPEQQNOTIFY = 132,
KCHATTYPERELATEACCOUNT = 131,
KCHATTYPESERVICEASSISTANT = 118,
KCHATTYPESERVICEASSISTANTSUB = 201,
KCHATTYPESQUAREPUBLIC = 115,
KCHATTYPESUBSCRIBEFOLDER = 30,
KCHATTYPETEMPADDRESSBOOK = 111,
KCHATTYPETEMPBUSSINESSCRM = 102,
KCHATTYPETEMPC2CFROMGROUP = 100,
KCHATTYPETEMPC2CFROMUNKNOWN = 99,
KCHATTYPETEMPFRIENDVERIFY = 101,
KCHATTYPETEMPNEARBYPRO = 119,
KCHATTYPETEMPPUBLICACCOUNT = 103,
KCHATTYPETEMPWPA = 117,
KCHATTYPEUNKNOWN = 0,
KCHATTYPEWEIYUN = 40,
}
/**
* 灰色提示元素子类型枚举
*/
export enum NTGrayTipElementSubTypeV2 {
GRAYTIP_ELEMENT_SUBTYPE_AIOOP = 15,
GRAYTIP_ELEMENT_SUBTYPE_BLOCK = 14,
GRAYTIP_ELEMENT_SUBTYPE_BUDDY = 5,
GRAYTIP_ELEMENT_SUBTYPE_BUDDYNOTIFY = 9,
GRAYTIP_ELEMENT_SUBTYPE_EMOJIREPLY = 3,
GRAYTIP_ELEMENT_SUBTYPE_ESSENCE = 7,
GRAYTIP_ELEMENT_SUBTYPE_FEED = 6,
GRAYTIP_ELEMENT_SUBTYPE_FEEDCHANNELMSG = 11,
GRAYTIP_ELEMENT_SUBTYPE_FILE = 10,
GRAYTIP_ELEMENT_SUBTYPE_GROUP = 4,
GRAYTIP_ELEMENT_SUBTYPE_GROUPNOTIFY = 8,
GRAYTIP_ELEMENT_SUBTYPE_JSON = 17,
GRAYTIP_ELEMENT_SUBTYPE_LOCALMSG = 13,
GRAYTIP_ELEMENT_SUBTYPE_PROCLAMATION = 2,
GRAYTIP_ELEMENT_SUBTYPE_REVOKE = 1,
GRAYTIP_ELEMENT_SUBTYPE_UNKNOWN = 0,
GRAYTIP_ELEMENT_SUBTYPE_WALLET = 16,
GRAYTIP_ELEMENT_SUBTYPE_XMLMSG = 12,
}
/**
* 表情类型枚举
*/
export enum FaceType {
normal = 1, // 小黄脸
normal2 = 2, // 新小黄脸
dice = 3, // 骰子
poke = 5 // 拍一拍
}
/**
* Poke 类型枚举
*/
export enum PokeType {
POKE_TYPE_APPROVE = 3,
POKE_TYPE_GIVING_HEART = 2,
POKE_TYPE_GREAT_MOVE = 6,
POKE_TYPE_HEART_BREAK = 4,
POKE_TYPE_HI_TOGETHER = 5,
POKE_TYPE_POKE = 1,
POKE_TYPE_POKE_OLD = 0,
POKE_TYPE_VAS_POKE = 126,
}
/**
* 表情索引枚举
*/
export enum FaceIndex {
dice = 358,
rps = 359
}
/**
* 视频类型枚举
*/
export enum NTVideoType {
VIDEO_FORMAT_AFS = 7,
VIDEO_FORMAT_AVI = 1,
VIDEO_FORMAT_MKV = 4,
VIDEO_FORMAT_MOD = 9,
VIDEO_FORMAT_MOV = 8,
VIDEO_FORMAT_MP4 = 2,
VIDEO_FORMAT_MTS = 11,
VIDEO_FORMAT_RM = 6,
VIDEO_FORMAT_RMVB = 5,
VIDEO_FORMAT_TS = 10,
VIDEO_FORMAT_WMV = 3,
}
/**
* Markdown元素接口
*/
export interface MarkdownElement {
content: string;
}
/**
* 内联键盘按钮接口
*/
export interface InlineKeyboardElementRowButton {
id: string;
label: string;
visitedLabel: string;
style: 1; // 未知
type: 2; // 未知
clickLimit: 0; // 未知
unsupportTips: string;
data: string;
atBotShowChannelList: boolean;
permissionType: number;
specifyRoleIds: [];
specifyTinyids: [];
isReply: false;
anchor: 0;
enter: false;
subscribeDataTemplateIds: [];
}
/**
* 内联键盘元素接口
*/
export interface InlineKeyboardElement {
rows: [{
buttons: InlineKeyboardElementRowButton[]
}],
botAppid: string;
}
/**
* Aio操作灰色提示元素接口
*/
export interface TipAioOpGrayTipElement {
operateType: number;
peerUid: string;
fromGrpCodeOfTmpChat: string;
}
/**
* 群提示元素类型枚举
*/
export enum TipGroupElementType {
KUNKNOWN = 0,
KMEMBERADD = 1,
KDISBANDED = 2,
KQUITTE = 3,
KCREATED = 4,
KGROUPNAMEMODIFIED = 5,
KBLOCK = 6,
KUNBLOCK = 7,
KSHUTUP = 8,
KBERECYCLED = 9,
KDISBANDORBERECYCLED = 10
}
/**
* 群加入ShowType
*/
export enum MemberAddShowType {
K_OTHER_ADD = 0,
K_OTHER_ADD_BY_OTHER_QRCODE = 2,
K_OTHER_ADD_BY_YOUR_QRCODE = 3,
K_OTHER_INVITE_OTHER = 5,
K_OTHER_INVITE_YOU = 6,
K_YOU_ADD = 1,
K_YOU_ADD_BY_OTHER_QRCODE = 4,
K_YOU_ALREADY_MEMBER = 8,
K_YOU_INVITE_OTHER = 7,
}
/**
* 群提示元素接口
*/
export interface TipGroupElement {
type: TipGroupElementType;
role: 0;
groupName: string;
memberUid: string;
memberNick: string;
memberRemark: string;
adminUid: string;
adminNick: string;
adminRemark: string;
createGroup: null;
memberAdd?: {
showType: MemberAddShowType;
otherAdd: null;
otherAddByOtherQRCode: null;
otherAddByYourQRCode: null;
youAddByOtherQRCode: null;
otherInviteOther: null;
otherInviteYou: null;
youInviteOther: null
};
shutUp?: {
curTime: string;
duration: string; // 禁言时间,秒
admin: {
uid: string;
card: string;
name: string;
role: NTGroupMemberRole
};
member: {
uid: string
card: string;
name: string;
role: NTGroupMemberRole
}
};
}
/**
* 多条转发消息元素接口
*/
export interface MultiForwardMsgElement {
xmlContent: string; // xml格式的消息内容
resId: string;
fileName: string;
}
/**
* 发送状态类型枚举
*/
export enum SendStatusType {
KSEND_STATUS_FAILED = 0,
KSEND_STATUS_SENDING = 1,
KSEND_STATUS_SUCCESS = 2,
KSEND_STATUS_SUCCESS_NOSEQ = 3
}
/**
* 原始消息接口
*/
export interface RawMessage {
parentMsgPeer: Peer; // 父消息的Peer
parentMsgIdList: string[];// 父消息 ID 列表
id?: number;// 扩展字段,与 Ob11 msg ID 有关
guildId: string;// 频道ID
msgRandom: string;// 消息ID相关
msgId: string;// 雪花ID
msgTime: string;// 消息时间戳
msgSeq: string;// 消息序列号
msgType: NTMsgType;// 消息类型
subMsgType: number;// 子消息类型
senderUid: string;// 发送者 UID
senderUin: string;// 发送者 QQ 号
peerUid: string;// 群号 / 用户 UID
peerUin: string;// 群号 / 用户 QQ 号
remark?: string;// 备注
peerName: string;// Peer名称
sendNickName: string;// 发送者昵称
sendRemarkName: string;// 发送者好友备注
sendMemberName?: string;// 发送者群名片(如果是群消息)
chatType: ChatType;// 会话类型
sendStatus?: SendStatusType;// 消息状态
recallTime: string;// 撤回时间,"0" 是没有撤回
records: RawMessage[];// 消息记录
elements: MessageElement[];// 消息元素
sourceType: MsgSourceType;// 消息来源类型
isOnlineMsg: boolean;// 是否为在线消息
}
/**
* 查询消息参数接口
*/
export interface QueryMsgsParams {
chatInfo: Peer;
filterMsgType: [];
filterSendersUid: string[];
filterMsgFromTime: string;
filterMsgToTime: string;
pageLimit: number;
isReverseOrder: boolean;
isIncludeCurrent: boolean;
}
/**
* 临时聊天信息API接口
*/
export interface TmpChatInfoApi {
errMsg: string;
result: number;
tmpChatInfo?: TmpChatInfo;
}
/**
* 临时聊天信息接口
*/
export interface TmpChatInfo {
chatType: number;
fromNick: string;
groupCode: string;
peerUid: string;
sessionType: number;
sig: string;
}
/**
* 消息请求类型接口
*/
export interface MsgReqType {
peer: Peer,
byType: number,
msgId: string,
msgSeq: string,
msgTime: string,
clientSeq: string,
cnt: number,
queryOrder: boolean,
includeSelf: boolean,
includeDeleteMsg: boolean,
extraCnt: number
}

View File

@@ -107,9 +107,12 @@ export interface GroupNotify {
warningTips: string;
}
export enum GroupRequestOperateTypes {
approve = 1,
reject = 2
export enum NTGroupRequestOperateTypes {
KUNSPECIFIED = 0,
KAGREE = 1,
KREFUSE = 2,
KIGNORE = 3,
KDELETE = 4
}
export enum BuddyReqType {
@@ -135,7 +138,7 @@ export interface FriendRequest {
isDecide: boolean;
friendUid: string;
reqType: BuddyReqType,
reqTime: string; // 时间戳;
reqTime: string; // 时间戳
extWords: string; // 申请人填写的验证消息
isUnread: boolean;
friendNick: string;

View File

@@ -1,16 +1,20 @@
export enum Sex {
male = 1,
female = 2,
unknown = 255,
// 性别枚举
export enum NTSex {
GENDER_UNKOWN = 0,
GENDER_MALE = 1,
GENDER_FEMALE = 2,
GENDER_PRIVACY = 255,
}
// 好友分类类型
export interface BuddyCategoryType {
categoryId: number;
categroyName: string;
categroyMbCount: number;
categoryName: string;
categoryMbCount: number;
buddyList: User[];
}
// 核心信息
export interface CoreInfo {
uid: string;
uin: string;
@@ -18,6 +22,7 @@ export interface CoreInfo {
remark: string;
}
// 基本信息
export interface BaseInfo {
qid: string;
longNick: string;
@@ -33,20 +38,24 @@ export interface BaseInfo {
richBuffer: string;
}
// 音乐信息
interface MusicInfo {
buf: string;
}
// 视频业务信息
interface VideoBizInfo {
cid: string;
tvUrl: string;
synchType: string;
}
// 视频信息
interface VideoInfo {
name: string;
}
// 扩展在线业务信息
interface ExtOnlineBusinessInfo {
buf: string;
customStatus: any;
@@ -54,10 +63,12 @@ interface ExtOnlineBusinessInfo {
videoInfo: VideoInfo;
}
// 扩展缓冲区
interface ExtBuffer {
buf: string;
}
// 用户状态
interface UserStatus {
uid: string;
uin: string;
@@ -79,12 +90,14 @@ interface UserStatus {
extBuffer: ExtBuffer;
}
// 特权图标
interface PrivilegeIcon {
jumpUrl: string;
openIconList: any[];
closeIconList: any[];
}
// 增值服务信息
interface VasInfo {
vipFlag: boolean;
yearVipFlag: boolean;
@@ -117,6 +130,7 @@ interface VasInfo {
privilegeIcon: PrivilegeIcon;
}
// 关系标志
interface RelationFlags {
topTime: string;
isBlock: boolean;
@@ -134,7 +148,7 @@ interface RelationFlags {
isHidePrivilegeIcon: number;
}
// 通用扩展信息
interface CommonExt {
constellation: number;
shengXiao: number;
@@ -153,20 +167,26 @@ interface CommonExt {
labels: any[];
qqLevel: QQLevel;
}
// 好友列表请求类型枚举
export enum BuddyListReqType {
KNOMAL,
KLETTER
}
// 图片信息
interface Pic {
picId: string;
picTime: number;
picUrlMap: Record<string, string>;
}
// 照片墙
interface PhotoWall {
picList: Pic[];
}
// 简单信息
export interface SimpleInfo {
uid?: string;
uin?: string;
@@ -179,8 +199,10 @@ export interface SimpleInfo {
intimate: any;
}
// 好友类型
export type FriendV2 = SimpleInfo;
// 自身状态信息
export interface SelfStatusInfo {
uid: string;
status: number;
@@ -192,6 +214,7 @@ export interface SelfStatusInfo {
setTime: string;
}
// 用户详细信息监听参数
export interface UserDetailInfoListenerArg {
uid: string;
uin: string;
@@ -200,14 +223,16 @@ export interface UserDetailInfoListenerArg {
photoWall: PhotoWall;
}
// 修改个人资料参数
export interface ModifyProfileParams {
nick: string,
longNick: string,
sex: Sex,
birthday: { birthday_year: string, birthday_month: string, birthday_day: string },
location: any//undefined
nick: string;
longNick: string;
sex: NTSex;
birthday: { birthday_year: string, birthday_month: string, birthday_day: string };
location: any;
}
// 好友资料点赞请求
export interface BuddyProfileLikeReq {
friendUids: string[];
basic: number;
@@ -219,6 +244,7 @@ export interface BuddyProfileLikeReq {
limit?: number;
}
// QQ等级信息
export interface QQLevel {
crownNum: number;
sunNum: number;
@@ -226,14 +252,15 @@ export interface QQLevel {
starNum: number;
}
// 用户信息
export interface User {
uid: string; // 加密的字符串
uin: string; // QQ号
uid: string;
uin: string;
nick: string;
avatarUrl?: string;
longNick?: string; // 签名
longNick?: string;
remark?: string;
sex?: Sex;
sex?: NTSex;
age?: number;
qqLevel?: QQLevel;
qid?: string;
@@ -244,7 +271,7 @@ export interface User {
constellation?: number;
shengXiao?: number;
kBloodType?: number;
homeTown?: string; //"0-0-0";
homeTown?: string;
makeFriendCareer?: number;
pos?: string;
eMail?: string;
@@ -266,10 +293,10 @@ export interface User {
privilegeIcon?: {
jumpUrl: string;
openIconList: unknown[];
closeIconList: unknown[]
closeIconList: unknown[];
};
photoWall?: {
picList: unknown[]
picList: unknown[];
};
vipFlag?: boolean;
yearVipFlag?: boolean;
@@ -285,34 +312,40 @@ export interface User {
pendantId?: string;
}
// 自身信息
export interface SelfInfo extends User {
online?: boolean;
}
// 好友类型
export type Friend = User;
// 本来是 Friend extends User 现在用不到
// 业务键枚举
export enum BizKey {
KPRIVILEGEICON,
KPHOTOWALL
}
// 根据UIN获取用户详细信息
export interface UserDetailInfoByUin {
result: number,
errMsg: string,
result: number;
errMsg: string;
detail: {
uid: string,
uin: string,
simpleInfo: SimpleInfo,
commonExt: CommonExt,
photoWall: null
}
uid: string;
uin: string;
simpleInfo: SimpleInfo;
commonExt: CommonExt;
photoWall: null;
};
}
// 用户详细信息来源枚举
export enum UserDetailSource {
KDB,
KSERVER
}
// 个人资料业务类型枚举
export enum ProfileBizType {
KALL,
KBASEEXTEND,

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