Compare commits

...

115 Commits

Author SHA1 Message Date
手瓜一十雪
99b504b5f6 fix: #880 2025-03-16 09:12:52 +08:00
Mlikiowa
1146454fec release: v4.6.9 2025-03-15 10:58:09 +00:00
手瓜一十雪
805e014a75 fix: #877 2025-03-15 18:54:51 +08:00
Mlikiowa
d3acd1efc1 release: v4.6.8 2025-03-14 10:13:29 +00:00
手瓜一十雪
9fcd218a5a fix: #873 2025-03-14 18:12:58 +08:00
手瓜一十雪
d6a0830cfe fix: #875 2025-03-14 18:07:03 +08:00
手瓜一十雪
40a63b9c66 fix: #870 2025-03-14 17:53:03 +08:00
手瓜一十雪
eeb19a04cc fix: packet异常 2025-03-14 17:39:37 +08:00
Mlikiowa
91e457eb03 release: v4.6.7 2025-03-09 08:31:07 +00:00
手瓜一十雪
78d1919d7f feat: 32896 2025-03-09 16:30:43 +08:00
手瓜一十雪
8393acf173 Merge pull request #856 from HDTianRu/main
feat: 额外返回原msgSeq条目
2025-03-09 10:09:41 +08:00
手瓜一十雪
bca152a047 feat: readme 翻新 2025-03-09 10:08:49 +08:00
HDTianRu
6a15908a93 feat: 额外返回原msgSeq条目 2025-03-08 16:36:17 +08:00
bietiaop
c626bbab74 fix: #854 2025-03-07 10:25:38 +08:00
Mlikiowa
c5c7dcc6f2 release: v4.6.6 2025-03-06 10:51:30 +00:00
手瓜一十雪
03dafe727e fix: win 2025-03-06 18:51:05 +08:00
Mlikiowa
744921c45e release: v4.6.5 2025-03-06 10:09:45 +00:00
手瓜一十雪
abc4a4dcba feat: 32793 2025-03-06 18:09:14 +08:00
Mlikiowa
7e0da2f929 release: v4.6.4 2025-03-05 13:15:11 +00:00
手瓜一十雪
a3b70d0f1f fix 2025-03-05 21:14:52 +08:00
Mlikiowa
d291724f06 release: v4.6.3 2025-03-03 09:17:03 +00:00
手瓜一十雪
122a9ca2cc feat: o3拦截 2025-03-03 17:16:36 +08:00
手瓜一十雪
48aaddd32b feat:rkey 2025-03-03 12:28:55 +08:00
手瓜一十雪
47401af856 feat: searchMsgWithKeywords 2025-03-02 16:07:27 +08:00
Mlikiowa
709adfd812 release: v4.6.2 2025-03-02 07:11:16 +00:00
手瓜一十雪
038d0c5412 fix: #785 2025-03-02 14:55:47 +08:00
手瓜一十雪
6bb4362ed4 feat: 32721 2025-03-02 14:36:11 +08:00
手瓜一十雪
e617f9452d fix: #841 2025-03-02 14:32:21 +08:00
手瓜一十雪
6d8bb49a37 fix: #837 2025-03-02 14:27:09 +08:00
手瓜一十雪
4f6073ee86 fix: 837 2025-03-02 14:26:28 +08:00
手瓜一十雪
2e7176304b fix: #843 2025-03-02 14:24:51 +08:00
Mlikiowa
e36cf11004 release: v4.6.1 2025-02-27 08:35:02 +00:00
手瓜一十雪
0e49e17f68 feat: 32690 2025-02-27 16:34:09 +08:00
手瓜一十雪
524de45f6b Merge pull request #827 from NapNeko/dependabot/npm_and_yarn/globals-16.0.0
chore(deps-dev): bump globals from 15.15.0 to 16.0.0
2025-02-27 16:18:17 +08:00
手瓜一十雪
85741a4b60 feat: 32690 2025-02-27 16:14:39 +08:00
dependabot[bot]
f9ccb8c978 chore(deps-dev): bump globals from 15.15.0 to 16.0.0
Bumps [globals](https://github.com/sindresorhus/globals) from 15.15.0 to 16.0.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v16.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:06:38 +00:00
手瓜一十雪
ea3d069e49 feat: vsc build dev体验增强 2025-02-23 17:54:19 +08:00
Mlikiowa
3e6024f183 release: v4.6.0 2025-02-23 09:31:55 +00:00
Mlikiowa
337871693a release: v4.5.24 2025-02-23 09:31:19 +00:00
手瓜一十雪
2d921c4577 feat: sisi的妙妙rkey 2025-02-23 17:30:01 +08:00
手瓜一十雪
9accff7323 fix: ts warning 2025-02-23 17:28:30 +08:00
手瓜一十雪
88b1ee8c31 docs: todo #819 2025-02-23 17:17:52 +08:00
手瓜一十雪
3ac618bb4e fix: #822 2025-02-23 17:01:00 +08:00
手瓜一十雪
0051df3741 fix: #824 2025-02-23 16:57:55 +08:00
手瓜一十雪
7eb4e010b0 Merge pull request #823 from NapNeko/refactor-worker
refactor: 即刻起逐出piscina
2025-02-23 14:31:33 +08:00
手瓜一十雪
33cc23ada3 refactor: 即刻起逐出piscina 2025-02-23 14:29:26 +08:00
手瓜一十雪
e5aee372e3 fix: 调整依赖 2025-02-23 13:40:47 +08:00
手瓜一十雪
6b6ce4a761 fix: 依赖迁移到dev 2025-02-22 12:59:37 +08:00
手瓜一十雪
8c4ea7f8f2 fix: 异常代码 2025-02-22 11:57:48 +08:00
手瓜一十雪
c8b268b806 fix: #791 2025-02-22 11:50:54 +08:00
手瓜一十雪
cf5e0e0f14 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-18 17:08:26 +08:00
手瓜一十雪
7b79f9cc17 fix: 日志显示 2025-02-18 17:08:24 +08:00
Mlikiowa
708d599966 release: v4.5.23 2025-02-18 08:56:25 +00:00
手瓜一十雪
1ecd5b78e6 feat: 文件移除path字段增强部分能力 2025-02-18 16:55:43 +08:00
手瓜一十雪
fca2e3c51a style: remove debug 2025-02-18 16:52:30 +08:00
手瓜一十雪
95ea761b2d feat: get_private_file_url 2025-02-18 16:51:51 +08:00
手瓜一十雪
6b3bfa1ee9 fix #810 2025-02-18 13:24:37 +08:00
bietiaop
df3e302a9d fix: #802 2025-02-14 21:26:16 +08:00
pk5ls20
c88a68c9a8 fix: typo x2 2025-02-14 20:52:31 +08:00
Mlikiowa
92d01b9cdd release: v4.5.22 2025-02-14 10:36:03 +00:00
手瓜一十雪
fe04fa5986 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-14 17:41:40 +08:00
手瓜一十雪
c382f541b4 fix: 优化文件处理错误信息并简化下载逻辑 2025-02-14 17:41:25 +08:00
手瓜一十雪
f420527207 Update msg.ts 2025-02-14 17:41:03 +08:00
手瓜一十雪
e0c83ebf79 fix: #793 2025-02-14 17:15:19 +08:00
手瓜一十雪
c7fb18fc08 feat: 补全一些type 2025-02-14 15:39:06 +08:00
手瓜一十雪
2db8ab937d feat: GetUnidirectionalFriendList router 2025-02-14 15:06:36 +08:00
手瓜一十雪
819f5dd8e5 fix: #785 2025-02-14 14:50:00 +08:00
手瓜一十雪
d4a8ed735e fix: #789 2025-02-14 14:48:36 +08:00
手瓜一十雪
f07e3bb4d5 fix: type 2025-02-14 14:44:10 +08:00
手瓜一十雪
fa5ef0c221 fix: #797 2025-02-14 14:41:16 +08:00
手瓜一十雪
da7499ec0b Merge pull request #790 from NapNeko/dependabot/npm_and_yarn/esbuild-0.25.0
chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
2025-02-14 13:51:47 +08:00
Mlikiowa
d2f4327e44 release: v4.5.21 2025-02-12 18:57:14 +00:00
pk5ls20
2eba640180 fix: typo 2025-02-13 02:56:07 +08:00
dependabot[bot]
29ae55f340 chore(deps-dev): bump esbuild from 0.24.0 to 0.25.0
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 08:16:17 +00:00
Mlikiowa
3d2bca3f9f release: v4.5.20 2025-02-09 05:05:52 +00:00
手瓜一十雪
7fd8c0c822 style:lint 2025-02-09 13:00:54 +08:00
手瓜一十雪
a9e9c81505 refactor: data recv 2025-02-09 13:00:17 +08:00
手瓜一十雪
e8cc68bdea style:lint 2025-02-09 12:53:42 +08:00
手瓜一十雪
9e51a661a4 fix: #761 2025-02-09 12:53:10 +08:00
bietiaop
a167aaf55f style: 修改首页卡片色适配主题 2025-02-09 12:28:57 +08:00
bietiaop
a54ecbcaa0 style: 修改侧边栏标题色适配主题 2025-02-09 12:21:34 +08:00
bietiaop
788462cdfa fix: 修复heroui primary色 2025-02-09 12:13:43 +08:00
bietiaop
45c5965b99 style: 增加heroui主题色 2025-02-09 12:11:27 +08:00
bietiaop
ce7614de46 fix: 缺少default 2025-02-09 12:00:02 +08:00
bietiaop
9f78e1ce1e feat: 预定义主题 2025-02-09 11:58:46 +08:00
pk5ls20
2c7b0625e8 chore: format 2025-02-09 01:35:37 +08:00
pk5ls20
c3a5da9be1 feat: #768 2025-02-09 01:33:56 +08:00
bietiaop
ca796e1920 feat: 设置快速登录QQ & 自定义webui主题色
feat: 设置快速登录QQ & 自定义webui主题色
2025-02-09 00:54:27 +08:00
bietiaop
7ce04cf781 final 2025-02-09 00:47:00 +08:00
bietiaop
024a3eb760 fix 2025-02-09 00:18:14 +08:00
bietiaop
1702f429b4 fix 2025-02-09 00:17:49 +08:00
bietiaop
96d79cf495 fix 2025-02-08 23:45:33 +08:00
bietiaop
a6a11a7026 fix 2025-02-08 23:38:30 +08:00
bietiaop
970a49e2a5 fix: 猪咪 2025-02-08 23:05:48 +08:00
bietiaop
2e013ed4f5 fix 2025-02-08 22:43:53 +08:00
bietiaop
f8c396b1fe feat(webui): 快速登录config 2025-02-08 21:16:49 +08:00
手瓜一十雪
b54870cb60 fix 2025-02-08 21:03:59 +08:00
bietiaop
84318acb18 feat(webui): theme 2025-02-08 21:01:29 +08:00
手瓜一十雪
a11a042b93 docs: update 2025-02-08 20:22:51 +08:00
Mlikiowa
8a8aa8f62c release: v4.5.18 2025-02-08 09:43:06 +00:00
手瓜一十雪
93f78f4db5 feat: #780 2025-02-08 17:34:31 +08:00
手瓜一十雪
404bfdd5e6 fix: #783 2025-02-08 17:00:11 +08:00
Mlikiowa
e4577dc2f1 release: v4.5.17 2025-02-07 12:40:47 +00:00
pk5ls20
5c932e5a27 fix: native rkey 2025-02-07 19:20:35 +08:00
Mlikiowa
4bd63c6267 release: v4.5.16 2025-02-07 10:02:35 +00:00
bietiaop
aabe24f903 Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-02-07 18:00:31 +08:00
bietiaop
69cebd7fbc feat: 提示修改默认密码 2025-02-07 18:00:22 +08:00
Mlikiowa
8da371176a release: v4.5.15 2025-02-07 09:52:51 +00:00
手瓜一十雪
dd08adf1d1 fix 2025-02-07 17:43:08 +08:00
手瓜一十雪
2f67bef139 fix: #775 2025-02-07 17:25:48 +08:00
手瓜一十雪
8968c51cdc fix: 砍掉mac pty 沙盒权限不足 2025-02-07 17:11:10 +08:00
手瓜一十雪
f2fdcc9289 feat: webui体验优化 2025-02-07 13:56:48 +08:00
手瓜一十雪
aa3a575cbe feat: 优化初始化步骤 2025-02-07 13:26:48 +08:00
bietiaop
11816d038d fix: #776 2025-02-06 20:10:11 +08:00
Mlikiowa
6a990edb38 release: v4.5.14 2025-02-06 09:17:22 +00:00
107 changed files with 2811 additions and 524 deletions

115
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,115 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "dev:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:shell",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:shell"
]
},
{
"type": "node",
"request": "launch",
"name": "build:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "build:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "build:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"build:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:universal",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:universal"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:framework"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:webui",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:webui"
]
},
{
"type": "node",
"request": "launch",
"name": "lint",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"lint"
]
},
{
"type": "node",
"request": "launch",
"name": "depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"depend"
]
},
{
"type": "node",
"request": "launch",
"name": "dev:depend",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"dev:depend"
]
}
]
}

View File

@@ -1,67 +1,62 @@
<div align="center"> <div align="center">
# NapCat
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto) ![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至.
</div> </div>
--- ---
## 欢迎回家
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 特性介绍 ## Welcome
- [x] **安装简单**:就算是笨蛋也能使用 + NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- [x] **性能友好**:就算是低内存也能使用 - NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
- [x] **接口丰富**:就算是没有也能使用
- [x] **稳定好用**:就算是被捉也能使用
## 使用框架 ## Feature
+ **Easy to Use**
- 作为初学者能够轻松使用.
+ **Quick and Efficient**
- 在低内存操作系统长时运行.
+ **Rich API Interface**
- 完整实现了大部分标准接口.
+ **Stable and Reliable**
- 持续稳定的开发与维护.
## Quick Start
可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本 可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本
**首次使用**请务必查看如下文档看使用教程 **首次使用**请务必查看如下文档看使用教程
### 文档地址 ## Link
[Cloudflare.Worker](https://doc.napneko.icu/) | Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) |
|:-:|:-:|:-:|:-:|
[Cloudflare.HKServer](https://napcat.napneko.icu/) | Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://docs.napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) |
|:-:|:-:|:-:|:-:|
[Github.IO](https://napneko.github.io/) | Contact | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) |
|:-:|:-:|:-:|:-:|
[Cloudflare.Pages](https://napneko.pages.dev/) ## Thanks
[Server.Other](https://docs.napcat.cyou/) + [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
[NapCat.Wiki](https://www.napcat.wiki) + [LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目部分开发
## 回家旅途 + 不过最最重要的 还是需要感谢屏幕前的你哦~
[QQ Group#1](https://qm.qq.com/q/I6LU87a0Yq)
[QQ Group#2](https://qm.qq.com/q/HaRcfrHpUk)
[Telegram](https://t.me/MelodicMoonlight)
> QQ Group#2 准许Bot / Telegram与QQ Group#2 为新建Group
## 性能设计/协议标准
NapCat 已实现90+的 OneBot / GoCQ 标准接口,并提供兼容性保留接口,其设计理念遵守 无数据库/异步优先/后台刷新 的性能思想。
由此设计带来一系列好处在开发中获取群员列表通常小于50Ms单条文本消息发送在320Ms以内在1k+的群聊流畅运行同时带来一些副作用消息Id无法持久无法上报撤回消息原始内容。
NapCat 在设计理念下遵守 OneBot 规范大多数要求并且积极改进,任何合理的标准化 Issue 与 Pr 将被接收。
## 感谢他们
感谢 [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
感谢 React 强力驱动 NapCat.WebUi
不过最最重要的 还是需要感谢屏幕前的你哦~
--- ---
## 特殊感谢 ## License
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目 本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
## 开源附加 **本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高IM易用性实现类似Hook推送此外禁止任何项目未经仓库主作者授权二次分发或基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**

Binary file not shown.

View File

@@ -19,7 +19,7 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" ( if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath% echo provided QQ path is invalid
pause pause
exit /b exit /b
) )

View File

@@ -19,7 +19,7 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" ( if not exist "%QQpath%" (
echo provided QQ path is invalid: %QQpath% echo provided QQ path is invalid
pause pause
exit /b exit /b
) )

View File

@@ -27,8 +27,8 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" ( if not exist "%QQPath%" (
echo provided QQ path is invalid: %QQpath% echo provided QQ path is invalid
pause pause
exit /b exit /b
) )

View File

@@ -27,8 +27,8 @@ for %%a in ("%RetString%") do (
SET QQPath=%pathWithoutUninstall%QQ.exe SET QQPath=%pathWithoutUninstall%QQ.exe
if not exist "%QQpath%" ( if not exist "%QQPath%" (
echo provided QQ path is invalid: %QQpath% echo provided QQ path is invalid
pause pause
exit /b exit /b
) )

View File

@@ -1,10 +1,9 @@
{ {
"name": "qq-chat", "name": "qq-chat",
"version": "9.9.17-30899", "version": "9.9.18-32793",
"verHash": "ececf273", "verHash": "d43f097e",
"linuxVersion": "3.2.15-30899", "linuxVersion": "3.2.16-32793",
"linuxVerHash": "63c751e8", "linuxVerHash": "ee4bd910",
"type": "module",
"private": true, "private": true,
"description": "QQ", "description": "QQ",
"productName": "QQ", "productName": "QQ",
@@ -17,10 +16,27 @@
"bin": { "bin": {
"qd": "externals/devtools/cli/index.js" "qd": "externals/devtools/cli/index.js"
}, },
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js", "main": "./loadNapCat.js",
"buildVersion": "30899", "peerDependenciesMeta": {
"*": {
"optional": true
}
},
"pnpm": {
"patchedDependencies": {
"@vue/runtime-dom@3.5.12": "patches/@vue__runtime-dom@3.5.12.patch",
"@swc/helpers@0.5.3": "patches/@swc__helpers@0.5.3.patch",
"vuex@4.1.0": "patches/vuex@4.1.0.patch"
}
},
"buildVersion": "32793",
"isPureShell": true, "isPureShell": true,
"isByteCodeShell": true, "isByteCodeShell": true,
"platform": "win32", "platform": "win32",
"eleArch": "x64" "eleArch": "x64"
} }

View File

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

View File

@@ -13,6 +13,7 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@heroui/accordion": "^2.2.8",
"@heroui/avatar": "2.2.7", "@heroui/avatar": "2.2.7",
"@heroui/breadcrumbs": "2.2.7", "@heroui/breadcrumbs": "2.2.7",
"@heroui/button": "2.2.10", "@heroui/button": "2.2.10",
@@ -64,6 +65,7 @@
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"quill": "^2.0.3", "quill": "^2.0.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-color": "^2.19.3",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^14.3.5", "react-dropzone": "^14.3.5",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",

View File

@@ -0,0 +1,36 @@
import { Popover, PopoverContent, PopoverTrigger } from '@heroui/popover'
import React from 'react'
import { ColorResult, SketchPicker } from 'react-color'
// 假定 heroui 提供的 Popover组件
interface ColorPickerProps {
color: string
onChange: (color: ColorResult) => void
}
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
const handleChange = (colorResult: ColorResult) => {
onChange(colorResult)
}
return (
<Popover triggerScaleOnOpen={false}>
<PopoverTrigger>
<div
className="w-36 h-8 rounded-md cursor-pointer border border-content4"
style={{ background: color }}
/>
</PopoverTrigger>
<PopoverContent>
<SketchPicker
color={color}
onChange={handleChange}
className="!bg-transparent !shadow-none"
/>
</PopoverContent>
</Popover>
)
}
export default ColorPicker

View File

@@ -1,4 +1,5 @@
import { Button } from '@heroui/button' import { Button } from '@heroui/button'
import clsx from 'clsx'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
@@ -7,15 +8,22 @@ export interface SaveButtonsProps {
reset: () => void reset: () => void
refresh?: () => void refresh?: () => void
isSubmitting: boolean isSubmitting: boolean
className?: string
} }
const SaveButtons: React.FC<SaveButtonsProps> = ({ const SaveButtons: React.FC<SaveButtonsProps> = ({
onSubmit, onSubmit,
reset, reset,
isSubmitting, isSubmitting,
refresh refresh,
className
}) => ( }) => (
<div className="max-w-full mx-3 w-96 flex flex-col justify-center gap-3"> <div
className={clsx(
'max-w-full mx-3 w-96 flex flex-col justify-center gap-3',
className
)}
>
<div className="flex items-center justify-center gap-2 mt-5"> <div className="flex items-center justify-center gap-2 mt-5">
<Button <Button
color="default" color="default"

View File

@@ -136,7 +136,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</div> </div>
<Card <Card
shadow="sm" shadow="sm"
className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible z-20" className="my-4 bg-opacity-50 backdrop-blur-md overflow-visible"
> >
<CardHeader className="font-bold text-lg gap-1 pb-0"> <CardHeader className="font-bold text-lg gap-1 pb-0">
<span className="mr-2"></span> <span className="mr-2"></span>

View File

@@ -58,14 +58,13 @@ const renderItems = (items: MenuItem[], children = false) => {
color="primary" color="primary"
endContent={ endContent={
canOpen ? ( canOpen ? (
// div实现箭头V效果
<div <div
className={clsx( className={clsx(
'ml-auto relative w-3 h-3 transition-transform', 'ml-auto relative w-3 h-3 transition-transform',
open && 'transform rotate-180', open && 'transform rotate-180',
isActive isActive
? 'text-primary-500' ? 'text-primary-500'
: 'text-red-300 dark:text-white', : 'text-primary-200 dark:text-white',
'before:rounded-full', 'before:rounded-full',
'before:content-[""]', 'before:content-[""]',
'before:block', 'before:block',
@@ -98,7 +97,7 @@ const renderItems = (items: MenuItem[], children = false) => {
'w-3 h-1.5 rounded-full ml-auto shadow-lg', 'w-3 h-1.5 rounded-full ml-auto shadow-lg',
isActive isActive
? 'bg-primary-500 animate-spinner-ease-spin' ? 'bg-primary-500 animate-spinner-ease-spin'
: 'bg-red-300 dark:bg-white' : 'bg-primary-200 dark:bg-white'
)} )}
/> />
) )

View File

@@ -34,7 +34,7 @@ const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
endContent endContent
}) => { }) => {
return ( return (
<div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-50 dark:shadow-primary-100 rounded text-primary-400"> <div className="flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400">
{icon} {icon}
<div className="w-24">{title}</div> <div className="w-24">{title}</div>
<div className="text-primary-200">{value}</div> <div className="text-primary-200">{value}</div>
@@ -234,7 +234,7 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
error: qqVersionError error: qqVersionError
} = useRequest(WebUIManager.getQQVersion) } = useRequest(WebUIManager.getQQVersion)
return ( return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 overflow-visible flex-1"> <Card className="bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1">
<CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold"> <CardHeader className="pb-0 items-center gap-1 text-primary-500 font-extrabold">
<FaCircleInfo className="text-lg" /> <FaCircleInfo className="text-lg" />
<span></span> <span></span>

View File

@@ -24,7 +24,7 @@ const SystemStatusItem: React.FC<SystemStatusItemProps> = ({
return ( return (
<div <div
className={clsx( className={clsx(
'shadow-sm p-2 rounded-md text-sm bg-content1 bg-opacity-30', 'shadow-sm shadow-primary-100 p-2 rounded-md text-sm bg-content1 bg-opacity-30',
size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between' size === 'lg' ? 'col-span-2' : 'col-span-1 flex justify-between'
)} )}
> >
@@ -55,7 +55,7 @@ const SystemStatusDisplay: React.FC<SystemStatusDisplayProps> = ({ data }) => {
} }
return ( return (
<Card className="bg-opacity-60 shadow-sm shadow-primary-50 dark:shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden"> <Card className="bg-opacity-60 shadow-sm shadow-primary-100 col-span-1 lg:col-span-2 relative overflow-hidden">
<div className="absolute h-full right-0 top-0"> <div className="absolute h-full right-0 top-0">
<Image <Image
src={bkg} src={bkg}

View File

@@ -0,0 +1,6 @@
import heroui from './themes/heroui'
import nc_pink from './themes/nc_pink'
const themes: ThemeInfo[] = [nc_pink, heroui]
export default themes

View File

@@ -0,0 +1,256 @@
const theme: ThemeConfig = {
dark: {
'--heroui-background': '0 0% 0%',
'--heroui-foreground-50': '240 5.88% 10%',
'--heroui-foreground-100': '240 3.7% 15.88%',
'--heroui-foreground-200': '240 5.26% 26.08%',
'--heroui-foreground-300': '240 5.2% 33.92%',
'--heroui-foreground-400': '240 3.83% 46.08%',
'--heroui-foreground-500': '240 5.03% 64.9%',
'--heroui-foreground-600': '240 4.88% 83.92%',
'--heroui-foreground-700': '240 5.88% 90%',
'--heroui-foreground-800': '240 4.76% 95.88%',
'--heroui-foreground-900': '0 0% 98.04%',
'--heroui-foreground': '210 5.56% 92.94%',
'--heroui-focus': '212.01999999999998 100% 46.67%',
'--heroui-overlay': '0 0% 0%',
'--heroui-divider': '0 0% 100%',
'--heroui-divider-opacity': '0.15',
'--heroui-content1': '240 5.88% 10%',
'--heroui-content1-foreground': '0 0% 98.04%',
'--heroui-content2': '240 3.7% 15.88%',
'--heroui-content2-foreground': '240 4.76% 95.88%',
'--heroui-content3': '240 5.26% 26.08%',
'--heroui-content3-foreground': '240 5.88% 90%',
'--heroui-content4': '240 5.2% 33.92%',
'--heroui-content4-foreground': '240 4.88% 83.92%',
'--heroui-default-50': '240 5.88% 10%',
'--heroui-default-100': '240 3.7% 15.88%',
'--heroui-default-200': '240 5.26% 26.08%',
'--heroui-default-300': '240 5.2% 33.92%',
'--heroui-default-400': '240 3.83% 46.08%',
'--heroui-default-500': '240 5.03% 64.9%',
'--heroui-default-600': '240 4.88% 83.92%',
'--heroui-default-700': '240 5.88% 90%',
'--heroui-default-800': '240 4.76% 95.88%',
'--heroui-default-900': '0 0% 98.04%',
'--heroui-default-foreground': '0 0% 100%',
'--heroui-default': '240 5.26% 26.08%',
'--heroui-danger-50': '340 84.91% 10.39%',
'--heroui-danger-100': '339.33 86.54% 20.39%',
'--heroui-danger-200': '339.11 85.99% 30.78%',
'--heroui-danger-300': '339 86.54% 40.78%',
'--heroui-danger-400': '339.2 90.36% 51.18%',
'--heroui-danger-500': '339 90% 60.78%',
'--heroui-danger-600': '339.11 90.6% 70.78%',
'--heroui-danger-700': '339.33 90% 80.39%',
'--heroui-danger-800': '340 91.84% 90.39%',
'--heroui-danger-900': '339.13 92% 95.1%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '339.2 90.36% 51.18%',
'--heroui-primary-50': '211.84 100% 9.61%',
'--heroui-primary-100': '211.84 100% 19.22%',
'--heroui-primary-200': '212.24 100% 28.82%',
'--heroui-primary-300': '212.14 100% 38.43%',
'--heroui-primary-400': '212.02 100% 46.67%',
'--heroui-primary-500': '212.14 92.45% 58.43%',
'--heroui-primary-600': '212.24 92.45% 68.82%',
'--heroui-primary-700': '211.84 92.45% 79.22%',
'--heroui-primary-800': '211.84 92.45% 89.61%',
'--heroui-primary-900': '212.5 92.31% 94.9%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '212.02 100% 46.67%',
'--heroui-secondary-50': '270 66.67% 9.41%',
'--heroui-secondary-100': '270 66.67% 18.82%',
'--heroui-secondary-200': '270 66.67% 28.24%',
'--heroui-secondary-300': '270 66.67% 37.65%',
'--heroui-secondary-400': '270 66.67% 47.06%',
'--heroui-secondary-500': '270 59.26% 57.65%',
'--heroui-secondary-600': '270 59.26% 68.24%',
'--heroui-secondary-700': '270 59.26% 78.82%',
'--heroui-secondary-800': '270 59.26% 89.41%',
'--heroui-secondary-900': '270 61.54% 94.9%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 59.26% 57.65%',
'--heroui-success-50': '145.71 77.78% 8.82%',
'--heroui-success-100': '146.2 79.78% 17.45%',
'--heroui-success-200': '145.79 79.26% 26.47%',
'--heroui-success-300': '146.01 79.89% 35.1%',
'--heroui-success-400': '145.96 79.46% 43.92%',
'--heroui-success-500': '146.01 62.45% 55.1%',
'--heroui-success-600': '145.79 62.57% 66.47%',
'--heroui-success-700': '146.2 61.74% 77.45%',
'--heroui-success-800': '145.71 61.4% 88.82%',
'--heroui-success-900': '146.67 64.29% 94.51%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '37.14 75% 10.98%',
'--heroui-warning-100': '37.14 75% 21.96%',
'--heroui-warning-200': '36.96 73.96% 33.14%',
'--heroui-warning-300': '37.01 74.22% 44.12%',
'--heroui-warning-400': '37.03 91.27% 55.1%',
'--heroui-warning-500': '37.01 91.26% 64.12%',
'--heroui-warning-600': '36.96 91.24% 73.14%',
'--heroui-warning-700': '37.14 91.3% 81.96%',
'--heroui-warning-800': '37.14 91.3% 90.98%',
'--heroui-warning-900': '54.55 91.67% 95.29%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '240 5.56% 7.06%',
'--heroui-strong': '190.14 94.67% 44.12%',
'--heroui-code-mdx': '190.14 94.67% 44.12%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-hover-opacity': '.9'
},
light: {
'--heroui-background': '0 0% 100%',
'--heroui-foreground-50': '240 5.88% 95%',
'--heroui-foreground-100': '240 3.7% 90%',
'--heroui-foreground-200': '240 5.26% 80%',
'--heroui-foreground-300': '240 5.2% 70%',
'--heroui-foreground-400': '240 3.83% 60%',
'--heroui-foreground-500': '240 5.03% 50%',
'--heroui-foreground-600': '240 4.88% 40%',
'--heroui-foreground-700': '240 5.88% 30%',
'--heroui-foreground-800': '240 4.76% 20%',
'--heroui-foreground-900': '0 0% 10%',
'--heroui-foreground': '210 5.56% 7.06%',
'--heroui-focus': '212.01999999999998 100% 53.33%',
'--heroui-overlay': '0 0% 100%',
'--heroui-divider': '0 0% 0%',
'--heroui-divider-opacity': '0.85',
'--heroui-content1': '240 5.88% 95%',
'--heroui-content1-foreground': '0 0% 10%',
'--heroui-content2': '240 3.7% 90%',
'--heroui-content2-foreground': '240 4.76% 20%',
'--heroui-content3': '240 5.26% 80%',
'--heroui-content3-foreground': '240 5.88% 30%',
'--heroui-content4': '240 5.2% 70%',
'--heroui-content4-foreground': '240 4.88% 40%',
'--heroui-default-50': '240 5.88% 95%',
'--heroui-default-100': '240 3.7% 90%',
'--heroui-default-200': '240 5.26% 80%',
'--heroui-default-300': '240 5.2% 70%',
'--heroui-default-400': '240 3.83% 60%',
'--heroui-default-500': '240 5.03% 50%',
'--heroui-default-600': '240 4.88% 40%',
'--heroui-default-700': '240 5.88% 30%',
'--heroui-default-800': '240 4.76% 20%',
'--heroui-default-900': '0 0% 10%',
'--heroui-default-foreground': '0 0% 0%',
'--heroui-default': '240 5.26% 80%',
'--heroui-danger-50': '339.13 92% 95.1%',
'--heroui-danger-100': '340 91.84% 90.39%',
'--heroui-danger-200': '339.33 90% 80.39%',
'--heroui-danger-300': '339.11 90.6% 70.78%',
'--heroui-danger-400': '339 90% 60.78%',
'--heroui-danger-500': '339.2 90.36% 51.18%',
'--heroui-danger-600': '339 86.54% 40.78%',
'--heroui-danger-700': '339.11 85.99% 30.78%',
'--heroui-danger-800': '339.33 86.54% 20.39%',
'--heroui-danger-900': '340 84.91% 10.39%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '339.2 90.36% 51.18%',
'--heroui-primary-50': '212.5 92.31% 94.9%',
'--heroui-primary-100': '211.84 92.45% 89.61%',
'--heroui-primary-200': '211.84 92.45% 79.22%',
'--heroui-primary-300': '212.24 92.45% 68.82%',
'--heroui-primary-400': '212.14 92.45% 58.43%',
'--heroui-primary-500': '212.02 100% 46.67%',
'--heroui-primary-600': '212.14 100% 38.43%',
'--heroui-primary-700': '212.24 100% 28.82%',
'--heroui-primary-800': '211.84 100% 19.22%',
'--heroui-primary-900': '211.84 100% 9.61%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '212.02 100% 46.67%',
'--heroui-secondary-50': '270 61.54% 94.9%',
'--heroui-secondary-100': '270 59.26% 89.41%',
'--heroui-secondary-200': '270 59.26% 78.82%',
'--heroui-secondary-300': '270 59.26% 68.24%',
'--heroui-secondary-400': '270 59.26% 57.65%',
'--heroui-secondary-500': '270 66.67% 47.06%',
'--heroui-secondary-600': '270 66.67% 37.65%',
'--heroui-secondary-700': '270 66.67% 28.24%',
'--heroui-secondary-800': '270 66.67% 18.82%',
'--heroui-secondary-900': '270 66.67% 9.41%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 66.67% 47.06%',
'--heroui-success-50': '146.67 64.29% 94.51%',
'--heroui-success-100': '145.71 61.4% 88.82%',
'--heroui-success-200': '146.2 61.74% 77.45%',
'--heroui-success-300': '145.79 62.57% 66.47%',
'--heroui-success-400': '146.01 62.45% 55.1%',
'--heroui-success-500': '145.96 79.46% 43.92%',
'--heroui-success-600': '146.01 79.89% 35.1%',
'--heroui-success-700': '145.79 79.26% 26.47%',
'--heroui-success-800': '146.2 79.78% 17.45%',
'--heroui-success-900': '145.71 77.78% 8.82%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '54.55 91.67% 95.29%',
'--heroui-warning-100': '37.14 91.3% 90.98%',
'--heroui-warning-200': '37.14 91.3% 81.96%',
'--heroui-warning-300': '36.96 91.24% 73.14%',
'--heroui-warning-400': '37.01 91.26% 64.12%',
'--heroui-warning-500': '37.03 91.27% 55.1%',
'--heroui-warning-600': '37.01 74.22% 44.12%',
'--heroui-warning-700': '36.96 73.96% 33.14%',
'--heroui-warning-800': '37.14 75% 21.96%',
'--heroui-warning-900': '37.14 75% 10.98%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '221.25 17.39% 18.04%',
'--heroui-strong': '316.95 100% 65.29%',
'--heroui-code-mdx': '316.95 100% 65.29%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-hover-opacity': '.8'
}
}
export default {
theme,
author: 'HeroUI',
name: 'heroui',
description: 'HeroUI Default Theme'
} satisfies ThemeInfo

View File

@@ -0,0 +1,256 @@
const theme: ThemeConfig = {
dark: {
'--heroui-background': '0 0% 0%',
'--heroui-foreground-50': '240 5.88% 10%',
'--heroui-foreground-100': '240 3.7% 15.88%',
'--heroui-foreground-200': '240 5.26% 26.08%',
'--heroui-foreground-300': '240 5.2% 33.92%',
'--heroui-foreground-400': '240 3.83% 46.08%',
'--heroui-foreground-500': '240 5.03% 64.9%',
'--heroui-foreground-600': '240 4.88% 83.92%',
'--heroui-foreground-700': '240 5.88% 90%',
'--heroui-foreground-800': '240 4.76% 95.88%',
'--heroui-foreground-900': '0 0% 98.04%',
'--heroui-foreground': '210 5.56% 92.94%',
'--heroui-focus': '212.01999999999998 100% 46.67%',
'--heroui-overlay': '0 0% 0%',
'--heroui-divider': '0 0% 100%',
'--heroui-divider-opacity': '0.15',
'--heroui-content1': '240 5.88% 10%',
'--heroui-content1-foreground': '0 0% 98.04%',
'--heroui-content2': '240 3.7% 15.88%',
'--heroui-content2-foreground': '240 4.76% 95.88%',
'--heroui-content3': '240 5.26% 26.08%',
'--heroui-content3-foreground': '240 5.88% 90%',
'--heroui-content4': '240 5.2% 33.92%',
'--heroui-content4-foreground': '240 4.88% 83.92%',
'--heroui-default-50': '240 5.88% 10%',
'--heroui-default-100': '240 3.7% 15.88%',
'--heroui-default-200': '240 5.26% 26.08%',
'--heroui-default-300': '240 5.2% 33.92%',
'--heroui-default-400': '240 3.83% 46.08%',
'--heroui-default-500': '240 5.03% 64.9%',
'--heroui-default-600': '240 4.88% 83.92%',
'--heroui-default-700': '240 5.88% 90%',
'--heroui-default-800': '240 4.76% 95.88%',
'--heroui-default-900': '0 0% 98.04%',
'--heroui-default-foreground': '0 0% 100%',
'--heroui-default': '240 5.26% 26.08%',
'--heroui-danger-50': '301.89 82.61% 22.55%',
'--heroui-danger-100': '308.18 76.39% 28.24%',
'--heroui-danger-200': '313.85 70.65% 36.08%',
'--heroui-danger-300': '319.73 65.64% 44.51%',
'--heroui-danger-400': '325.82 69.62% 53.53%',
'--heroui-danger-500': '331.82 75% 65.49%',
'--heroui-danger-600': '337.84 83.46% 73.92%',
'--heroui-danger-700': '343.42 90.48% 83.53%',
'--heroui-danger-800': '350.53 90.48% 91.76%',
'--heroui-danger-900': '324 90.91% 95.69%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '325.82 69.62% 53.53%',
'--heroui-primary-50': '340 84.91% 10.39%',
'--heroui-primary-100': '339.33 86.54% 20.39%',
'--heroui-primary-200': '339.11 85.99% 30.78%',
'--heroui-primary-300': '339 86.54% 40.78%',
'--heroui-primary-400': '339.2 90.36% 51.18%',
'--heroui-primary-500': '339 90% 60.78%',
'--heroui-primary-600': '339.11 90.6% 70.78%',
'--heroui-primary-700': '339.33 90% 80.39%',
'--heroui-primary-800': '340 91.84% 90.39%',
'--heroui-primary-900': '339.13 92% 95.1%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '339.2 90.36% 51.18%',
'--heroui-secondary-50': '270 66.67% 9.41%',
'--heroui-secondary-100': '270 66.67% 18.82%',
'--heroui-secondary-200': '270 66.67% 28.24%',
'--heroui-secondary-300': '270 66.67% 37.65%',
'--heroui-secondary-400': '270 66.67% 47.06%',
'--heroui-secondary-500': '270 59.26% 57.65%',
'--heroui-secondary-600': '270 59.26% 68.24%',
'--heroui-secondary-700': '270 59.26% 78.82%',
'--heroui-secondary-800': '270 59.26% 89.41%',
'--heroui-secondary-900': '270 61.54% 94.9%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 59.26% 57.65%',
'--heroui-success-50': '145.71 77.78% 8.82%',
'--heroui-success-100': '146.2 79.78% 17.45%',
'--heroui-success-200': '145.79 79.26% 26.47%',
'--heroui-success-300': '146.01 79.89% 35.1%',
'--heroui-success-400': '145.96 79.46% 43.92%',
'--heroui-success-500': '146.01 62.45% 55.1%',
'--heroui-success-600': '145.79 62.57% 66.47%',
'--heroui-success-700': '146.2 61.74% 77.45%',
'--heroui-success-800': '145.71 61.4% 88.82%',
'--heroui-success-900': '146.67 64.29% 94.51%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '37.14 75% 10.98%',
'--heroui-warning-100': '37.14 75% 21.96%',
'--heroui-warning-200': '36.96 73.96% 33.14%',
'--heroui-warning-300': '37.01 74.22% 44.12%',
'--heroui-warning-400': '37.03 91.27% 55.1%',
'--heroui-warning-500': '37.01 91.26% 64.12%',
'--heroui-warning-600': '36.96 91.24% 73.14%',
'--heroui-warning-700': '37.14 91.3% 81.96%',
'--heroui-warning-800': '37.14 91.3% 90.98%',
'--heroui-warning-900': '54.55 91.67% 95.29%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '240 5.56% 7.06%',
'--heroui-strong': '190.14 94.67% 44.12%',
'--heroui-code-mdx': '190.14 94.67% 44.12%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)',
'--heroui-hover-opacity': '.9'
},
light: {
'--heroui-background': '0 0% 100%',
'--heroui-foreground-50': '240 5.88% 95%',
'--heroui-foreground-100': '240 3.7% 90%',
'--heroui-foreground-200': '240 5.26% 80%',
'--heroui-foreground-300': '240 5.2% 70%',
'--heroui-foreground-400': '240 3.83% 60%',
'--heroui-foreground-500': '240 5.03% 50%',
'--heroui-foreground-600': '240 4.88% 40%',
'--heroui-foreground-700': '240 5.88% 30%',
'--heroui-foreground-800': '240 4.76% 20%',
'--heroui-foreground-900': '0 0% 10%',
'--heroui-foreground': '210 5.56% 7.06%',
'--heroui-focus': '212.01999999999998 100% 53.33%',
'--heroui-overlay': '0 0% 100%',
'--heroui-divider': '0 0% 0%',
'--heroui-divider-opacity': '0.85',
'--heroui-content1': '240 5.88% 95%',
'--heroui-content1-foreground': '0 0% 10%',
'--heroui-content2': '240 3.7% 90%',
'--heroui-content2-foreground': '240 4.76% 20%',
'--heroui-content3': '240 5.26% 80%',
'--heroui-content3-foreground': '240 5.88% 30%',
'--heroui-content4': '240 5.2% 70%',
'--heroui-content4-foreground': '240 4.88% 40%',
'--heroui-default-50': '240 5.88% 95%',
'--heroui-default-100': '240 3.7% 90%',
'--heroui-default-200': '240 5.26% 80%',
'--heroui-default-300': '240 5.2% 70%',
'--heroui-default-400': '240 3.83% 60%',
'--heroui-default-500': '240 5.03% 50%',
'--heroui-default-600': '240 4.88% 40%',
'--heroui-default-700': '240 5.88% 30%',
'--heroui-default-800': '240 4.76% 20%',
'--heroui-default-900': '0 0% 10%',
'--heroui-default-foreground': '0 0% 0%',
'--heroui-default': '240 5.26% 80%',
'--heroui-danger-50': '324 90.91% 95.69%',
'--heroui-danger-100': '350.53 90.48% 91.76%',
'--heroui-danger-200': '343.42 90.48% 83.53%',
'--heroui-danger-300': '337.84 83.46% 73.92%',
'--heroui-danger-400': '331.82 75% 65.49%',
'--heroui-danger-500': '325.82 69.62% 53.53%',
'--heroui-danger-600': '319.73 65.64% 44.51%',
'--heroui-danger-700': '313.85 70.65% 36.08%',
'--heroui-danger-800': '308.18 76.39% 28.24%',
'--heroui-danger-900': '301.89 82.61% 22.55%',
'--heroui-danger-foreground': '0 0% 100%',
'--heroui-danger': '325.82 69.62% 53.53%',
'--heroui-primary-50': '339.13 92% 95.1%',
'--heroui-primary-100': '340 91.84% 90.39%',
'--heroui-primary-200': '339.33 90% 80.39%',
'--heroui-primary-300': '339.11 90.6% 70.78%',
'--heroui-primary-400': '339 90% 60.78%',
'--heroui-primary-500': '339.2 90.36% 51.18%',
'--heroui-primary-600': '339 86.54% 40.78%',
'--heroui-primary-700': '339.11 85.99% 30.78%',
'--heroui-primary-800': '339.33 86.54% 20.39%',
'--heroui-primary-900': '340 84.91% 10.39%',
'--heroui-primary-foreground': '0 0% 100%',
'--heroui-primary': '339.2 90.36% 51.18%',
'--heroui-secondary-50': '270 61.54% 94.9%',
'--heroui-secondary-100': '270 59.26% 89.41%',
'--heroui-secondary-200': '270 59.26% 78.82%',
'--heroui-secondary-300': '270 59.26% 68.24%',
'--heroui-secondary-400': '270 59.26% 57.65%',
'--heroui-secondary-500': '270 66.67% 47.06%',
'--heroui-secondary-600': '270 66.67% 37.65%',
'--heroui-secondary-700': '270 66.67% 28.24%',
'--heroui-secondary-800': '270 66.67% 18.82%',
'--heroui-secondary-900': '270 66.67% 9.41%',
'--heroui-secondary-foreground': '0 0% 100%',
'--heroui-secondary': '270 66.67% 47.06%',
'--heroui-success-50': '146.67 64.29% 94.51%',
'--heroui-success-100': '145.71 61.4% 88.82%',
'--heroui-success-200': '146.2 61.74% 77.45%',
'--heroui-success-300': '145.79 62.57% 66.47%',
'--heroui-success-400': '146.01 62.45% 55.1%',
'--heroui-success-500': '145.96 79.46% 43.92%',
'--heroui-success-600': '146.01 79.89% 35.1%',
'--heroui-success-700': '145.79 79.26% 26.47%',
'--heroui-success-800': '146.2 79.78% 17.45%',
'--heroui-success-900': '145.71 77.78% 8.82%',
'--heroui-success-foreground': '0 0% 0%',
'--heroui-success': '145.96 79.46% 43.92%',
'--heroui-warning-50': '54.55 91.67% 95.29%',
'--heroui-warning-100': '37.14 91.3% 90.98%',
'--heroui-warning-200': '37.14 91.3% 81.96%',
'--heroui-warning-300': '36.96 91.24% 73.14%',
'--heroui-warning-400': '37.01 91.26% 64.12%',
'--heroui-warning-500': '37.03 91.27% 55.1%',
'--heroui-warning-600': '37.01 74.22% 44.12%',
'--heroui-warning-700': '36.96 73.96% 33.14%',
'--heroui-warning-800': '37.14 75% 21.96%',
'--heroui-warning-900': '37.14 75% 10.98%',
'--heroui-warning-foreground': '0 0% 0%',
'--heroui-warning': '37.03 91.27% 55.1%',
'--heroui-code-background': '221.25 17.39% 18.04%',
'--heroui-strong': '316.95 100% 65.29%',
'--heroui-code-mdx': '316.95 100% 65.29%',
'--heroui-divider-weight': '1px',
'--heroui-disabled-opacity': '.5',
'--heroui-font-size-tiny': '0.75rem',
'--heroui-font-size-small': '0.875rem',
'--heroui-font-size-medium': '1rem',
'--heroui-font-size-large': '1.125rem',
'--heroui-line-height-tiny': '1rem',
'--heroui-line-height-small': '1.25rem',
'--heroui-line-height-medium': '1.5rem',
'--heroui-line-height-large': '1.75rem',
'--heroui-radius-small': '8px',
'--heroui-radius-medium': '12px',
'--heroui-radius-large': '14px',
'--heroui-border-width-small': '1px',
'--heroui-border-width-medium': '2px',
'--heroui-border-width-large': '3px',
'--heroui-box-shadow-small':
'0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-medium':
'0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-box-shadow-large':
'0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)',
'--heroui-hover-opacity': '.8'
}
}
export default {
theme,
author: 'NapCat',
name: 'nc_pink',
description: 'NapCat Pink Theme'
} satisfies ThemeInfo

View File

@@ -73,4 +73,17 @@ export default class QQManager {
) )
return data.data.data return data.data.data
} }
public static async getQuickLoginQQ() {
const { data } = await serverRequest.post<ServerResponse<string>>(
'/QQLogin/GetQuickLoginQQ'
)
return data.data
}
public static async setQuickLoginQQ(uin: string) {
await serverRequest.post<ServerResponse<null>>('/QQLogin/SetQuickLoginQQ', {
uin
})
}
} }

View File

@@ -9,14 +9,6 @@ export interface Log {
message: string message: string
} }
export interface TerminalSession {
id: string
}
export interface TerminalInfo {
id: string
}
export default class WebUIManager { export default class WebUIManager {
public static async checkWebUiLogined() { public static async checkWebUiLogined() {
const { data } = const { data } =
@@ -40,6 +32,13 @@ export default class WebUIManager {
return data.data return data.data
} }
public static async checkUsingDefaultToken() {
const { data } = await serverRequest.get<ServerResponse<boolean>>(
'/auth/check_using_default_token'
)
return data.data
}
public static async proxy<T>(url = '') { public static async proxy<T>(url = '') {
const data = await serverRequest.get<ServerResponse<string>>( const data = await serverRequest.get<ServerResponse<string>>(
'/base/proxy?url=' + encodeURIComponent(url) '/base/proxy?url=' + encodeURIComponent(url)
@@ -60,6 +59,20 @@ export default class WebUIManager {
return data.data return data.data
} }
public static async getThemeConfig() {
const { data } =
await serverRequest.get<ServerResponse<ThemeConfig>>('/base/Theme')
return data.data
}
public static async setThemeConfig(theme: ThemeConfig) {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/base/SetTheme',
{ theme }
)
return data.data
}
public static async getLogList() { public static async getLogList() {
const { data } = const { data } =
await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList') await serverRequest.get<ServerResponse<string[]>>('/Log/GetLogList')

View File

@@ -8,6 +8,7 @@ import '@/styles/globals.css'
import key from './const/key' import key from './const/key'
import WebUIManager from './controllers/webui_manager' import WebUIManager from './controllers/webui_manager'
import { loadTheme } from './utils/theme'
WebUIManager.checkWebUiLogined() WebUIManager.checkWebUiLogined()
@@ -22,6 +23,8 @@ if (theme && !theme.startsWith('"')) {
localStorage.setItem(key.theme, JSON.stringify(theme)) localStorage.setItem(key.theme, JSON.stringify(theme))
} }
loadTheme()
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
// <React.StrictMode> // <React.StrictMode>
<BrowserRouter basename="/webui/"> <BrowserRouter basename="/webui/">

View File

@@ -1,20 +1,36 @@
import { Card, CardBody } from '@heroui/card' import { Card, CardBody } from '@heroui/card'
import { Tab, Tabs } from '@heroui/tabs' import { Tab, Tabs } from '@heroui/tabs'
import clsx from 'clsx'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { useNavigate, useSearchParams } from 'react-router-dom'
import ChangePasswordCard from './change_password' import ChangePasswordCard from './change_password'
import LoginConfigCard from './login'
import OneBotConfigCard from './onebot' import OneBotConfigCard from './onebot'
import ThemeConfigCard from './theme'
import WebUIConfigCard from './webui' import WebUIConfigCard from './webui'
export interface ConfigPageProps { export interface ConfigPageProps {
children?: React.ReactNode children?: React.ReactNode
size?: 'sm' | 'md' | 'lg'
} }
const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => { const ConfingPageItem: React.FC<ConfigPageProps> = ({
children,
size = 'md'
}) => {
return ( return (
<Card className="bg-opacity-50 backdrop-blur-sm"> <Card className="bg-opacity-50 backdrop-blur-sm">
<CardBody className="items-center py-5"> <CardBody className="items-center py-5">
<div className="w-96 max-w-full flex flex-col gap-2">{children}</div> <div
className={clsx('max-w-full flex flex-col gap-2', {
'w-72': size === 'sm',
'w-96': size === 'md',
'w-[32rem]': size === 'lg'
})}
>
{children}
</div>
</CardBody> </CardBody>
</Card> </Card>
) )
@@ -22,6 +38,11 @@ const ConfingPageItem: React.FC<ConfigPageProps> = ({ children }) => {
export default function ConfigPage() { export default function ConfigPage() {
const isMediumUp = useMediaQuery({ minWidth: 768 }) const isMediumUp = useMediaQuery({ minWidth: 768 })
const navigate = useNavigate()
const search = useSearchParams({
tab: 'onebot'
})[0]
const tab = search.get('tab') ?? 'onebot'
return ( return (
<section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10"> <section className="w-[1000px] max-w-full md:mx-auto gap-4 py-8 px-2 md:py-10">
@@ -30,6 +51,10 @@ export default function ConfigPage() {
fullWidth fullWidth
className="w-full" className="w-full"
isVertical={isMediumUp} isVertical={isMediumUp}
selectedKey={tab}
onSelectionChange={(key) => {
navigate(`/config?tab=${key}`)
}}
classNames={{ classNames={{
tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm', tabList: 'sticky flex top-14 bg-opacity-50 backdrop-blur-sm',
panel: 'w-full relative', panel: 'w-full relative',
@@ -47,12 +72,22 @@ export default function ConfigPage() {
<WebUIConfigCard /> <WebUIConfigCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="登录配置" key="login">
<ConfingPageItem>
<LoginConfigCard />
</ConfingPageItem>
</Tab>
<Tab title="修改密码" key="token"> <Tab title="修改密码" key="token">
<ConfingPageItem> <ConfingPageItem>
<ChangePasswordCard /> <ChangePasswordCard />
</ConfingPageItem> </ConfingPageItem>
</Tab> </Tab>
<Tab title="主题配置" key="theme">
<ConfingPageItem size="lg">
<ThemeConfigCard />
</ConfingPageItem>
</Tab>
</Tabs> </Tabs>
</section> </section>
) )

View File

@@ -0,0 +1,89 @@
import { Input } from '@heroui/input'
import { useRequest } from 'ahooks'
import { useEffect } from 'react'
import { Controller, useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import QQManager from '@/controllers/qq_manager'
const LoginConfigCard = () => {
const {
data: quickLoginData,
loading: quickLoginLoading,
error: quickLoginError,
refreshAsync: refreshQuickLogin
} = useRequest(QQManager.getQuickLoginQQ)
const {
control,
handleSubmit: handleOnebotSubmit,
formState: { isSubmitting },
setValue: setOnebotValue
} = useForm<{
quickLoginQQ: string
}>({
defaultValues: {
quickLoginQQ: ''
}
})
const reset = () => {
setOnebotValue('quickLoginQQ', quickLoginData ?? '')
}
const onSubmit = handleOnebotSubmit(async (data) => {
try {
await QQManager.setQuickLoginQQ(data.quickLoginQQ)
toast.success('保存成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshQuickLogin()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
reset()
}, [quickLoginData])
if (quickLoginLoading) return <PageLoading loading={true} />
return (
<>
<title>OneBot配置 - NapCat WebUI</title>
<div className="flex-shrink-0 w-full">QQ</div>
<Controller
control={control}
name="quickLoginQQ"
render={({ field }) => (
<Input
{...field}
label="快速登录QQ"
placeholder="请输入QQ号"
isDisabled={!!quickLoginError}
errorMessage={quickLoginError ? '获取快速登录QQ失败' : undefined}
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting || quickLoginLoading}
refresh={onRefresh}
/>
</>
)
}
export default LoginConfigCard

View File

@@ -30,9 +30,9 @@ const OneBotConfigCard = () => {
setOnebotValue('parseMultMsg', config.parseMultMsg) setOnebotValue('parseMultMsg', config.parseMultMsg)
} }
const onSubmit = handleOnebotSubmit((data) => { const onSubmit = handleOnebotSubmit(async (data) => {
try { try {
saveConfigWithoutNetwork(data) await saveConfigWithoutNetwork(data)
toast.success('保存成功') toast.success('保存成功')
} catch (error) { } catch (error) {
const msg = (error as Error).message const msg = (error as Error).message

View File

@@ -0,0 +1,279 @@
import { Accordion, AccordionItem } from '@heroui/accordion'
import { Card, CardBody, CardHeader } from '@heroui/card'
import { useRequest } from 'ahooks'
import clsx from 'clsx'
import { useEffect, useRef } from 'react'
import { Controller, useForm, useWatch } from 'react-hook-form'
import toast from 'react-hot-toast'
import { FaUserAstronaut } from 'react-icons/fa'
import { FaPaintbrush } from 'react-icons/fa6'
import { IoIosColorPalette } from 'react-icons/io'
import { MdDarkMode, MdLightMode } from 'react-icons/md'
import themes from '@/const/themes'
import ColorPicker from '@/components/ColorPicker'
import SaveButtons from '@/components/button/save_buttons'
import PageLoading from '@/components/page_loading'
import { colorKeys, generateTheme, loadTheme } from '@/utils/theme'
import WebUIManager from '@/controllers/webui_manager'
export type PreviewThemeCardProps = {
theme: ThemeInfo
onPreview: () => void
}
const values = [
'',
'-50',
'-100',
'-200',
'-300',
'-400',
'-500',
'-600',
'-700',
'-800',
'-900'
]
const colors = [
'primary',
'secondary',
'success',
'danger',
'warning',
'default'
]
function PreviewThemeCard({ theme, onPreview }: PreviewThemeCardProps) {
const style = document.createElement('style')
style.innerHTML = generateTheme(theme.theme, theme.name)
const cardRef = useRef<HTMLDivElement>(null)
useEffect(() => {
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
}, [])
return (
<Card
ref={cardRef}
shadow="sm"
radius="sm"
isPressable
onPress={onPreview}
className={clsx('text-primary bg-primary-50', theme.name)}
>
<CardHeader className="pb-0 flex flex-col items-start gap-1">
<div className="px-1 rounded-md bg-primary text-primary-foreground">
{theme.name}
</div>
<div className="text-xs flex items-center gap-1 text-primary-300">
<FaUserAstronaut />
{theme.author ?? '未知'}
</div>
<div className="text-xs text-primary-200">{theme.description}</div>
</CardHeader>
<CardBody>
<div className="flex flex-col gap-1">
{colors.map((color) => (
<div className="flex gap-1 items-center flex-wrap" key={color}>
<div className="text-xs w-4 text-right">
{color[0].toUpperCase()}
</div>
{values.map((value) => (
<div
key={value}
className={clsx(
'w-2 h-2 rounded-full shadow-small',
`bg-${color}${value}`
)}
></div>
))}
</div>
))}
</div>
</CardBody>
</Card>
)
}
const ThemeConfigCard = () => {
const { data, loading, error, refreshAsync } = useRequest(
WebUIManager.getThemeConfig
)
const {
control,
handleSubmit: handleOnebotSubmit,
formState: { isSubmitting },
setValue: setOnebotValue
} = useForm<{
theme: ThemeConfig
}>({
defaultValues: {
theme: {
dark: {},
light: {}
}
}
})
// 使用 useRef 存储 style 标签引用
const styleTagRef = useRef<HTMLStyleElement | null>(null)
// 在组件挂载时创建 style 标签,并在卸载时清理
useEffect(() => {
const styleTag = document.createElement('style')
document.head.appendChild(styleTag)
styleTagRef.current = styleTag
return () => {
if (styleTagRef.current) {
document.head.removeChild(styleTagRef.current)
}
}
}, [])
const theme = useWatch({ control, name: 'theme' })
const reset = () => {
if (data) setOnebotValue('theme', data)
}
const onSubmit = handleOnebotSubmit(async (data) => {
try {
await WebUIManager.setThemeConfig(data.theme)
toast.success('保存成功')
loadTheme()
} catch (error) {
const msg = (error as Error).message
toast.error(`保存失败: ${msg}`)
}
})
const onRefresh = async () => {
try {
await refreshAsync()
toast.success('刷新成功')
} catch (error) {
const msg = (error as Error).message
toast.error(`刷新失败: ${msg}`)
}
}
useEffect(() => {
reset()
}, [data])
useEffect(() => {
if (theme && styleTagRef.current) {
const css = generateTheme(theme)
styleTagRef.current.innerHTML = css
}
}, [theme])
if (loading) return <PageLoading loading={true} />
if (error)
return (
<div className="py-24 text-danger-500 text-center">{error.message}</div>
)
return (
<>
<title> - NapCat WebUI</title>
<SaveButtons
onSubmit={onSubmit}
reset={reset}
isSubmitting={isSubmitting}
refresh={onRefresh}
className="items-end w-full p-4"
/>
<div className="px-4 text-sm text-default-600"></div>
<Accordion variant="splitted" defaultExpandedKeys={['select']}>
<AccordionItem
key="select"
aria-label="Pick Color"
title="选择主题"
subtitle="可以切换夜间/白昼模式查看对应颜色"
className="shadow-small"
startContent={<IoIosColorPalette />}
>
<div className="flex flex-wrap gap-2">
{themes.map((theme) => (
<PreviewThemeCard
key={theme.name}
theme={theme}
onPreview={() => {
setOnebotValue('theme', theme.theme)
}}
/>
))}
</div>
</AccordionItem>
<AccordionItem
key="pick"
aria-label="Pick Color"
title="自定义配色"
className="shadow-small"
startContent={<FaPaintbrush />}
>
<div className="space-y-2">
{(['dark', 'light'] as const).map((mode) => (
<div
key={mode}
className={clsx(
'p-2 rounded-md',
mode === 'dark' ? 'text-white' : 'text-black',
mode === 'dark'
? 'bg-content1-foreground dark:bg-content1'
: 'bg-content1 dark:bg-content1-foreground'
)}
>
<h3 className="text-center p-2 rounded-md bg-content2 mb-2 text-default-800 flex items-center justify-center">
{mode === 'dark' ? (
<MdDarkMode size={24} />
) : (
<MdLightMode size={24} />
)}
{mode === 'dark' ? '夜间模式主题' : '白昼模式主题'}
</h3>
{colorKeys.map((key) => (
<div
key={key}
className="grid grid-cols-2 items-center mb-2 gap-2"
>
<label className="text-right">{key}</label>
<Controller
control={control}
name={`theme.${mode}.${key}`}
render={({ field: { value, onChange } }) => {
const hslArray = value?.split(' ') ?? [0, 0, 0]
const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`
return (
<ColorPicker
color={color}
onChange={(result) => {
onChange(
`${result.hsl.h} ${result.hsl.s * 100}% ${result.hsl.l * 100}%`
)
}}
/>
)
}}
/>
</div>
))}
</div>
))}
</div>
</AccordionItem>
</Accordion>
</>
)
}
export default ThemeConfigCard

View File

@@ -81,6 +81,10 @@ const WebUIConfigCard = () => {
onDelete={async () => { onDelete={async () => {
try { try {
await FileManager.deleteWebUIFont() await FileManager.deleteWebUIFont()
toast.success('删除成功')
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) { } catch (error) {
toast.error('删除失败: ' + (error as Error).message) toast.error('删除失败: ' + (error as Error).message)
} }

View File

@@ -105,7 +105,7 @@ const DashboardIndexPage: React.FC = () => {
<SystemStatusCard setArchInfo={setArchInfo} /> <SystemStatusCard setArchInfo={setArchInfo} />
</div> </div>
<Networks /> <Networks />
<Card className="bg-opacity-60 shadow-sm shadow-primary-50"> <Card className="bg-opacity-60 shadow-sm shadow-primary-100">
<CardBody> <CardBody>
<Hitokoto /> <Hitokoto />
</CardBody> </CardBody>

View File

@@ -1,14 +1,46 @@
import { Spinner } from '@heroui/spinner' import { Spinner } from '@heroui/spinner'
import { AnimatePresence, motion } from 'motion/react' import { AnimatePresence, motion } from 'motion/react'
import { Suspense } from 'react' import { Suspense, useEffect } from 'react'
import { Outlet, useLocation } from 'react-router-dom' import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import useAuth from '@/hooks/auth'
import useDialog from '@/hooks/use-dialog'
import WebUIManager from '@/controllers/webui_manager'
import DefaultLayout from '@/layouts/default' import DefaultLayout from '@/layouts/default'
const CheckDefaultPassword = () => {
const { isAuth } = useAuth()
const dialog = useDialog()
const navigate = useNavigate()
const checkDefaultPassword = async () => {
const data = await WebUIManager.checkUsingDefaultToken()
if (data) {
dialog.confirm({
title: '修改默认密码',
content: '检测到当前密码为默认密码,请尽快修改密码。',
confirmText: '前往修改',
onConfirm: () => {
navigate('/config?tab=token')
}
})
}
}
useEffect(() => {
if (isAuth) {
checkDefaultPassword()
}
}, [isAuth])
return null
}
export default function IndexPage() { export default function IndexPage() {
const location = useLocation() const location = useLocation()
return ( return (
<DefaultLayout> <DefaultLayout>
<CheckDefaultPassword />
<Suspense <Suspense
fallback={ fallback={
<div className="flex justify-center px-10"> <div className="flex justify-center px-10">

View File

@@ -1,6 +1,6 @@
@layer base { @layer base {
.shiny-text { .shiny-text {
@apply text-pink-400 text-opacity-60; @apply text-primary-400 text-opacity-60;
background-size: 200% 100%; background-size: 200% 100%;
-webkit-background-clip: text; -webkit-background-clip: text;
background-clip: text; background-clip: text;
@@ -10,7 +10,7 @@
background-image: linear-gradient( background-image: linear-gradient(
120deg, 120deg,
rgba(255, 50, 50, 0) 40%, rgba(255, 50, 50, 0) 40%,
rgba(255, 76, 76, 0.8) 50%, hsl(var(--heroui-primary-400) / 0.8) 50%,
rgba(255, 50, 50, 0) 60% rgba(255, 50, 50, 0) 60%
); );
} }
@@ -18,11 +18,10 @@
background-image: linear-gradient( background-image: linear-gradient(
120deg, 120deg,
rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 40%,
rgba(206, 21, 21, 0.8) 50%, hsl(var(--heroui-primary-600) / 0.8) 50%,
rgba(255, 255, 255, 0) 60% rgba(255, 255, 255, 0) 60%
); );
} }
@keyframes shine { @keyframes shine {
0% { 0% {
background-position: 100%; background-position: 100%;

View File

@@ -48,3 +48,136 @@ interface SystemStatus {
} }
arch: string arch: string
} }
interface ThemeConfigItem {
'--heroui-background': string
'--heroui-foreground-50': string
'--heroui-foreground-100': string
'--heroui-foreground-200': string
'--heroui-foreground-300': string
'--heroui-foreground-400': string
'--heroui-foreground-500': string
'--heroui-foreground-600': string
'--heroui-foreground-700': string
'--heroui-foreground-800': string
'--heroui-foreground-900': string
'--heroui-foreground': string
'--heroui-focus': string
'--heroui-overlay': string
'--heroui-divider': string
'--heroui-divider-opacity': string
'--heroui-content1': string
'--heroui-content1-foreground': string
'--heroui-content2': string
'--heroui-content2-foreground': string
'--heroui-content3': string
'--heroui-content3-foreground': string
'--heroui-content4': string
'--heroui-content4-foreground': string
'--heroui-default-50': string
'--heroui-default-100': string
'--heroui-default-200': string
'--heroui-default-300': string
'--heroui-default-400': string
'--heroui-default-500': string
'--heroui-default-600': string
'--heroui-default-700': string
'--heroui-default-800': string
'--heroui-default-900': string
'--heroui-default-foreground': string
'--heroui-default': string
// 新增 danger
'--heroui-danger-50': string
'--heroui-danger-100': string
'--heroui-danger-200': string
'--heroui-danger-300': string
'--heroui-danger-400': string
'--heroui-danger-500': string
'--heroui-danger-600': string
'--heroui-danger-700': string
'--heroui-danger-800': string
'--heroui-danger-900': string
'--heroui-danger-foreground': string
'--heroui-danger': string
// 新增 primary
'--heroui-primary-50': string
'--heroui-primary-100': string
'--heroui-primary-200': string
'--heroui-primary-300': string
'--heroui-primary-400': string
'--heroui-primary-500': string
'--heroui-primary-600': string
'--heroui-primary-700': string
'--heroui-primary-800': string
'--heroui-primary-900': string
'--heroui-primary-foreground': string
'--heroui-primary': string
// 新增 secondary
'--heroui-secondary-50': string
'--heroui-secondary-100': string
'--heroui-secondary-200': string
'--heroui-secondary-300': string
'--heroui-secondary-400': string
'--heroui-secondary-500': string
'--heroui-secondary-600': string
'--heroui-secondary-700': string
'--heroui-secondary-800': string
'--heroui-secondary-900': string
'--heroui-secondary-foreground': string
'--heroui-secondary': string
// 新增 success
'--heroui-success-50': string
'--heroui-success-100': string
'--heroui-success-200': string
'--heroui-success-300': string
'--heroui-success-400': string
'--heroui-success-500': string
'--heroui-success-600': string
'--heroui-success-700': string
'--heroui-success-800': string
'--heroui-success-900': string
'--heroui-success-foreground': string
'--heroui-success': string
// 新增 warning
'--heroui-warning-50': string
'--heroui-warning-100': string
'--heroui-warning-200': string
'--heroui-warning-300': string
'--heroui-warning-400': string
'--heroui-warning-500': string
'--heroui-warning-600': string
'--heroui-warning-700': string
'--heroui-warning-800': string
'--heroui-warning-900': string
'--heroui-warning-foreground': string
'--heroui-warning': string
// 其它配置
'--heroui-code-background': string
'--heroui-strong': string
'--heroui-code-mdx': string
'--heroui-divider-weight': string
'--heroui-disabled-opacity': string
'--heroui-font-size-tiny': string
'--heroui-font-size-small': string
'--heroui-font-size-medium': string
'--heroui-font-size-large': string
'--heroui-line-height-tiny': string
'--heroui-line-height-small': string
'--heroui-line-height-medium': string
'--heroui-line-height-large': string
'--heroui-radius-small': string
'--heroui-radius-medium': string
'--heroui-radius-large': string
'--heroui-border-width-small': string
'--heroui-border-width-medium': string
'--heroui-border-width-large': string
'--heroui-box-shadow-small': string
'--heroui-box-shadow-medium': string
'--heroui-box-shadow-large': string
'--heroui-hover-opacity': string
}
interface ThemeConfig {
dark: ThemeConfigItem
light: ThemeConfigItem
}

6
napcat.webui/src/types/theme.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
interface ThemeInfo {
theme: ThemeConfig
name: string
description?: string
author?: string
}

View File

@@ -1,19 +1,21 @@
import { PlayMode } from '@/const/enum' import { PlayMode } from '@/const/enum'
import WebUIManager from '@/controllers/webui_manager'
import type { import type {
FinalMusic, FinalMusic,
Music163ListResponse, Music163ListResponse,
Music163URLResponse Music163URLResponse
} from '@/types/music' } from '@/types/music'
import WebUIManager from '@/controllers/webui_manager'
/** /**
* 获取网易云音乐歌单 * 获取网易云音乐歌单
* @param id 歌单id * @param id 歌单id
* @returns 歌单信息 * @returns 歌单信息
*/ */
export const get163MusicList = async (id: string) => { export const get163MusicList = async (id: string) => {
let res = await WebUIManager.proxy<Music163ListResponse>('https://wavesgame.top/playlist/track/all?id=' + id); let res = await WebUIManager.proxy<Music163ListResponse>(
'https://wavesgame.top/playlist/track/all?id=' + id
)
// const res = await request.get<Music163ListResponse>( // const res = await request.get<Music163ListResponse>(
// `https://wavesgame.top/playlist/track/all?id=${id}` // `https://wavesgame.top/playlist/track/all?id=${id}`
// ) // )
@@ -71,7 +73,7 @@ export const get163MusicListSongs = async (id: string) => {
if (songURL) { if (songURL) {
finalMusic.push({ finalMusic.push({
id: song.id, id: song.id,
url: songURL, url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
title: song.name, title: song.name,
artist: song.ar.map((p) => p.name).join('/'), artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl cover: song.al.picUrl

View File

@@ -0,0 +1,141 @@
import { request } from './request'
const style = document.createElement('style')
document.head.appendChild(style)
export function loadTheme() {
request('/files/theme.css?_t=' + Date.now())
.then((res) => res.data)
.then((css) => {
style.innerHTML = css
})
.catch(() => {
console.error('Failed to load theme.css')
})
}
export const colorKeys = [
'--heroui-background',
'--heroui-foreground-50',
'--heroui-foreground-100',
'--heroui-foreground-200',
'--heroui-foreground-300',
'--heroui-foreground-400',
'--heroui-foreground-500',
'--heroui-foreground-600',
'--heroui-foreground-700',
'--heroui-foreground-800',
'--heroui-foreground-900',
'--heroui-foreground',
'--heroui-content1',
'--heroui-content1-foreground',
'--heroui-content2',
'--heroui-content2-foreground',
'--heroui-content3',
'--heroui-content3-foreground',
'--heroui-content4',
'--heroui-content4-foreground',
'--heroui-default-50',
'--heroui-default-100',
'--heroui-default-200',
'--heroui-default-300',
'--heroui-default-400',
'--heroui-default-500',
'--heroui-default-600',
'--heroui-default-700',
'--heroui-default-800',
'--heroui-default-900',
'--heroui-default-foreground',
'--heroui-default',
'--heroui-danger-50',
'--heroui-danger-100',
'--heroui-danger-200',
'--heroui-danger-300',
'--heroui-danger-400',
'--heroui-danger-500',
'--heroui-danger-600',
'--heroui-danger-700',
'--heroui-danger-800',
'--heroui-danger-900',
'--heroui-danger-foreground',
'--heroui-danger',
'--heroui-primary-50',
'--heroui-primary-100',
'--heroui-primary-200',
'--heroui-primary-300',
'--heroui-primary-400',
'--heroui-primary-500',
'--heroui-primary-600',
'--heroui-primary-700',
'--heroui-primary-800',
'--heroui-primary-900',
'--heroui-primary-foreground',
'--heroui-primary',
'--heroui-secondary-50',
'--heroui-secondary-100',
'--heroui-secondary-200',
'--heroui-secondary-300',
'--heroui-secondary-400',
'--heroui-secondary-500',
'--heroui-secondary-600',
'--heroui-secondary-700',
'--heroui-secondary-800',
'--heroui-secondary-900',
'--heroui-secondary-foreground',
'--heroui-secondary',
'--heroui-success-50',
'--heroui-success-100',
'--heroui-success-200',
'--heroui-success-300',
'--heroui-success-400',
'--heroui-success-500',
'--heroui-success-600',
'--heroui-success-700',
'--heroui-success-800',
'--heroui-success-900',
'--heroui-success-foreground',
'--heroui-success',
'--heroui-warning-50',
'--heroui-warning-100',
'--heroui-warning-200',
'--heroui-warning-300',
'--heroui-warning-400',
'--heroui-warning-500',
'--heroui-warning-600',
'--heroui-warning-700',
'--heroui-warning-800',
'--heroui-warning-900',
'--heroui-warning-foreground',
'--heroui-warning',
'--heroui-focus',
'--heroui-overlay',
'--heroui-divider',
'--heroui-code-background',
'--heroui-strong',
'--heroui-code-mdx'
] as const
export const generateTheme = (theme: ThemeConfig, validField?: string) => {
let css = `:root ${validField ? `.${validField}` : ''}, .light ${validField ? `.${validField}` : ''}, [data-theme="light"] ${validField ? `.${validField}` : ''} {`
for (const key in theme.light) {
const _key = key as keyof ThemeConfigItem
css += `${_key}: ${theme.light[_key]};`
}
css += `}`
css += `.dark ${validField ? `.${validField}` : ''}, [data-theme="dark"] ${validField ? `.${validField}` : ''} {`
for (const key in theme.dark) {
const _key = key as keyof ThemeConfigItem
css += `${_key}: ${theme.dark[_key]};`
}
css += `}`
return css
}

View File

@@ -9,6 +9,12 @@ export default {
'./src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}' './node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'
], ],
safelist: [
{
pattern:
/bg-(primary|secondary|success|danger|warning|default)-(50|100|200|300|400|500|600|700|800|900)/
}
],
theme: { theme: {
extend: {} extend: {}
}, },

View File

@@ -34,7 +34,8 @@ export default defineConfig(({ mode }) => {
ws: true, ws: true,
changeOrigin: true changeOrigin: true
}, },
'/api': backendDebugUrl '/api': backendDebugUrl,
'/files': backendDebugUrl
} }
}, },
build: { build: {

View File

@@ -2,7 +2,7 @@
"name": "napcat", "name": "napcat",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "4.5.12", "version": "4.6.9",
"scripts": { "scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1", "build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1", "build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -32,7 +32,10 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"@types/node": "^22.0.1", "@types/node": "^22.0.1",
"@types/on-finished": "^2.3.4",
"@types/qrcode-terminal": "^0.12.2", "@types/qrcode-terminal": "^0.12.2",
"@types/react-color": "^3.0.13",
"@types/type-is": "^1.6.7",
"@types/ws": "^8.5.12", "@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0", "@typescript-eslint/parser": "^8.3.0",
@@ -40,14 +43,14 @@
"async-mutex": "^0.5.0", "async-mutex": "^0.5.0",
"commander": "^13.0.0", "commander": "^13.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"esbuild": "0.24.0", "esbuild": "0.25.0",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0", "express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6", "fast-xml-parser": "^4.3.6",
"file-type": "^20.0.0", "file-type": "^20.0.0",
"globals": "^15.12.0", "globals": "^16.0.0",
"image-size": "^1.1.1", "image-size": "^1.1.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
@@ -56,13 +59,13 @@
"vite": "^6.0.1", "vite": "^6.0.1",
"vite-plugin-cp": "^4.0.8", "vite-plugin-cp": "^4.0.8",
"vite-tsconfig-paths": "^5.1.0", "vite-tsconfig-paths": "^5.1.0",
"winston": "^3.17.0" "napcat.protobuf": "^1.1.3",
"winston": "^3.17.0",
"compressing": "^1.10.1"
}, },
"dependencies": { "dependencies": {
"@ffmpeg.wasm/core-mt": "^0.13.2", "@ffmpeg.wasm/core-mt": "^0.13.2",
"compressing": "^1.10.1",
"express": "^5.0.0", "express": "^5.0.0",
"piscina": "^4.7.0",
"silk-wasm": "^3.6.1", "silk-wasm": "^3.6.1",
"ws": "^8.18.0" "ws": "^8.18.0"
} }

View File

@@ -1,9 +1,20 @@
import { encode } from 'silk-wasm'; import { encode } from 'silk-wasm';
import { parentPort } from 'worker_threads';
export interface EncodeArgs { export interface EncodeArgs {
input: ArrayBufferView | ArrayBuffer input: ArrayBufferView | ArrayBuffer
sampleRate: number sampleRate: number
} }
export default async ({ input, sampleRate }: EncodeArgs) => { export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
recvTask<EncodeArgs>(async ({ input, sampleRate }) => {
return await encode(input, sampleRate); return await encode(input, sampleRate);
}; });

View File

@@ -1,4 +1,3 @@
import Piscina from 'piscina';
import fsPromise from 'fs/promises'; import fsPromise from 'fs/promises';
import path from 'node:path'; import path from 'node:path';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
@@ -6,16 +5,16 @@ import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-w
import { LogWrapper } from '@/common/log'; import { LogWrapper } from '@/common/log';
import { EncodeArgs } from '@/common/audio-worker'; import { EncodeArgs } from '@/common/audio-worker';
import { FFmpegService } from '@/common/ffmpeg'; import { FFmpegService } from '@/common/ffmpeg';
import { runTask } from './worker';
import { fileURLToPath } from 'node:url';
const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000]; const ALLOW_SAMPLE_RATE = [8000, 12000, 16000, 24000, 32000, 44100, 48000];
async function getWorkerPath() { function getWorkerPath() {
return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href; //return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
return path.join(path.dirname(fileURLToPath(import.meta.url)), 'audio-worker.mjs');
} }
const piscina = new Piscina<EncodeArgs, EncodeResult>({
filename: await getWorkerPath(),
});
async function guessDuration(pttPath: string, logger: LogWrapper) { async function guessDuration(pttPath: string, logger: LogWrapper) {
const pttFileInfo = await fsPromise.stat(pttPath); const pttFileInfo = await fsPromise.stat(pttPath);
@@ -46,7 +45,7 @@ export async function encodeSilk(filePath: string, TEMP_DIR: string, logger: Log
const { input, sampleRate } = isWav(file) const { input, sampleRate } = isWav(file)
? await handleWavFile(file, filePath, pcmPath) ? await handleWavFile(file, filePath, pcmPath)
: { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 }; : { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
const silk = await piscina.run({ input: input, sampleRate: sampleRate }); const silk = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { input: input, sampleRate: sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e)); fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data)); await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration); logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);

View File

@@ -5,6 +5,17 @@ import { readFileSync, statSync, writeFileSync } from 'fs';
import type { VideoInfo } from './video'; import type { VideoInfo } from './video';
import { fileTypeFromFile } from 'file-type'; import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size'; import imageSize from 'image-size';
import { parentPort } from 'worker_threads';
export function recvTask<T>(cb: (taskData: T) => Promise<unknown>) {
parentPort?.on('message', async (taskData: T) => {
try {
let ret = await cb(taskData);
parentPort?.postMessage(ret);
} catch (error: unknown) {
parentPort?.postMessage({ error: (error as Error).message });
}
});
}
class FFmpegService { class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' }); const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
@@ -137,15 +148,18 @@ interface FFmpegTask {
} }
export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> { export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise<any> {
switch (method) { switch (method) {
case 'extractThumbnail': case 'extractThumbnail':
return await FFmpegService.extractThumbnail(...args as [string, string]); return await FFmpegService.extractThumbnail(...args as [string, string]);
case 'convertFile': case 'convertFile':
return await FFmpegService.convertFile(...args as [string, string, string]); return await FFmpegService.convertFile(...args as [string, string, string]);
case 'convert': case 'convert':
return await FFmpegService.convert(...args as [string, string]); return await FFmpegService.convert(...args as [string, string]);
case 'getVideoInfo': case 'getVideoInfo':
return await FFmpegService.getVideoInfo(...args as [string, string]); return await FFmpegService.getVideoInfo(...args as [string, string]);
default: default:
throw new Error(`Unknown method: ${method}`); throw new Error(`Unknown method: ${method}`);
} }
} }
recvTask<FFmpegTask>(async ({ method, args }: FFmpegTask) => {
return await handleFFmpegTask({ method, args });
});

View File

@@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import Piscina from 'piscina';
import { VideoInfo } from './video'; import { VideoInfo } from './video';
import path from 'path';
import { fileURLToPath } from 'url';
import { runTask } from './worker';
type EncodeArgs = { type EncodeArgs = {
method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo'; method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
@@ -9,42 +11,26 @@ type EncodeArgs = {
type EncodeResult = any; type EncodeResult = any;
async function getWorkerPath() { function getWorkerPath() {
return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href; return path.join(path.dirname(fileURLToPath(import.meta.url)), './ffmpeg-worker.mjs');
} }
export class FFmpegService { export class FFmpegService {
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> { public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
filename: await getWorkerPath(),
});
await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
await piscina.destroy();
} }
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> { public static async convertFile(inputFile: string, outputFile: string, format: string): Promise<void> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convertFile', args: [inputFile, outputFile, format] });
filename: await getWorkerPath(),
});
await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
await piscina.destroy();
} }
public static async convert(filePath: string, pcmPath: string): Promise<Buffer> { public static async convert(filePath: string, pcmPath: string): Promise<Buffer> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ const result = await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'convert', args: [filePath, pcmPath] });
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
await piscina.destroy();
return result; return result;
} }
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> { public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise<VideoInfo> {
const piscina = new Piscina<EncodeArgs, EncodeResult>({ const result = await await runTask<EncodeArgs, EncodeResult>(getWorkerPath(), { method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
filename: await getWorkerPath(),
});
const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
await piscina.destroy();
return result; return result;
} }
} }

View File

@@ -54,7 +54,7 @@ export class ForwardMsgBuilder {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const isGroupMsg = msg.some(m => m.isGroupMsg); const isGroupMsg = msg.some(m => m.isGroupMsg);
if (!source) { if (!source) {
source = isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录'; source = msg.length === 0 ? '聊天记录' : (isGroupMsg ? '群聊的聊天记录' : msg.map(m => m.senderName).filter((v, i, a) => a.indexOf(v) === i).slice(0, 4).join('和') + '的聊天记录');
} }
if (!news) { if (!news) {
news = msg.length === 0 ? [{ news = msg.length === 0 ? [{

View File

@@ -232,7 +232,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`); tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
} }
if (msg.senderUin !== '0') { if (msg.senderUin !== '0') {
tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`); tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
} }
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) { } else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备'); tokens.push('移动设备');

View File

@@ -163,7 +163,7 @@ class Store {
const current = this.get<StoreValueType>(key); const current = this.get<StoreValueType>(key);
if (current === null) { if (current === null) {
this.set(key, 1); this.set(key, 1, 60);
return 1; return 1;
} }
@@ -180,7 +180,7 @@ class Store {
} }
const newValue = numericValue + 1; const newValue = numericValue + 1;
this.set(key, newValue); this.set(key, newValue, 60);
return newValue; return newValue;
} }
} }

View File

@@ -1 +1 @@
export const napCatVersion = '4.5.12'; export const napCatVersion = '4.6.9';

29
src/common/worker.ts Normal file
View File

@@ -0,0 +1,29 @@
import { Worker } from 'worker_threads';
export async function runTask<T, R>(workerScript: string, taskData: T): Promise<R> {
let worker = new Worker(workerScript);
try {
return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => {
resolve(result);
});
worker.on('error', (error) => {
reject(new Error(`Worker error: ${error.message}`));
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
worker.postMessage(taskData);
});
} catch (error: unknown) {
throw new Error(`Failed to run task: ${(error as Error).message}`);
} finally {
// Ensure the worker is terminated after the promise is settled
worker.terminate();
}
}

View File

@@ -41,7 +41,8 @@ export class NTQQFileApi {
this.context = context; this.context = context;
this.core = core; this.core = core;
this.rkeyManager = new RkeyManager([ this.rkeyManager = new RkeyManager([
'https://rkey.napneko.icu/rkeys' 'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://secret-service.bietiaop.com/rkeys',//国内
], ],
this.context.logger this.context.logger
); );
@@ -434,9 +435,9 @@ export class NTQQFileApi {
}; };
try { try {
if (this.core.apis.PacketApi.available && this.packetRkey?.[0] && this.packetRkey?.[1]) { if (this.core.apis.PacketApi.available) {
const rkey_expired_private = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000; const rkey_expired_private = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
const rkey_expired_group = !this.packetRkey || this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000; const rkey_expired_group = !this.packetRkey || (this.packetRkey[0] && this.packetRkey[0].time + Number(this.packetRkey[0].ttl) < Date.now() / 1000);
if (rkey_expired_private || rkey_expired_group) { if (rkey_expired_private || rkey_expired_group) {
this.packetRkey = await this.fetchRkeyWithRetry(); this.packetRkey = await this.fetchRkeyWithRetry();
} }

View File

@@ -27,6 +27,9 @@ export class NTQQGroupApi {
this.core = core; this.core = core;
} }
async setGroupRemark(groupCode: string, remark: string) {
return this.context.session.getGroupService().modifyGroupRemark(groupCode, remark);
}
async fetchGroupDetail(groupCode: string) { async fetchGroupDetail(groupCode: string) {
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2( const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupDetailInfo', 'NodeIKernelGroupService/getGroupDetailInfo',
@@ -165,7 +168,13 @@ export class NTQQGroupApi {
return this.groupMemberCache.get(groupCode); return this.groupMemberCache.get(groupCode);
} }
async refreshGroupMemberCachePartial(groupCode: string, uid: string) {
const member = await this.getGroupMemberEx(groupCode, uid, true);
if (member) {
this.groupMemberCache.get(groupCode)?.set(uid, member);
}
return member;
}
async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) { async getGroupMember(groupCode: string | number, memberUinOrUid: string | number) {
const groupCodeStr = groupCode.toString(); const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString(); const memberUinOrUidStr = memberUinOrUid.toString();
@@ -339,9 +348,9 @@ export class NTQQGroupApi {
return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl); return this.context.session.getGroupService().uploadGroupBulletinPic(groupCode, _Pskey, imageurl);
} }
async handleGroupRequest(notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) { async handleGroupRequest(doubt: boolean, notify: GroupNotify, operateType: NTGroupRequestOperateTypes, reason?: string) {
return this.context.session.getGroupService().operateSysNotify( return this.context.session.getGroupService().operateSysNotify(
false, doubt,
{ {
operateType: operateType, operateType: operateType,
targetMsg: { targetMsg: {

View File

@@ -1,5 +1,5 @@
import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types'; import { ChatType, GetFileListParam, Peer, RawMessage, SendMessageElement, SendStatusType } from '@/core/types';
import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore } from '@/core'; import { GroupFileInfoUpdateItem, InstanceContext, NapCatCore, NodeIKernelMsgService } from '@/core';
import { GeneralCallResult } from '@/core/services/common'; import { GeneralCallResult } from '@/core/services/common';
export class NTQQMsgApi { export class NTQQMsgApi {
@@ -12,6 +12,11 @@ export class NTQQMsgApi {
this.context = context; this.context = context;
this.core = core; this.core = core;
} }
async clickInlineKeyboardButton(...params: Parameters<NodeIKernelMsgService['clickInlineKeyboardButton']>) {
return this.context.session.getMsgService().clickInlineKeyboardButton(...params);
}
getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) { getMsgByClientSeqAndTime(peer: Peer, replyMsgClientSeq: string, replyMsgTime: string) {
// https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取 // https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html#EmojiType 可以用过特殊方式拉取
return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime); return this.context.session.getMsgService().getMsgByClientSeqAndTime(peer, replyMsgClientSeq, replyMsgTime);
@@ -131,6 +136,20 @@ export class NTQQMsgApi {
}); });
} }
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
pageLimit: 20000,
});
}
async setMsgRead(peer: Peer) { async setMsgRead(peer: Peer) {
return this.context.session.getMsgService().setMsgRead(peer); return this.context.session.getMsgService().setMsgRead(peer);
} }

View File

@@ -186,5 +186,37 @@
"9.9.17-31363": { "9.9.17-31363": {
"appid": 537266500, "appid": 537266500,
"qua": "V1_WIN_NQ_9.9.17_31363_GW_B" "qua": "V1_WIN_NQ_9.9.17_31363_GW_B"
},
"3.2.16-32690": {
"appid": 537271229,
"qua": "V1_LNX_NQ_3.2.16_32690_GW_B"
},
"9.9.18-32690": {
"appid": 537271194,
"qua": "V1_WIN_NQ_9.9.18_32690_GW_B"
},
"6.9.66-32690": {
"appid": 537271218,
"qua": "V1_MAC_NQ_6.9.66_32690_GW_B"
},
"3.2.16-32721": {
"appid": 537271229,
"qua": "V1_LNX_NQ_3.2.16_32721_GW_B"
},
"9.9.18-32793": {
"appid": 537271244,
"qua": "V1_WIN_NQ_9.9.18_32793_GW_B"
},
"3.2.16-32793": {
"appid": 537271279,
"qua": "V1_LNX_NQ_3.2.16_32793_GW_B"
},
"3.2.16-32869": {
"appid": 537271329,
"qua": "V1_LNX_NQ_3.2.16_32869_GW_B"
},
"9.9.18-32869": {
"appid": 537271294,
"qua": "V1_WIN_NQ_9.9.18_32869_GW_B"
} }
} }

View File

@@ -4,5 +4,6 @@
"fileLogLevel": "debug", "fileLogLevel": "debug",
"consoleLogLevel": "info", "consoleLogLevel": "info",
"packetBackend": "auto", "packetBackend": "auto",
"packetServer": "" "packetServer": "",
} "o3HookMode": 1
}

View File

@@ -175,7 +175,7 @@
"send": "713A318", "send": "713A318",
"recv": "713DB50" "recv": "713DB50"
}, },
"6.9.63.30851-x64": { "6.9.63-30851-x64": {
"send": "46C8040", "send": "46C8040",
"recv": "46CA8AC" "recv": "46CA8AC"
}, },
@@ -195,7 +195,7 @@
"send": "713A318", "send": "713A318",
"recv": "713DB50" "recv": "713DB50"
}, },
"6.9.63.30899-x64": { "6.9.63-30899-x64": {
"send": "46C8040", "send": "46C8040",
"recv": "46CA8AC" "recv": "46CA8AC"
}, },
@@ -211,7 +211,7 @@
"send": "39C1350", "send": "39C1350",
"recv": "39C5784" "recv": "39C5784"
}, },
"6.9.63.31245-x64": { "6.9.63-31245-x64": {
"send": "4720A40", "send": "4720A40",
"recv": "47232AC" "recv": "47232AC"
}, },
@@ -239,12 +239,56 @@
"send": "71BFD48", "send": "71BFD48",
"recv": "71C3580" "recv": "71C3580"
}, },
"6.9.65.31363-x64": { "6.9.65-31363-x64": {
"send": "4720E80", "send": "4720E80",
"recv": "47236EC" "recv": "47236EC"
}, },
"6.9.65.31363-arm64": { "6.9.65-31363-arm64": {
"send": "422CEF8", "send": "422CEF8",
"recv": "422F710" "recv": "422F710"
},
"9.9.18-32690-x64": {
"send": "39F9630",
"recv": "39FDE30"
},
"3.2.16-32690-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32690-arm64": {
"send": "7226630",
"recv": "7229F60"
},
"3.2.16-32721-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32721-arm64": {
"send": "7226630",
"recv": "7229F60"
},
"9.9.18-32793-x64": {
"send": "39F9A30",
"recv": "39FE230"
},
"3.2.16-32793-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32793-arm64": {
"send": "7226630",
"recv": "7229F60"
},
"9.9.18-32869-x64": {
"send": "39F9A30",
"recv": "39FE230"
},
"3.2.16-32869-x64": {
"send": "A5E24C0",
"recv": "A5E5EE0"
},
"3.2.16-32869-arm64": {
"send": "7226630",
"recv": "7229F60"
} }
} }

View File

@@ -9,7 +9,8 @@ export const NapcatConfigSchema = Type.Object({
fileLogLevel: Type.String({ default: 'debug' }), fileLogLevel: Type.String({ default: 'debug' }),
consoleLogLevel: Type.String({ default: 'info' }), consoleLogLevel: Type.String({ default: 'info' }),
packetBackend: Type.String({ default: 'auto' }), packetBackend: Type.String({ default: 'auto' }),
packetServer: Type.String({ default: '' }) packetServer: Type.String({ default: '' }),
o3HookMode: Type.Number({ default: 0 }),
}); });
export type NapcatConfig = Static<typeof NapcatConfigSchema>; export type NapcatConfig = Static<typeof NapcatConfigSchema>;

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core'; import { ChatType, RawMessage } from '@/core';
export interface SearchGroupInfo { export interface SearchGroupInfo {
groupCode: string; groupCode: string;
ownerUid: string; ownerUid: string;
@@ -56,7 +56,7 @@ export interface GroupSearchResult {
nextPos: number; nextPos: number;
} }
export interface NodeIKernelSearchListener { export interface NodeIKernelSearchListener {
onSearchGroupResult(params: GroupSearchResult): any; onSearchGroupResult(params: GroupSearchResult): any;
onSearchFileKeywordsResult(params: { onSearchFileKeywordsResult(params: {
@@ -94,4 +94,27 @@ export interface NodeIKernelSearchListener {
}[] }[]
}[] }[]
}): any; }): any;
onSearchMsgKeywordsResult(params: {
searchId: string,
hasMore: boolean,
resultItems: Array<{
msgId: string,
msgSeq: string,
msgTime: string,
senderUid: string,
senderUin: string,
senderNick: string,
senderNickHits: unknown[],
senderRemark: string,
senderRemarkHits: unknown[],
senderCard: string,
senderCardHits: unknown[],
fieldType: number,
fieldText: string,
msgRecord: RawMessage;
hitsInfo: Array<unknown>,
msgAbstract: unknown,
}>
}): void | Promise<void>;
} }

View File

@@ -11,7 +11,7 @@ export class NodeIKernelSessionListener {
} }
onOpentelemetryInit(args: unknown): any { onOpentelemetryInit(info: { is_init: boolean, is_report: boolean }): any {
} }

View File

@@ -11,7 +11,7 @@ import { PacketLogger } from '@/core/packet/context/loggerContext';
// 0 send 1 recv // 0 send 1 recv
export interface NativePacketExportType { export interface NativePacketExportType {
InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void) => boolean; InitHook?: (send: string, recv: string, callback: (type: number, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean;
SendPacket?: (cmd: string, data: string, trace_id: string) => void; SendPacket?: (cmd: string, data: string, trace_id: string) => void;
} }
@@ -42,6 +42,7 @@ export class NativePacketClient extends IPacketClient {
const platform = process.platform + '.' + process.arch; const platform = process.platform + '.' + process.arch;
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node'); const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => { this.MoeHooExport.exports.InitHook?.(send, recv, (type: number, uin: string, cmd: string, seq: number, hex_data: string) => {
const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex'); const trace_id = createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex');
if (type === 0 && this.cb.get(trace_id + 'recv')) { if (type === 0 && this.cb.get(trace_id + 'recv')) {
@@ -55,7 +56,7 @@ export class NativePacketClient extends IPacketClient {
// console.log('callback:', callback, trace_id); // console.log('callback:', callback, trace_id);
callback?.({ seq, cmd, hex_data }); callback?.({ seq, cmd, hex_data });
} }
}); }, this.napcore.config.o3HookMode == 1);
this.available = true; this.available = true;
} }

View File

@@ -1,22 +1,22 @@
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import {PacketContext} from '@/core/packet/context/packetContext'; import { PacketContext } from '@/core/packet/context/packetContext';
import * as trans from '@/core/packet/transformer'; import * as trans from '@/core/packet/transformer';
import {PacketMsg} from '@/core/packet/message/message'; import { PacketMsg } from '@/core/packet/message/message';
import { import {
PacketMsgFileElement, PacketMsgFileElement,
PacketMsgPicElement, PacketMsgPicElement,
PacketMsgPttElement, PacketMsgPttElement,
PacketMsgVideoElement PacketMsgVideoElement
} from '@/core/packet/message/element'; } from '@/core/packet/message/element';
import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core'; import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp'; import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
import {AIVoiceChatType} from '@/core/packet/entities/aiChat'; import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core'; import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto'; import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto';
import {OidbPacket} from '@/core/packet/transformer/base'; import { OidbPacket } from '@/core/packet/transformer/base';
import {ImageOcrResult} from '@/core/packet/entities/ocrResult'; import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
import {gunzipSync} from 'zlib'; import { gunzipSync } from 'zlib';
import {PacketMsgConverter} from '@/core/packet/message/converter'; import { PacketMsgConverter } from '@/core/packet/message/converter';
export class PacketOperationContext { export class PacketOperationContext {
private readonly context: PacketContext; private readonly context: PacketContext;
@@ -59,10 +59,10 @@ export class PacketOperationContext {
const res = trans.GetStrangerInfo.parse(resp); const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value); const extBigInt = BigInt(res.data.status.value);
if (extBigInt <= 10n) { if (extBigInt <= 10n) {
return {status: Number(extBigInt) * 10, ext_status: 0}; return { status: Number(extBigInt) * 10, ext_status: 0 };
} }
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn)); status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
return {status: 10, ext_status: status}; return { status: 10, ext_status: status };
} catch { } catch {
return undefined; return undefined;
} }
@@ -79,13 +79,13 @@ export class PacketOperationContext {
const reqList = msg.flatMap(m => const reqList = msg.flatMap(m =>
m.msg.map(e => { m.msg.map(e => {
if (e instanceof PacketMsgPicElement) { if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({chatType, peerUid}, e); return this.context.highway.uploadImage({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgVideoElement) { } else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({chatType, peerUid}, e); return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgPttElement) { } else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({chatType, peerUid}, e); return this.context.highway.uploadPtt({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgFileElement) { } else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({chatType, peerUid}, e); return this.context.highway.uploadFile({ chatType, peerUid }, e);
} }
return null; return null;
}).filter(Boolean) }).filter(Boolean)
@@ -160,6 +160,12 @@ export class PacketOperationContext {
const res = trans.DownloadGroupFile.parse(resp); const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`; return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
} }
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadPrivateFile.parse(resp);
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) { async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node); const req = trans.DownloadGroupPtt.build(groupUin, node);

View File

@@ -144,7 +144,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadGroupImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -181,7 +181,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadC2CImageReq get upload ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -219,7 +219,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadGroupVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid, fileUuid: index.fileUuid,
@@ -244,16 +244,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`); this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
} }
const subFile = preRespData.upload.subFileInfos[0]; const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != '') { if (subFile!.uKey && subFile!.uKey != '') {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index; const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid, fileUuid: index.fileUuid,
uKey: subFile.uKey, uKey: subFile!.uKey,
network: { network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
}, },
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize, blockSize: BlockSize,
@@ -269,7 +269,7 @@ export class PacketHighwayContext {
extend extend
); );
} else { } else {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
} }
video.msgInfo = preRespData.upload.msgInfo; video.msgInfo = preRespData.upload.msgInfo;
} }
@@ -284,7 +284,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadC2CVideoReq get upload video ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid, fileUuid: index.fileUuid,
@@ -309,16 +309,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`); this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
} }
const subFile = preRespData.upload.subFileInfos[0]; const subFile = preRespData.upload.subFileInfos[0];
if (subFile.uKey && subFile.uKey != '') { if (subFile!.uKey && subFile!.uKey != '') {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`); this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[1].index; const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid, fileUuid: index.fileUuid,
uKey: subFile.uKey, uKey: subFile!.uKey,
network: { network: {
ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S) ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
}, },
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody, msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize, blockSize: BlockSize,
@@ -334,7 +334,7 @@ export class PacketHighwayContext {
extend extend
); );
} else { } else {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile.uKey}, don't need upload!`); this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid thumb ukey ${subFile!.uKey}, don't need upload!`);
} }
video.msgInfo = preRespData.upload.msgInfo; video.msgInfo = preRespData.upload.msgInfo;
} }
@@ -347,7 +347,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadGroupPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -383,7 +383,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey; const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') { if (ukey && ukey != '') {
this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`); this.logger.debug(`[Highway] uploadC2CPttReq get upload ptt ukey: ${ukey}, need upload!`);
const index = preRespData.upload.msgInfo.msgInfoBody[0].index; const index = preRespData.upload.msgInfo.msgInfoBody[0]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex'); const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex'); const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({ const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({

View File

@@ -9,10 +9,10 @@ import {
SendFileElement, SendFileElement,
SendMarkdownElement, SendMarkdownElement,
SendMarketFaceElement, SendMarketFaceElement,
SendMultiForwardMsgElement,
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
SendStructLongMsgElement,
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from '@/core'; } from '@/core';
@@ -46,7 +46,7 @@ const SupportedElementTypes = [
ElementType.PTT, ElementType.PTT,
ElementType.ARK, ElementType.ARK,
ElementType.MARKDOWN, ElementType.MARKDOWN,
ElementType.STRUCTLONGMSG ElementType.MULTIFORWARD
]; ];
type SendMessageTypeElementMap = { type SendMessageTypeElementMap = {
@@ -59,7 +59,7 @@ type SendMessageTypeElementMap = {
[ElementType.REPLY]: SendReplyElement, [ElementType.REPLY]: SendReplyElement,
[ElementType.ARK]: SendArkElement, [ElementType.ARK]: SendArkElement,
[ElementType.MFACE]: SendMarketFaceElement, [ElementType.MFACE]: SendMarketFaceElement,
[ElementType.STRUCTLONGMSG]: SendStructLongMsgElement, [ElementType.MULTIFORWARD]: SendMultiForwardMsgElement,
[ElementType.MARKDOWN]: SendMarkdownElement, [ElementType.MARKDOWN]: SendMarkdownElement,
}; };
@@ -118,9 +118,8 @@ export class PacketMsgConverter {
[ElementType.MARKDOWN]: (element) => { [ElementType.MARKDOWN]: (element) => {
return new PacketMsgMarkDownElement(element as SendMarkdownElement); return new PacketMsgMarkDownElement(element as SendMarkdownElement);
}, },
// TODO: check this logic, move it in arkElement? [ElementType.MULTIFORWARD]: (element) => {
[ElementType.STRUCTLONGMSG]: (element) => { return new PacketMultiMsgElement(element as SendMultiForwardMsgElement);
return new PacketMultiMsgElement(element as SendStructLongMsgElement);
} }
}; };

View File

@@ -27,7 +27,7 @@ import {
SendPicElement, SendPicElement,
SendPttElement, SendPttElement,
SendReplyElement, SendReplyElement,
SendStructLongMsgElement, SendMultiForwardMsgElement,
SendTextElement, SendTextElement,
SendVideoElement SendVideoElement
} from '@/core'; } from '@/core';
@@ -661,13 +661,13 @@ export class PacketMsgMarkDownElement extends IPacketMsgElement<SendMarkdownElem
} }
} }
export class PacketMultiMsgElement extends IPacketMsgElement<SendStructLongMsgElement> { export class PacketMultiMsgElement extends IPacketMsgElement<SendMultiForwardMsgElement> {
resid: string; resid: string;
message: PacketMsg[]; message: PacketMsg[];
constructor(rawElement: SendStructLongMsgElement, message?: PacketMsg[]) { constructor(rawElement: SendMultiForwardMsgElement, message?: PacketMsg[]) {
super(rawElement); super(rawElement);
this.resid = rawElement.structLongMsgElement.resId; this.resid = rawElement.multiForwardMsgElement.resId;
this.message = message ?? []; this.message = message ?? [];
} }

View File

@@ -1,7 +1,7 @@
import { IPacketMsgElement } from '@/core/packet/message/element'; import { IPacketMsgElement } from '@/core/packet/message/element';
import { SendMessageElement, SendStructLongMsgElement } from '@/core'; import {SendMessageElement, SendMultiForwardMsgElement} from '@/core';
export type PacketSendMsgElement = SendMessageElement | SendStructLongMsgElement export type PacketSendMsgElement = SendMessageElement | SendMultiForwardMsgElement
export interface PacketMsg { export interface PacketMsg {
seq?: number; seq?: number;

View File

@@ -1,4 +1,5 @@
import { DownloadBaseEmojiByIdReq, DownloadBaseEmojiByUrlReq, GetBaseEmojiPathReq, PullSysEmojisReq } from '../types'; import { DownloadBaseEmojiByIdReq, DownloadBaseEmojiByUrlReq, GetBaseEmojiPathReq, PullSysEmojisReq } from '../types';
import { GeneralCallResult } from './common';
export interface NodeIKernelBaseEmojiService { export interface NodeIKernelBaseEmojiService {
removeKernelBaseEmojiListener(listenerId: number): void; removeKernelBaseEmojiListener(listenerId: number): void;
@@ -7,7 +8,26 @@ export interface NodeIKernelBaseEmojiService {
isBaseEmojiPathExist(args: Array<string>): unknown; isBaseEmojiPathExist(args: Array<string>): unknown;
fetchFullSysEmojis(pullSysEmojisReq: PullSysEmojisReq): unknown; fetchFullSysEmojis(pullSysEmojisReq: PullSysEmojisReq): Promise<GeneralCallResult & {
rsp: {
otherPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
normalPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
superPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
},
redHeartPanelResult: {
SysEmojiGroupList: Array<unknown>,
downloadInfo: Array<unknown>
}
}
}>;
getBaseEmojiPathByIds(getBaseEmojiPathReqs: Array<GetBaseEmojiPathReq>): unknown; getBaseEmojiPathByIds(getBaseEmojiPathReqs: Array<GetBaseEmojiPathReq>): unknown;

View File

@@ -165,7 +165,7 @@ export interface NodeIKernelGroupService {
modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise<GeneralCallResult>; modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise<GeneralCallResult>;
modifyGroupRemark(groupCode: string, remark: string): void; modifyGroupRemark(groupCode: string, remark: string): Promise<GeneralCallResult>;
modifyGroupDetailInfo(groupCode: string, arg: unknown): void; modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
@@ -253,7 +253,7 @@ export interface NodeIKernelGroupService {
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>; getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;
setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<void>; setMemberShutUp(groupCode: string, memberTimes: { uid: string, timeStamp: number }[]): Promise<GeneralCallResult>;
getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>; getGroupRecommendContactArkJson(groupCode: string): Promise<GeneralCallResult & { arkJson: string }>;

View File

@@ -1,5 +1,9 @@
import { GeneralCallResult } from './common'; import { GeneralCallResult } from './common';
enum ProxyType {
CLOSE = 0,
HTTP = 1,
SOCKET = 2
}
export interface NodeIKernelMSFService { export interface NodeIKernelMSFService {
getServerTime(): string; getServerTime(): string;
setNetworkProxy(param: { setNetworkProxy(param: {
@@ -7,10 +11,19 @@ export interface NodeIKernelMSFService {
userPwd: string, userPwd: string,
address: string, address: string,
port: number, port: number,
proxyType: number, proxyType: ProxyType,
domain: string, domain: string,
isSocket: boolean isSocket: boolean
}): Promise<GeneralCallResult>; }): Promise<GeneralCallResult>;
getNetworkProxy(): Promise<{
userName: string,
userPwd: string,
address: string,
port: number,
proxyType: ProxyType,
domain: string,
isSocket: boolean
}>;
//http //http
// userName: '', // userName: '',
// userPwd: '', // userPwd: '',

View File

@@ -464,11 +464,20 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikesForRole(...args: unknown[]): unknown; setMsgEmojiLikesForRole(...args: unknown[]): unknown;
clickInlineKeyboardButton(...args: unknown[]): unknown; clickInlineKeyboardButton(params: {
guildId?: string,
peerId: string,
botAppid: string,
msgSeq: string,
buttonId: string,
callback_data: string,
dmFlag: number,
chatType: number // 1私聊 2群
}): Promise<GeneralCallResult & { status: number, promptText: string, promptType: number, promptIcon: number }>;
setCurOnScreenMsg(...args: unknown[]): unknown; setCurOnScreenMsg(...args: unknown[]): unknown;
setCurOnScreenMsgForMsgEvent(...args: unknown[]): unknown; setCurOnScreenMsgForMsgEvent(peer: Peer, msgRegList: Map<string, Uint8Array>): void;
getMiscData(key: string): unknown; getMiscData(key: string): unknown;

View File

@@ -1,4 +1,5 @@
import { NodeIKernelRobotListener } from '@/core/listeners'; import { NodeIKernelRobotListener } from '@/core/listeners';
import { GeneralCallResult, Peer } from '..';
export interface NodeIKernelRobotService { export interface NodeIKernelRobotService {
fetchGroupRobotStoreDiscovery(arg: unknown): unknown; fetchGroupRobotStoreDiscovery(arg: unknown): unknown;
@@ -31,5 +32,17 @@ export interface NodeIKernelRobotService {
getRobotUinRange(data: unknown): Promise<{ response: { robotUinRanges: Array<unknown> } }>; getRobotUinRange(data: unknown): Promise<{ response: { robotUinRanges: Array<unknown> } }>;
getRobotFunctions(peer: Peer, params: {
uins: Array<string>,
num: 0,
client_info: { platform: 4, version: '', build_num: 9999 },
tinyids: [],
page: 0,
full_fetch: false,
scene: 4,
filter: 1,
bkn: ''
}): Promise<GeneralCallResult & { response: { bot_features: Array<unknown>, next_page: number } }>;
isNull(): boolean; isNull(): boolean;
} }

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core/types'; import { ChatType, Peer } from '@/core/types';
import { GeneralCallResult } from './common'; import { GeneralCallResult } from './common';
export interface NodeIKernelSearchService { export interface NodeIKernelSearchService {
@@ -54,7 +54,7 @@ export interface NodeIKernelSearchService {
cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments cancelSearchChatMsgs(...args: unknown[]): unknown;// needs 3 arguments
searchMsgWithKeywords(...args: unknown[]): unknown;// needs 2 arguments searchMsgWithKeywords(keyWords: string[], param: Peer & { searchFields: number, pageLimit: number }): Promise<GeneralCallResult>;
searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments

View File

@@ -347,6 +347,8 @@ export type SendMarkdownElement = SendElementBase<ElementType.MARKDOWN> & Elemen
export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>; export type SendShareLocationElement = SendElementBase<ElementType.SHARELOCATION> & ElementBase<'shareLocationElement'>;
export type SendMultiForwardMsgElement = SendElementBase<ElementType.MULTIFORWARD> & ElementBase<'multiForwardMsgElement'>;
export type SendMessageElement = SendTextElement | SendPttElement | export type SendMessageElement = SendTextElement | SendPttElement |
SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement | SendPicElement | SendReplyElement | SendFaceElement | SendMarketFaceElement | SendFileElement |
SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement; SendVideoElement | SendArkElement | SendMarkdownElement | SendShareLocationElement;

View File

@@ -7,13 +7,13 @@ import { SelfInfo } from '@/core/types';
import { NodeIKernelLoginListener } from '@/core/listeners'; import { NodeIKernelLoginListener } from '@/core/listeners';
import { NodeIKernelLoginService } from '@/core/services'; import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper'; import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig } from '@/webui'; import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
//Framework ES入口文件 //Framework ES入口文件
export async function getWebUiUrl() { export async function getWebUiUrl() {
const WebUiConfigData = (await WebUiConfig.GetWebUIConfig()); const WebUiConfigData = (await WebUiConfig.GetWebUIConfig());
return 'http://127.0.0.1:' + WebUiConfigData.port + '/webui/?token=' + WebUiConfigData.token; return 'http://127.0.0.1:' + webUiRuntimePort + '/webui/?token=' + WebUiConfigData.token;
} }
export async function NCoreInitFramework( export async function NCoreInitFramework(

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
export class BotExit extends OneBotAction<void, void> {
override actionName = ActionName.Exit;
async _handle() {
process.exit(0);
}
}

View File

@@ -0,0 +1,30 @@
import { ActionName } from '@/onebot/action/router';
import { OneBotAction } from '../OneBotAction';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]),
bot_appid: Type.String(),
button_id: Type.String({ default: '' }),
callback_data: Type.String({ default: '' }),
msg_seq: Type.String({ default: '10086' }),
});
type Payload = Static<typeof SchemaData>;
export class ClickInlineKeyboardButton extends OneBotAction<Payload, unknown> {
override actionName = ActionName.ClickInlineKeyboardButton;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
buttonId: payload.button_id,
peerId: payload.group_id.toString(),
botAppid: payload.bot_appid,
msgSeq: payload.msg_seq,
callback_data: payload.callback_data,
dmFlag: 0,
chatType: 2
})
}
}

View File

@@ -0,0 +1,56 @@
import { PacketHexStr } from '@/core/packet/transformer/base';
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { ProtoBuf, ProtoBufBase, PBUint32, PBString } from 'napcat.protobuf';
interface Friend {
uin: number;
uid: string;
nick_name: string;
age: number;
source: string;
}
interface Block {
str_uid: string;
bytes_source: string;
uint32_sex: number;
uint32_age: number;
bytes_nick: string;
uint64_uin: number;
}
export class GetUnidirectionalFriendList extends OneBotAction<void, Friend[]> {
override actionName = ActionName.GetUnidirectionalFriendList;
async pack_data(data: string): Promise<Uint8Array> {
return ProtoBuf(class extends ProtoBufBase {
type = PBUint32(2, false, 0);
data = PBString(3, false, data);
}).encode();
}
async _handle(): Promise<Friend[]> {
const self_id = this.core.selfInfo.uin;
const req_json = {
uint64_uin: self_id,
uint64_top: 0,
uint32_req_num: 99,
bytes_cookies: ""
};
const packed_data = await this.pack_data(JSON.stringify(req_json));
const data = Buffer.from(packed_data).toString('hex');
const rsq = { cmd: 'MQUpdateSvc_com_qq_ti.web.OidbSvc.0xe17_0', data: data as PacketHexStr };
const rsp_data = await this.core.apis.PacketApi.pkt.operation.sendPacket(rsq, true);
const block_json = ProtoBuf(class extends ProtoBufBase { data = PBString(4); }).decode(rsp_data);
const block_list: Block[] = JSON.parse(block_json.data).rpt_block_list;
return block_list.map((block) => ({
uin: block.uint64_uin,
uid: block.str_uid,
nick_name: Buffer.from(block.bytes_nick, 'base64').toString(),
age: block.uint32_age,
source: Buffer.from(block.bytes_source, 'base64').toString()
}));
}
}

View File

@@ -0,0 +1,22 @@
import { OneBotAction } from '@/onebot/action/OneBotAction';
import { ActionName } from '@/onebot/action/router';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_id: Type.String(),
remark: Type.String(),
});
type Payload = Static<typeof SchemaData>;
export default class SetGroupRemark extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupRemark;
override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> {
let ret = await this.core.apis.GroupApi.setGroupRemark(payload.group_id, payload.remark);
if (ret.result != 0) {
throw new Error(`设置群备注失败, ${ret.result}:${ret.errMsg}`);
}
return null;
}
}

View File

@@ -5,7 +5,7 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({ const SchemaData = Type.Object({
group_id: Type.Union([Type.Number(), Type.String()]), group_id: Type.Union([Type.Number(), Type.String()]),
user_id: Type.Union([Type.Number(), Type.String()]), user_id: Type.Union([Type.Number(), Type.String()]),
special_title: Type.String(), special_title: Type.String({ default: '' }),
}); });
type Payload = Static<typeof SchemaData>; type Payload = Static<typeof SchemaData>;
@@ -16,7 +16,7 @@ export class SetSpecialTittle extends GetPacketStatusDepends<Payload, void> {
async _handle(payload: Payload) { async _handle(payload: Payload) {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if(!uid) throw new Error('User not found'); if (!uid) throw new Error('User not found');
await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title); await this.core.apis.PacketApi.pkt.operation.SetGroupSpecialTitle(+payload.group_id, uid, payload.special_title);
} }
} }

View File

@@ -0,0 +1,36 @@
import { ActionName } from '@/onebot/action/router';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
file_id: Type.String(),
});
type Payload = Static<typeof SchemaData>;
interface GetPrivateFileUrlResponse {
url?: string;
}
export class GetPrivateFileUrl extends GetPacketStatusDepends<Payload, GetPrivateFileUrlResponse> {
override actionName = ActionName.NapCat_GetPrivateFileUrl;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id);
if (contextMsgFile?.fileUUID && contextMsgFile.msgId) {
let msg = await this.core.apis.MsgApi.getMsgsByMsgId(contextMsgFile.peer, [contextMsgFile.msgId]);
let self_id = this.core.selfInfo.uid;
let file_hash = msg.msgList[0]?.elements.map(ele => ele.fileElement?.file10MMd5)[0];
if (file_hash) {
return {
url: await this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(self_id, contextMsgFile.fileUUID, file_hash)
};
}
}
throw new Error('real fileUUID not found!');
}
}

View File

@@ -20,11 +20,12 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
const approve = payload.approve?.toString() !== 'false'; const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' '; const reason = payload.reason ?? ' ';
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag); const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(flag);
const notify = invite_notify ?? await this.findNotify(flag); const { doubt, notify } = invite_notify ? { doubt: false, notify: invite_notify } : await this.findNotify(flag);
if (!notify) { if (!notify) {
throw new Error('No such request'); throw new Error('No such request');
} }
await this.core.apis.GroupApi.handleGroupRequest( await this.core.apis.GroupApi.handleGroupRequest(
doubt,
notify, notify,
approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
reason, reason,
@@ -36,7 +37,8 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag); let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
if (!notify) { if (!notify) {
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag); notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
return { doubt: true, notify };
} }
return notify; return { doubt: false, notify };
} }
} }

View File

@@ -13,12 +13,15 @@ type Payload = Static<typeof SchemaData>;
export default class SetGroupBan extends OneBotAction<Payload, null> { export default class SetGroupBan extends OneBotAction<Payload, null> {
override actionName = ActionName.SetGroupBan; override actionName = ActionName.SetGroupBan;
override payloadSchema = SchemaData; override payloadSchema = SchemaData;
async _handle(payload: Payload): Promise<null> { async _handle(payload: Payload): Promise<null> {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString()); const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!uid) throw new Error('uid error'); if (!uid) throw new Error('uid error');
await this.core.apis.GroupApi.banMember(payload.group_id.toString(), let member_role = (await this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, true))?.role;
if (member_role === 4) throw new Error('cannot ban owner');
// 例如无管理员权限时 result为 120101005 errMsg为 'ERR_NOT_GROUP_ADMIN'
let ret = await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
[{ uid: uid, timeStamp: +payload.duration }]); [{ uid: uid, timeStamp: +payload.duration }]);
if (ret.result !== 0) throw new Error(ret.errMsg);
return null; return null;
} }
} }

View File

@@ -104,10 +104,16 @@ import { GetClientkey } from './extends/GetClientkey';
import { SendPacket } from './extends/SendPacket'; import { SendPacket } from './extends/SendPacket';
import { SendPoke } from '@/onebot/action/packet/SendPoke'; import { SendPoke } from '@/onebot/action/packet/SendPoke';
import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus'; import { SetDiyOnlineStatus } from './extends/SetDiyOnlineStatus';
import { BotExit } from './extends/BotExit';
import { ClickInlineKeyboardButton } from './extends/ClickInlineKeyboardButton';
import { GetPrivateFileUrl } from './file/GetPrivateFileUrl';
import { GetUnidirectionalFriendList } from './extends/GetUnidirectionalFriendList';
import SetGroupRemark from './extends/SetGroupRemark';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) { export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [ const actionHandlers = [
new SetGroupRemark(obContext, core),
new GetGroupInfoEx(obContext, core), new GetGroupInfoEx(obContext, core),
new FetchEmojiLike(obContext, core), new FetchEmojiLike(obContext, core),
new GetFile(obContext, core), new GetFile(obContext, core),
@@ -221,6 +227,10 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SendPacket(obContext, core), new SendPacket(obContext, core),
new SendPoke(obContext, core), new SendPoke(obContext, core),
new GetGroupSystemMsg(obContext, core), new GetGroupSystemMsg(obContext, core),
new BotExit(obContext, core),
new ClickInlineKeyboardButton(obContext, core),
new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core),
]; ];
type HandlerUnion = typeof actionHandlers[number]; type HandlerUnion = typeof actionHandlers[number];

View File

@@ -10,6 +10,10 @@ export interface InvalidCheckResult {
} }
export const ActionName = { export const ActionName = {
SetGroupRemark: 'set_group_remark',
NapCat_GetPrivateFileUrl: 'get_private_file_url',
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
GetUnidirectionalFriendList: 'get_unidirectional_friend_list',
// onebot 11 // onebot 11
SendPrivateMsg: 'send_private_msg', SendPrivateMsg: 'send_private_msg',
SendGroupMsg: 'send_group_msg', SendGroupMsg: 'send_group_msg',
@@ -49,7 +53,7 @@ export const ActionName = {
GetVersionInfo: 'get_version_info', GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart', // Reboot : 'set_restart',
// CleanCache : 'clean_cache', // CleanCache : 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp // go-cqhttp
SetQQProfile: 'set_qq_profile', SetQQProfile: 'set_qq_profile',
// QidianGetAccountInfo : 'qidian_get_account_info', // QidianGetAccountInfo : 'qidian_get_account_info',
@@ -141,6 +145,6 @@ export const ActionName = {
SendGroupAiRecord: 'send_group_ai_record', SendGroupAiRecord: 'send_group_ai_record',
GetClientkey: 'get_clientkey', GetClientkey: 'get_clientkey',
SendPoke: 'send_poke', SendPoke: 'send_poke',
} as const; } as const;

View File

@@ -49,6 +49,7 @@ export class OneBotGroupApi {
duration = -1; duration = -1;
} }
} }
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin; const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
if (memberUin && adminUin) { if (memberUin && adminUin) {
return new OB11GroupBanEvent( return new OB11GroupBanEvent(
@@ -113,12 +114,16 @@ export class OneBotGroupApi {
async parseCardChangedEvent(msg: RawMessage) { async parseCardChangedEvent(msg: RawMessage) {
if (msg.senderUin && msg.senderUin !== '0') { if (msg.senderUin && msg.senderUin !== '0') {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin); const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
if (member && member.cardName !== msg.sendMemberName) { if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName ?? ''; const newCardName = msg.sendMemberName ?? '';
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName); const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
member.cardName = newCardName; member.cardName = newCardName;
return event; return event;
} }
if (member && member.nick !== msg.sendNickName) {
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
}
} }
return undefined; return undefined;
} }

View File

@@ -1,5 +1,5 @@
import {FileNapCatOneBotUUID} from '@/common/file-uuid'; import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import {MessageUnique} from '@/common/message-unique'; import { MessageUnique } from '@/common/message-unique';
import { import {
ChatType, ChatType,
CustomMusicSignPostData, CustomMusicSignPostData,
@@ -29,22 +29,22 @@ import {
OB11MessageImage, OB11MessageImage,
OB11MessageVideo, OB11MessageVideo,
} from '@/onebot'; } from '@/onebot';
import {OB11Construct} from '@/onebot/helper/data'; import { OB11Construct } from '@/onebot/helper/data';
import {EventType} from '@/onebot/event/OneBotEvent'; import { EventType } from '@/onebot/event/OneBotEvent';
import {encodeCQCode} from '@/onebot/helper/cqcode'; import { encodeCQCode } from '@/onebot/helper/cqcode';
import {uriToLocalFile} from '@/common/file'; import { uriToLocalFile } from '@/common/file';
import {RequestUtil} from '@/common/request'; import { RequestUtil } from '@/common/request';
import fsPromise, {constants} from 'node:fs/promises'; import fsPromise, { constants } from 'node:fs/promises';
import {OB11FriendAddNoticeEvent} from '@/onebot/event/notice/OB11FriendAddNoticeEvent'; import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import {ForwardMsgBuilder} from '@/common/forward-msg-builder'; import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
import {NapProtoMsg} from '@napneko/nap-proto-core'; import { NapProtoMsg } from '@napneko/nap-proto-core';
import {OB11GroupIncreaseEvent} from '../event/notice/OB11GroupIncreaseEvent'; import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import {GroupDecreaseSubType, OB11GroupDecreaseEvent} from '../event/notice/OB11GroupDecreaseEvent'; import { GroupDecreaseSubType, OB11GroupDecreaseEvent } from '../event/notice/OB11GroupDecreaseEvent';
import {GroupAdmin} from '@/core/packet/transformer/proto/message/groupAdmin'; import { GroupAdmin } from '@/core/packet/transformer/proto/message/groupAdmin';
import {OB11GroupAdminNoticeEvent} from '../event/notice/OB11GroupAdminNoticeEvent'; import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeEvent';
import {GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody} from '@/core/packet/transformer/proto'; import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
import {OB11GroupRequestEvent} from '../event/request/OB11GroupRequest'; import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import {LRUCache} from '@/common/lru-cache'; import { LRUCache } from '@/common/lru-cache';
type RawToOb11Converters = { type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: ( [Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -93,12 +93,12 @@ export class OneBotMsgApi {
} }
return { return {
type: OB11MessageDataType.text, type: OB11MessageDataType.text,
data: {text}, data: { text },
}; };
} else { } else {
let qq: string = 'all'; let qq: string = 'all';
if (element.atType !== NTMsgAtType.ATTYPEALL) { if (element.atType !== NTMsgAtType.ATTYPEALL) {
const {atNtUid, atUid} = element; const { atNtUid, atUid } = element;
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid; qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid;
} }
return { return {
@@ -132,7 +132,6 @@ export class OneBotMsgApi {
file: element.fileName, file: element.fileName,
sub_type: element.picSubType, sub_type: element.picSubType,
url: await this.core.apis.FileApi.getImageUrl(element), url: await this.core.apis.FileApi.getImageUrl(element),
path: element.filePath,
file_size: element.fileSize, file_size: element.fileSize,
}, },
}; };
@@ -148,13 +147,13 @@ export class OneBotMsgApi {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
}; };
const file = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName); FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return { return {
type: OB11MessageDataType.file, type: OB11MessageDataType.file,
data: { data: {
file: file, file: element.fileName,
path: element.filePath, file_id: element.fileUuid,
file_id: file,
file_size: element.fileSize, file_size: element.fileSize,
}, },
}; };
@@ -206,7 +205,7 @@ export class OneBotMsgApi {
peerUid: msg.peerUid, peerUid: msg.peerUid,
guildId: '', guildId: '',
}; };
const {emojiId} = _; const { emojiId } = _;
const dir = emojiId.substring(0, 2); const dir = emojiId.substring(0, 2);
const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`; const url = `https://gxh.vip.qq.com/club/item/parcel/item/${dir}/${emojiId}/raw300.gif`;
const filename = `${dir}-${emojiId}.gif`; const filename = `${dir}-${emojiId}.gif`;
@@ -216,7 +215,6 @@ export class OneBotMsgApi {
data: { data: {
summary: _.faceName, // 商城表情名称 summary: _.faceName, // 商城表情名称
file: filename, file: filename,
path: url,
url: url, url: url,
key: _.key, key: _.key,
emoji_id: _.emojiId, emoji_id: _.emojiId,
@@ -339,7 +337,6 @@ export class OneBotMsgApi {
type: OB11MessageDataType.video, type: OB11MessageDataType.video,
data: { data: {
file: fileCode, file: fileCode,
path: videoDownUrl,
url: videoDownUrl, url: videoDownUrl,
file_size: element.fileSize, file_size: element.fileSize,
}, },
@@ -357,8 +354,8 @@ export class OneBotMsgApi {
type: OB11MessageDataType.voice, type: OB11MessageDataType.voice,
data: { data: {
file: fileCode, file: fileCode,
path: element.filePath,
file_size: element.fileSize, file_size: element.fileSize,
path: element.filePath,
}, },
}; };
}, },
@@ -381,7 +378,7 @@ export class OneBotMsgApi {
} }
const forward: OB11MessageForward = { const forward: OB11MessageForward = {
type: OB11MessageDataType.forward, type: OB11MessageDataType.forward,
data: {id: msg.msgId} data: { id: msg.msgId }
}; };
if (!context.parseMultMsg) return forward; if (!context.parseMultMsg) return forward;
forward.data.content = await this.parseMultiMessageContent( forward.data.content = await this.parseMultiMessageContent(
@@ -412,7 +409,7 @@ export class OneBotMsgApi {
}; };
ob11ToRawConverters: Ob11ToRawConverters = { ob11ToRawConverters: Ob11ToRawConverters = {
[OB11MessageDataType.text]: async ({data: {text}}) => ({ [OB11MessageDataType.text]: async ({ data: { text } }) => ({
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
elementId: '', elementId: '',
textElement: { textElement: {
@@ -424,7 +421,7 @@ export class OneBotMsgApi {
}, },
}), }),
[OB11MessageDataType.at]: async ({data: {qq: atQQ}}, context) => { [OB11MessageDataType.at]: async ({ data: { qq: atQQ } }, context) => {
function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement { function at(atUid: string, atNtUid: string, atType: NTMsgAtType, atName: string): SendTextElement {
return { return {
elementType: ElementType.TEXT, elementType: ElementType.TEXT,
@@ -451,7 +448,7 @@ export class OneBotMsgApi {
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || ''); return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || '');
}, },
[OB11MessageDataType.reply]: async ({data: {id}}) => { [OB11MessageDataType.reply]: async ({ data: { id } }) => {
const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id)); const replyMsgM = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
if (!replyMsgM) { if (!replyMsgM) {
this.core.context.logger.logWarn('回复消息不存在', id); this.core.context.logger.logWarn('回复消息不存在', id);
@@ -473,7 +470,7 @@ export class OneBotMsgApi {
undefined; undefined;
}, },
[OB11MessageDataType.face]: async ({data: {id, resultId, chainCount}}) => { [OB11MessageDataType.face]: async ({ data: { id, resultId, chainCount } }) => {
const parsedFaceId = +id; const parsedFaceId = +id;
// 从face_config.json中获取表情名称 // 从face_config.json中获取表情名称
const sysFaces = faceConfig.sysface; const sysFaces = faceConfig.sysface;
@@ -537,12 +534,12 @@ export class OneBotMsgApi {
}, },
[OB11MessageDataType.file]: async (sendMsg, context) => { [OB11MessageDataType.file]: async (sendMsg, context) => {
const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context);
return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName); return await this.core.apis.FileApi.createValidSendFileElement(context, path, fileName);
}, },
[OB11MessageDataType.video]: async (sendMsg, context) => { [OB11MessageDataType.video]: async (sendMsg, context) => {
const {path, fileName} = await this.handleOb11FileLikeMessage(sendMsg, context); const { path, fileName } = await this.handleOb11FileLikeMessage(sendMsg, context);
let thumb = sendMsg.data.thumb; let thumb = sendMsg.data.thumb;
if (thumb) { if (thumb) {
@@ -560,7 +557,7 @@ export class OneBotMsgApi {
this.core.apis.FileApi.createValidSendPttElement( this.core.apis.FileApi.createValidSendPttElement(
(await this.handleOb11FileLikeMessage(sendMsg, context)).path), (await this.handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({data: {data}}) => ({ [OB11MessageDataType.json]: async ({ data: { data } }) => ({
elementType: ElementType.ARK, elementType: ElementType.ARK,
elementId: '', elementId: '',
arkElement: { arkElement: {
@@ -603,13 +600,13 @@ export class OneBotMsgApi {
}), }),
// Need signing // Need signing
[OB11MessageDataType.markdown]: async ({data: {content}}) => ({ [OB11MessageDataType.markdown]: async ({ data: { content } }) => ({
elementType: ElementType.MARKDOWN, elementType: ElementType.MARKDOWN,
elementId: '', elementId: '',
markdownElement: {content}, markdownElement: { content },
}), }),
[OB11MessageDataType.music]: async ({data}, context) => { [OB11MessageDataType.music]: async ({ data }, context) => {
// 保留, 直到...找到更好的解决方案 // 保留, 直到...找到更好的解决方案
if (data.id !== undefined) { if (data.id !== undefined) {
if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) { if (!['qq', '163', 'kugou', 'kuwo', 'migu'].includes(data.type)) {
@@ -633,8 +630,8 @@ export class OneBotMsgApi {
let postData: IdMusicSignPostData | CustomMusicSignPostData; let postData: IdMusicSignPostData | CustomMusicSignPostData;
if (data.id === undefined && data.content) { if (data.id === undefined && data.content) {
const {content, ...others} = data; const { content, ...others } = data;
postData = {singer: content, ...others}; postData = { singer: content, ...others };
} else { } else {
postData = data; postData = data;
} }
@@ -646,7 +643,7 @@ export class OneBotMsgApi {
try { try {
const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData); const musicJson = await RequestUtil.HttpGetJson<string>(signUrl, 'POST', postData);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: musicJson}, data: { data: musicJson },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} catch (e) { } catch (e) {
@@ -657,10 +654,23 @@ export class OneBotMsgApi {
[OB11MessageDataType.node]: async () => undefined, [OB11MessageDataType.node]: async () => undefined,
[OB11MessageDataType.forward]: async ({data}, context) => { [OB11MessageDataType.forward]: async ({ data }, context) => {
// let id = data.id.toString();
// let peer: Peer | undefined = context.peer;
// if (isNumeric(id)) {
// let msgid = '';
// if (BigInt(data.id) > 2147483647n) {
// peer = MessageUnique.getPeerByMsgId(id)?.Peer;
// msgid = id;
// } else {
// let data = MessageUnique.getMsgIdAndPeerByShortId(parseInt(id));
// msgid = data?.MsgId ?? '';
// peer = data?.Peer;
// }
// }
const jsonData = ForwardMsgBuilder.fromResId(data.id); const jsonData = ForwardMsgBuilder.fromResId(data.id);
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: JSON.stringify(jsonData)}, data: { data: JSON.stringify(jsonData) },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
}, },
@@ -680,17 +690,17 @@ export class OneBotMsgApi {
[OB11MessageDataType.miniapp]: async () => undefined, [OB11MessageDataType.miniapp]: async () => undefined,
[OB11MessageDataType.contact]: async ({data: {type = 'qq', id}}, context) => { [OB11MessageDataType.contact]: async ({ data: { type = 'qq', id } }, context) => {
if (type === 'qq') { if (type === 'qq') {
const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), ''); const arkJson = await this.core.apis.UserApi.getBuddyRecommendContactArkJson(id.toString(), '');
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: arkJson.arkMsg}, data: { data: arkJson.arkMsg },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} else if (type === 'group') { } else if (type === 'group') {
const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString()); const arkJson = await this.core.apis.GroupApi.getGroupRecommendContactArkJson(id.toString());
return this.ob11ToRawConverters.json({ return this.ob11ToRawConverters.json({
data: {data: arkJson.arkJson}, data: { data: arkJson.arkJson },
type: OB11MessageDataType.json type: OB11MessageDataType.json
}, context); }, context);
} }
@@ -799,6 +809,7 @@ export class OneBotMsgApi {
message_id: msg.id!, message_id: msg.id!,
message_seq: msg.id!, message_seq: msg.id!,
real_id: msg.id!, real_id: msg.id!,
real_seq: msg.msgSeq,
message_type: msg.chatType == ChatType.KCHATTYPEGROUP ? 'group' : 'private', message_type: msg.chatType == ChatType.KCHATTYPEGROUP ? 'group' : 'private',
sender: { sender: {
user_id: +(msg.senderUin ?? 0), user_id: +(msg.senderUin ?? 0),
@@ -867,7 +878,7 @@ export class OneBotMsgApi {
element[key], element[key],
msg, msg,
element, element,
{parseMultMsg} { parseMultMsg }
); );
if (key === 'faceElement' && !parsedElement) { if (key === 'faceElement' && !parsedElement) {
return null; return null;
@@ -920,13 +931,13 @@ export class OneBotMsgApi {
) => Promise<SendMessageElement | undefined>; ) => Promise<SendMessageElement | undefined>;
const callResult = converter( const callResult = converter(
sendMsg, sendMsg,
{peer, deleteAfterSentFiles}, { peer, deleteAfterSentFiles },
)?.catch(undefined); )?.catch(undefined);
callResultList.push(callResult); callResultList.push(callResult);
} }
const ret = await Promise.all(callResultList); const ret = await Promise.all(callResultList);
const sendElements: SendMessageElement[] = ret.filter(ele => !!ele); const sendElements: SendMessageElement[] = ret.filter(ele => !!ele);
return {sendElements, deleteAfterSentFiles}; return { sendElements, deleteAfterSentFiles };
} }
async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) { async sendMsgWithOb11UniqueId(peer: Peer, sendElements: SendMessageElement[], deleteAfterSentFiles: string[]) {
@@ -937,16 +948,16 @@ export class OneBotMsgApi {
const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => { const calculateTotalSize = async (elements: SendMessageElement[]): Promise<number> => {
const sizePromises = elements.map(async element => { const sizePromises = elements.map(async element => {
switch (element.elementType) { switch (element.elementType) {
case ElementType.PTT: case ElementType.PTT:
return (await fsPromise.stat(element.pttElement.filePath)).size; return (await fsPromise.stat(element.pttElement.filePath)).size;
case ElementType.FILE: case ElementType.FILE:
return (await fsPromise.stat(element.fileElement.filePath)).size; return (await fsPromise.stat(element.fileElement.filePath)).size;
case ElementType.VIDEO: case ElementType.VIDEO:
return (await fsPromise.stat(element.videoElement.filePath)).size; return (await fsPromise.stat(element.videoElement.filePath)).size;
case ElementType.PIC: case ElementType.PIC:
return (await fsPromise.stat(element.picElement.sourcePath)).size; return (await fsPromise.stat(element.picElement.sourcePath)).size;
default: default:
return 0; return 0;
} }
}); });
const sizes = await Promise.all(sizePromises); const sizes = await Promise.all(sizePromises);
@@ -988,39 +999,32 @@ export class OneBotMsgApi {
} }
private async handleOb11FileLikeMessage( private async handleOb11FileLikeMessage(
{data: inputdata}: OB11MessageFileBase, { data: inputdata }: OB11MessageFileBase,
{deleteAfterSentFiles}: SendMessageContext { deleteAfterSentFiles }: SendMessageContext
) { ) {
let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? ''; let realUri = [inputdata.url, inputdata.file, inputdata.path].find(uri => uri && uri.trim()) ?? '';
if (!realUri) { if (!realUri) {
this.core.context.logger.logError('文件消息缺少参数', inputdata); this.core.context.logger.logError('文件消息缺少参数', inputdata);
throw new Error('文件消息缺少参数'); throw new Error('文件消息缺少参数');
} }
realUri = await this.handleObfuckName(realUri) ?? realUri;
const downloadFile = async (uri: string) => {
const {path, fileName, errMsg, success} = await uriToLocalFile(this.core.NapCatTempPath, uri);
if (!success) {
this.core.context.logger.logError('文件下载失败', errMsg);
throw new Error('文件下载失败: ' + errMsg);
}
return {path, fileName};
};
try { try {
const {path, fileName} = await downloadFile(realUri); const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, realUri);
if (!success) {
this.core.context.logger.logError('文件处理失败', errMsg);
throw new Error('文件处理失败: ' + errMsg);
}
deleteAfterSentFiles.push(path); deleteAfterSentFiles.push(path);
return {path, fileName: inputdata.name ?? fileName}; return { path, fileName: inputdata.name ?? fileName };
} catch { } catch (e: unknown) {
realUri = await this.handleObfuckName(realUri); throw new Error((e as Error).message);
const {path, fileName} = await downloadFile(realUri);
deleteAfterSentFiles.push(path);
return {path, fileName: inputdata.name ?? fileName};
} }
} }
async handleObfuckName(name: string) { async handleObfuckName(name: string) {
const contextMsgFile = FileNapCatOneBotUUID.decode(name); const contextMsgFile = FileNapCatOneBotUUID.decode(name);
if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) { if (contextMsgFile && contextMsgFile.msgId && contextMsgFile.elementId) {
const {peer, msgId, elementId} = contextMsgFile; const { peer, msgId, elementId } = contextMsgFile;
const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId); const rawMessage = (await this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgId]))?.msgList.find(msg => msg.msgId === msgId);
const mixElement = rawMessage?.elements.find(e => e.elementId === elementId); const mixElement = rawMessage?.elements.find(e => e.elementId === elementId);
const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement; const mixElementInner = mixElement?.videoElement ?? mixElement?.fileElement ?? mixElement?.pttElement ?? mixElement?.picElement;
@@ -1028,29 +1032,29 @@ export class OneBotMsgApi {
let url = ''; let url = '';
if (mixElement?.picElement && rawMessage) { if (mixElement?.picElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageImage | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.picElement?.(mixElement?.picElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageImage | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
if (mixElement?.videoElement && rawMessage) { if (mixElement?.videoElement && rawMessage) {
const tempData = const tempData =
await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, {parseMultMsg: false}) as OB11MessageVideo | undefined; await this.obContext.apis.MsgApi.rawToOb11Converters.videoElement?.(mixElement?.videoElement, rawMessage, mixElement, { parseMultMsg: false }) as OB11MessageVideo | undefined;
url = tempData?.data.url ?? ''; url = tempData?.data.url ?? '';
} }
return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', ''); return url !== '' ? url : await this.core.apis.FileApi.downloadMedia(msgId, peer.chatType, peer.peerUid, elementId, '', '');
} }
throw new Error('文件名解析失败'); return undefined;
} }
groupChangDecreseType2String(type: number): GroupDecreaseSubType { groupChangDecreseType2String(type: number): GroupDecreaseSubType {
switch (type) { switch (type) {
case 130: case 130:
return 'leave'; return 'leave';
case 131: case 131:
return 'kick'; return 'kick';
case 3: case 3:
return 'kick_me'; return 'kick_me';
default: default:
return 'kick'; return 'kick';
} }
} }
@@ -1069,7 +1073,7 @@ export class OneBotMsgApi {
} }
} }
return false; return false;
}, 1, 1000).catch(undefined); }, 1, 1000).catch(() => undefined);
if (dataNotify) { if (dataNotify) {
return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid; return !dataNotify.actionUser.uid ? dataNotify.user2.uid : dataNotify.actionUser.uid;
} }

View File

@@ -84,17 +84,19 @@ export class OneBotQuickActionApi {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag); let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
if (!notify) { if (!notify) {
notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag); notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(true, 100)).find(e => e.seq == flag);
return { doubt: true, notify };
} }
return notify; return { doubt: false, notify };
} }
async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) { async handleGroupRequest(request: OB11GroupRequestEvent, quickAction: QuickActionGroupRequest) {
const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag); const invite_notify = this.obContext.apis.MsgApi.notifyGroupInvite.get(request.flag);
const notify = invite_notify ?? await this.findNotify(request.flag); const { doubt, notify } = invite_notify ? { doubt: false, notify: invite_notify } : await this.findNotify(request.flag);
if (!isNull(quickAction.approve) && notify) { if (!isNull(quickAction.approve) && notify) {
this.core.apis.GroupApi.handleGroupRequest( this.core.apis.GroupApi.handleGroupRequest(
doubt,
notify, notify,
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
quickAction.reason, quickAction.reason,

View File

@@ -3,7 +3,7 @@ import { NapCatCore } from '@/core';
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent { export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = 'group_admin'; notice_type = 'group_admin';
sub_type: 'set' | 'unset'; sub_type: 'set' | 'unset';
constructor(core: NapCatCore, group_id: number, user_id: number, sub_type: 'set' | 'unset') { constructor(core: NapCatCore, group_id: number, user_id: number, sub_type: 'set' | 'unset') {
super(core, group_id, user_id); super(core, group_id, user_id);

View File

@@ -9,7 +9,8 @@ import { HttpServerConfig } from '@/onebot/config/config';
import { NapCatOneBot11Adapter } from '@/onebot'; import { NapCatOneBot11Adapter } from '@/onebot';
import { IOB11NetworkAdapter } from '@/onebot/network/adapter'; import { IOB11NetworkAdapter } from '@/onebot/network/adapter';
import json5 from 'json5'; import json5 from 'json5';
import { isFinished } from 'on-finished';
import typeis from 'type-is';
export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> { export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig> {
private app: Express | undefined; private app: Express | undefined;
private server: http.Server | undefined; private server: http.Server | undefined;
@@ -45,13 +46,23 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
this.app = undefined; this.app = undefined;
} }
private initializeServer() { private initializeServer() {
this.app = express(); this.app = express();
this.server = http.createServer(this.app); this.server = http.createServer(this.app);
this.app.use(cors()); this.app.use(cors());
this.app.use(express.urlencoded({ extended: true, limit: '5000mb' })); this.app.use(express.urlencoded({ extended: true, limit: '5000mb' }));
this.app.use((req, res, next) => { this.app.use((req, res, next) => {
if (isFinished(req)) {
next();
return;
}
if (!typeis.hasBody(req)) {
next();
return;
}
// 兼容处理没有带content-type的请求 // 兼容处理没有带content-type的请求
req.headers['content-type'] = 'application/json'; req.headers['content-type'] = 'application/json';
let rawData = ''; let rawData = '';
@@ -98,7 +109,7 @@ export class OB11HttpServerAdapter extends IOB11NetworkAdapter<HttpServerConfig>
if (req.method == 'get') { if (req.method == 'get') {
payload = req.query; payload = req.query;
} else if (req.query) { } else if (req.query) {
payload = { ...req.query, ...req.body }; payload = { ...req.body, ...req.query };
} }
if (req.path === '' || req.path === '/') { if (req.path === '' || req.path === '/') {
const hello = OB11Response.ok({}); const hello = OB11Response.ok({});

View File

@@ -10,6 +10,7 @@ export enum OB11MessageType {
// 消息接口定义 // 消息接口定义
export interface OB11Message { export interface OB11Message {
real_seq?: string;// 自行扩展
temp_source?: number; temp_source?: number;
message_sent_type?: string; message_sent_type?: string;
target_id?: number; // 自己发送消息/私聊消息 target_id?: number; // 自己发送消息/私聊消息

View File

@@ -236,11 +236,11 @@ async function initializeSession(
) { ) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const sessionListener = new NodeIKernelSessionListener(); const sessionListener = new NodeIKernelSessionListener();
sessionListener.onSessionInitComplete = (r: unknown) => { sessionListener.onOpentelemetryInit = (info) => {
if (r === 0) { if (info.is_init) {
resolve(); resolve();
} else { } else {
reject(new Error('登录异常' + r?.toString())); reject(new Error('opentelemetry init failed'));
} }
}; };
session.init( session.init(
@@ -260,7 +260,30 @@ async function initializeSession(
} }
}); });
} }
async function handleProxy(session: NodeIQQNTWrapperSession, logger: LogWrapper) {
if (process.env['NAPCAT_PROXY_PORT']) {
session.getMSFService().setNetworkProxy({
userName: '',
userPwd: '',
address: process.env['NAPCAT_PROXY_ADDRESS'] || '127.0.0.1',
port: +process.env['NAPCAT_PROXY_PORT'],
proxyType: 2,
domain: '',
isSocket: true
});
logger.logWarn('已设置代理', process.env['NAPCAT_PROXY_ADDRESS'], process.env['NAPCAT_PROXY_PORT']);
} else if (process.env['NAPCAT_PROXY_CLOSE']) {
session.getMSFService().setNetworkProxy({
userName: '',
userPwd: '',
address: '',
port: 0,
proxyType: 0,
domain: '',
isSocket: false
});
}
}
export async function NCoreInitShell() { export async function NCoreInitShell() {
console.log('NapCat Shell App Loading...'); console.log('NapCat Shell App Loading...');
const pathWrapper = new NapCatPathWrapper(); const pathWrapper = new NapCatPathWrapper();
@@ -286,7 +309,7 @@ export async function NCoreInitShell() {
await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion); await initializeEngine(engine, basicInfoWrapper, dataPathGlobal, systemPlatform, systemVersion);
await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname); await initializeLoginService(loginService, basicInfoWrapper, dataPathGlobal, systemVersion, hostname);
handleProxy(session, logger);
program.option('-q, --qq [number]', 'QQ号').parse(process.argv); program.option('-q, --qq [number]', 'QQ号').parse(process.argv);
const cmdOptions = program.opts(); const cmdOptions = program.opts();
const quickLoginUin = cmdOptions['qq']; const quickLoginUin = cmdOptions['qq'];
@@ -294,6 +317,7 @@ export async function NCoreInitShell() {
const dataTimestape = new Date().getTime().toString(); const dataTimestape = new Date().getTime().toString();
o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']); o3Service.reportAmgomWeather('login', 'a1', [dataTimestape, '0', '0']);
const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList); const selfInfo = await handleLogin(loginService, logger, pathWrapper, quickLoginUin, historyLoginList);
const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129'; const amgomDataPiece = 'eb1fd6ac257461580dc7438eb099f23aae04ca679f4d88f53072dc56e3bb1129';
o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex'))); o3Service.setAmgomDataPiece(basicInfoWrapper.QQVersionAppid, new Uint8Array(Buffer.from(amgomDataPiece, 'hex')));

View File

@@ -10,7 +10,7 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router'; import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors'; import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url'; import { createUrl } from '@webapi/utils/url';
import { sendError, sendSuccess } from '@webapi/utils/response'; import { sendError } from '@webapi/utils/response';
import { join } from 'node:path'; import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager'; import { terminalManager } from '@webapi/terminal/terminal_manager';
import multer from 'multer'; // 新增引入multer用于错误捕获 import multer from 'multer'; // 新增引入multer用于错误捕获
@@ -26,16 +26,44 @@ const server = createServer(app);
*/ */
export let WebUiConfig: WebUiConfigWrapper; export let WebUiConfig: WebUiConfigWrapper;
export let webUiPathWrapper: NapCatPathWrapper; export let webUiPathWrapper: NapCatPathWrapper;
const MAX_PORT_TRY = 100;
import * as net from 'node:net';
import { WebUiDataRuntime } from './src/helper/Data';
export let webUiRuntimePort = 6099;
export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
try {
await tryUseHost(parsedConfig.host);
const port = await tryUsePort(parsedConfig.port, parsedConfig.host);
return [parsedConfig.host, port, parsedConfig.token];
} catch (error) {
console.log('host或port不可用', error);
return ['', 0, ''];
}
}
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) { export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
webUiPathWrapper = pathWrapper; webUiPathWrapper = pathWrapper;
WebUiConfig = new WebUiConfigWrapper(); WebUiConfig = new WebUiConfigWrapper();
const config = await WebUiConfig.GetWebUIConfig(); const [host, port, token] = await InitPort(await WebUiConfig.GetWebUIConfig());
if (config.port == 0) { webUiRuntimePort = port;
if (port == 0) {
logger.log('[NapCat] [WebUi] Current WebUi is not run.'); logger.log('[NapCat] [WebUi] Current WebUi is not run.');
return; return;
} }
setTimeout(async () => {
let autoLoginAccount = process.env['NAPCAT_QUICK_ACCOUNT'] || WebUiConfig.getAutoLoginAccount();
if (autoLoginAccount) {
try {
const { result, message } = await WebUiDataRuntime.requestQuickLogin(autoLoginAccount);
if (!result) {
throw new Error(message);
}
console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
} catch (error) {
console.log(`[NapCat] [WebUi] Auto login account failed.` + error);
}
}
}, 30000);
// ------------注册中间件------------ // ------------注册中间件------------
// 使用express的json中间件 // 使用express的json中间件
app.use(express.json()); app.use(express.json());
@@ -46,15 +74,32 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 如果是webui字体文件挂载字体文件 // 如果是webui字体文件挂载字体文件
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => { app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist(); const isFontExist = await WebUiConfig.CheckWebUIFontExist();
console.log(isFontExist, 'isFontExist');
if (isFontExist) { if (isFontExist) {
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath()); res.sendFile(WebUiConfig.GetWebUIFontPath());
} else { } else {
next(); next();
} }
}); });
// 如果是自定义色彩构建一个css文件
app.use('/files/theme.css', async (_req, res) => {
const colors = await WebUiConfig.GetTheme();
let css = ':root, .light, [data-theme="light"] {';
for (const key in colors.light) {
css += `${key}: ${colors.light[key]};`;
}
css += '}';
css += '.dark, [data-theme="dark"] {';
for (const key in colors.dark) {
css += `${key}: ${colors.dark[key]};`;
}
css += '}';
res.send(css);
});
// ------------中间件结束------------ // ------------中间件结束------------
// ------------挂载路由------------ // ------------挂载路由------------
@@ -75,7 +120,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 初始服务(先放个首页) // 初始服务(先放个首页)
app.all('/', (_req, res) => { app.all('/', (_req, res) => {
sendSuccess(res, null, 'NapCat WebAPI is now running!'); res.status(301).header('Location', '/webui').send();
}); });
// 错误处理中间件捕获multer的错误 // 错误处理中间件捕获multer的错误
@@ -92,16 +137,74 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
}); });
// ------------启动服务------------ // ------------启动服务------------
server.listen(config.port, config.host, async () => { server.listen(port, host, async () => {
// 启动后打印出相关地址 // 启动后打印出相关地址
const port = config.port.toString(), let searchParams = { token: token };
searchParams = { token: config.token }; if (host !== '' && host !== '0.0.0.0') {
if (config.host !== '' && config.host !== '0.0.0.0') {
logger.log( logger.log(
`[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(config.host, port, '/webui', searchParams)}` `[NapCat] [WebUi] WebUi User Panel Url: ${createUrl(host, port.toString(), '/webui', searchParams)}`
); );
} }
logger.log(`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port, '/webui', searchParams)}`); logger.log(
`[NapCat] [WebUi] WebUi Local Panel Url: ${createUrl('127.0.0.1', port.toString(), '/webui', searchParams)}`
);
}); });
// ------------Over------------ // ------------Over------------
} }
async function tryUseHost(host: string): Promise<string> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(host);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRNOTAVAIL') {
reject(new Error('主机地址验证失败,可能为非本机地址'));
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听 让系统随机分配一个端口
server.listen(0, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(port);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
if (tryCount < MAX_PORT_TRY) {
// 使用循环代替递归
resolve(tryUsePort(port + 1, host, tryCount + 1));
} else {
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
}
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听端口
server.listen(port, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}

View File

@@ -7,6 +7,15 @@ import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess, sendError } from '@webapi/utils/response'; import { sendSuccess, sendError } from '@webapi/utils/response';
import { isEmpty } from '@webapi/utils/check'; import { isEmpty } from '@webapi/utils/check';
// 检查是否使用默认Token
export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
const webuiToken = await WebUiConfig.GetWebUIConfig();
if (webuiToken.token === 'napcat') {
return sendSuccess(res, true);
}
return sendSuccess(res, false);
};
// 登录 // 登录
export const LoginHandler: RequestHandler = async (req, res) => { export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置 // 获取WebUI配置
@@ -93,7 +102,7 @@ export const UpdateTokenHandler: RequestHandler = async (req, res) => {
try { try {
// 注销当前的Token // 注销当前的Token
if (authorization) { if (authorization) {
const CredentialBase64: string = authorization.split(' ')[1]; const CredentialBase64: string = authorization.split(' ')[1] as string;
const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString()); const Credential = JSON.parse(Buffer.from(CredentialBase64, 'base64').toString());
AuthHelper.revokeCredential(Credential); AuthHelper.revokeCredential(Credential);
} }

View File

@@ -2,14 +2,25 @@ import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@webapi/helper/Data'; import { WebUiDataRuntime } from '@webapi/helper/Data';
import { sendSuccess } from '@webapi/utils/response'; import { sendSuccess } from '@webapi/utils/response';
import { WebUiConfig } from '@/webui';
export const PackageInfoHandler: RequestHandler = (_, res) => { export const PackageInfoHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getPackageJson(); const data = WebUiDataRuntime.getPackageJson();
sendSuccess(res, data); sendSuccess(res, data);
}; };
export const QQVersionHandler: RequestHandler = (_, res) => { export const QQVersionHandler: RequestHandler = (_, res) => {
const data = WebUiDataRuntime.getQQVersion(); const data = WebUiDataRuntime.getQQVersion();
sendSuccess(res, data); sendSuccess(res, data);
}; };
export const GetThemeConfigHandler: RequestHandler = async (_, res) => {
const data = await WebUiConfig.GetTheme();
sendSuccess(res, data);
};
export const SetThemeConfigHandler: RequestHandler = async (req, res) => {
const { theme } = req.body;
await WebUiConfig.UpdateTheme(theme);
sendSuccess(res, { message: '更新成功' });
};

View File

@@ -7,9 +7,9 @@ import os from 'os';
import compressing from 'compressing'; import compressing from 'compressing';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import multer from 'multer'; import multer from 'multer';
import { WebUiConfigWrapper } from '../helper/config';
import webUIFontUploader from '../uploader/webui_font'; import webUIFontUploader from '../uploader/webui_font';
import diskUploader from '../uploader/disk'; import diskUploader from '../uploader/disk';
import { WebUiConfig } from '@/webui';
const isWindows = os.platform() === 'win32'; const isWindows = os.platform() === 'win32';
@@ -384,8 +384,8 @@ export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
// 删除WebUI字体文件处理方法 // 删除WebUI字体文件处理方法
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => { export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
try { try {
const fontPath = WebUiConfigWrapper.GetWebUIFontPath(); const fontPath = WebUiConfig.GetWebUIFontPath();
const exists = await WebUiConfigWrapper.CheckWebUIFontExist(); const exists = await WebUiConfig.CheckWebUIFontExist();
if (!exists) { if (!exists) {
return sendSuccess(res, true); return sendSuccess(res, true);

View File

@@ -1,9 +1,10 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response'; import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config';
import { logSubscription } from '@/common/log'; import { logSubscription } from '@/common/log';
import { terminalManager } from '../terminal/terminal_manager'; import { terminalManager } from '../terminal/terminal_manager';
import { WebUiConfig } from '@/webui';
// 判断是否是 macos
const isMacOS = process.platform === 'darwin';
// 日志记录 // 日志记录
export const LogHandler: RequestHandler = async (req, res) => { export const LogHandler: RequestHandler = async (req, res) => {
const filename = req.query['id']; const filename = req.query['id'];
@@ -14,13 +15,13 @@ export const LogHandler: RequestHandler = async (req, res) => {
if (filename.includes('..')) { if (filename.includes('..')) {
return sendError(res, 'ID不合法'); return sendError(res, 'ID不合法');
} }
const logContent = await WebUiConfigWrapper.GetLogContent(filename); const logContent = await WebUiConfig.GetLogContent(filename);
return sendSuccess(res, logContent); return sendSuccess(res, logContent);
}; };
// 日志列表 // 日志列表
export const LogListHandler: RequestHandler = async (_, res) => { export const LogListHandler: RequestHandler = async (_, res) => {
const logList = await WebUiConfigWrapper.GetLogsList(); const logList = await WebUiConfig.GetLogsList();
return sendSuccess(res, logList); return sendSuccess(res, logList);
}; };
@@ -43,6 +44,9 @@ export const LogRealTimeHandler: RequestHandler = async (req, res) => {
// 终端相关处理器 // 终端相关处理器
export const CreateTerminalHandler: RequestHandler = async (req, res) => { export const CreateTerminalHandler: RequestHandler = async (req, res) => {
if (isMacOS) {
return sendError(res, 'MacOS不支持终端');
}
try { try {
const { cols, rows } = req.body; const { cols, rows } = req.body;
const { id } = terminalManager.createTerminal(cols, rows); const { id } = terminalManager.createTerminal(cols, rows);

View File

@@ -3,9 +3,10 @@ import { RequestHandler } from 'express';
import { WebUiDataRuntime } from '@webapi/helper/Data'; import { WebUiDataRuntime } from '@webapi/helper/Data';
import { isEmpty } from '@webapi/utils/check'; import { isEmpty } from '@webapi/utils/check';
import { sendError, sendSuccess } from '@webapi/utils/response'; import { sendError, sendSuccess } from '@webapi/utils/response';
import { WebUiConfig } from '@/webui';
// 获取QQ登录二维码 // 获取QQ登录二维码
export const QQGetQRcodeHandler: RequestHandler = async (req, res) => { export const QQGetQRcodeHandler: RequestHandler = async (_, res) => {
// 判断是否已经登录 // 判断是否已经登录
if (WebUiDataRuntime.getQQLoginStatus()) { if (WebUiDataRuntime.getQQLoginStatus()) {
// 已经登录 // 已经登录
@@ -25,7 +26,7 @@ export const QQGetQRcodeHandler: RequestHandler = async (req, res) => {
}; };
// 获取QQ登录状态 // 获取QQ登录状态
export const QQCheckLoginStatusHandler: RequestHandler = async (req, res) => { export const QQCheckLoginStatusHandler: RequestHandler = async (_, res) => {
const data = { const data = {
isLogin: WebUiDataRuntime.getQQLoginStatus(), isLogin: WebUiDataRuntime.getQQLoginStatus(),
qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(), qrcodeurl: WebUiDataRuntime.getQQLoginQrcodeURL(),
@@ -74,3 +75,16 @@ export const getQQLoginInfoHandler: RequestHandler = async (_, res) => {
const data = WebUiDataRuntime.getQQLoginInfo(); const data = WebUiDataRuntime.getQQLoginInfo();
return sendSuccess(res, data); return sendSuccess(res, data);
}; };
// 获取自动登录QQ账号
export const getAutoLoginAccountHandler: RequestHandler = async (_, res) => {
const data = WebUiConfig.getAutoLoginAccount();
return sendSuccess(res, data);
};
// 设置自动登录QQ账号
export const setAutoLoginAccountHandler: RequestHandler = async (req, res) => {
const { uin } = req.body;
await WebUiConfig.UpdateAutoLoginAccount(uin);
return sendSuccess(res, null);
};

View File

@@ -1,167 +1,86 @@
import { webUiPathWrapper } from '@/webui'; import { webUiPathWrapper } from '@/webui';
import { Type, Static } from '@sinclair/typebox';
import Ajv from 'ajv';
import fs, { constants } from 'node:fs/promises'; import fs, { constants } from 'node:fs/promises';
import * as net from 'node:net';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { deepMerge } from '../utils/object';
import { themeType } from '../types/theme';
// 限制尝试端口的次数,避免死循环 // 限制尝试端口的次数,避免死循环
const MAX_PORT_TRY = 100;
async function tryUseHost(host: string): Promise<string> { // 定义配置的类型
return new Promise((resolve, reject) => { const WebUiConfigSchema = Type.Object({
try { host: Type.String({ default: '0.0.0.0' }),
const server = net.createServer(); port: Type.Number({ default: 6099 }),
server.on('listening', () => { token: Type.String({ default: 'napcat' }),
server.close(); loginRate: Type.Number({ default: 10 }),
resolve(host); autoLoginAccount: Type.String({ default: '' }),
}); theme: themeType,
});
server.on('error', (err: any) => { export type WebUiConfigType = Static<typeof WebUiConfigSchema>;
if (err.code === 'EADDRNOTAVAIL') {
reject(new Error('主机地址验证失败,可能为非本机地址'));
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听 让系统随机分配一个端口
server.listen(0, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
async function tryUsePort(port: number, host: string, tryCount: number = 0): Promise<number> {
return new Promise((resolve, reject) => {
try {
const server = net.createServer();
server.on('listening', () => {
server.close();
resolve(port);
});
server.on('error', (err: any) => {
if (err.code === 'EADDRINUSE') {
if (tryCount < MAX_PORT_TRY) {
// 使用循环代替递归
resolve(tryUsePort(port + 1, host, tryCount + 1));
} else {
reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`));
}
} else {
reject(new Error(`遇到错误: ${err.code}`));
}
});
// 尝试监听端口
server.listen(port, host);
} catch (error) {
// 这里捕获到的错误应该是启动服务器时的同步错误
reject(new Error(`服务器启动时发生错误: ${error}`));
}
});
}
// 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件 // 读取当前目录下名为 webui.json 的配置文件,如果不存在则创建初始化配置文件
export class WebUiConfigWrapper { export class WebUiConfigWrapper {
WebUiConfigData: WebUiConfigType | undefined = undefined; WebUiConfigData: WebUiConfigType | undefined = undefined;
private applyDefaults<T>(obj: Partial<T>, defaults: T): T { private validateAndApplyDefaults(config: Partial<WebUiConfigType>): WebUiConfigType {
const result = { ...defaults } as T; new Ajv({ coerceTypes: true, useDefaults: true }).compile(WebUiConfigSchema)(config);
for (const key in obj) { return config as WebUiConfigType;
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { }
result[key] = this.applyDefaults(obj[key], defaults[key]);
} else if (obj[key] !== undefined) { private async ensureConfigFileExists(configPath: string): Promise<void> {
result[key] = obj[key] as T[Extract<keyof T, string>]; const configExists = await fs
} .access(configPath, constants.F_OK)
.then(() => true)
.catch(() => false);
if (!configExists) {
await fs.writeFile(configPath, JSON.stringify(this.validateAndApplyDefaults({}), null, 4));
}
}
private async readAndValidateConfig(configPath: string): Promise<WebUiConfigType> {
const fileContent = await fs.readFile(configPath, 'utf-8');
return this.validateAndApplyDefaults(JSON.parse(fileContent));
}
private async writeConfig(configPath: string, config: WebUiConfigType): Promise<void> {
const hasWritePermission = await fs
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false);
if (hasWritePermission) {
await fs.writeFile(configPath, JSON.stringify(config, null, 4));
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
} }
return result;
} }
async GetWebUIConfig(): Promise<WebUiConfigType> { async GetWebUIConfig(): Promise<WebUiConfigType> {
if (this.WebUiConfigData) { if (this.WebUiConfigData) {
return this.WebUiConfigData; return this.WebUiConfigData;
} }
const defaultconfig: WebUiConfigType = {
host: '0.0.0.0',
port: 6099,
token: '', // 默认先填空,空密码无法登录
loginRate: 3,
};
try {
defaultconfig.token = Math.random().toString(36).slice(2); //生成随机密码
} catch (e) {
console.log('随机密码生成失败', e);
}
try { try {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
await this.ensureConfigFileExists(configPath);
if ( const parsedConfig = await this.readAndValidateConfig(configPath);
!(await fs
.access(configPath, constants.F_OK)
.then(() => true)
.catch(() => false))
) {
await fs.writeFile(configPath, JSON.stringify(defaultconfig, null, 4));
}
const fileContent = await fs.readFile(configPath, 'utf-8');
const parsedConfig = this.applyDefaults(JSON.parse(fileContent) as Partial<WebUiConfigType>, defaultconfig);
if (
await fs
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false)
) {
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 4));
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
const [host_err, host] = await tryUseHost(parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (host_err) {
console.log('host不可用', host_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.host = host;
const [port_err, port] = await tryUsePort(parsedConfig.port, parsedConfig.host)
.then((data) => [null, data])
.catch((err) => [err, null]);
if (port_err) {
console.log('port不可用', port_err);
parsedConfig.port = 0; // 设置为0禁用WebUI
} else {
parsedConfig.port = port;
}
}
this.WebUiConfigData = parsedConfig; this.WebUiConfigData = parsedConfig;
return this.WebUiConfigData; return this.WebUiConfigData;
} catch (e) { } catch (e) {
console.log('读取配置文件失败', e); console.log('读取配置文件失败', e);
return this.validateAndApplyDefaults({});
} }
return defaultconfig; // 理论上这行代码到不了,到了只能返回默认配置了
} }
async UpdateWebUIConfig(newConfig: Partial<WebUiConfigType>): Promise<void> { async UpdateWebUIConfig(newConfig: Partial<WebUiConfigType>): Promise<void> {
const configPath = resolve(webUiPathWrapper.configPath, './webui.json'); const configPath = resolve(webUiPathWrapper.configPath, './webui.json');
const currentConfig = await this.GetWebUIConfig(); const currentConfig = await this.GetWebUIConfig();
const updatedConfig = this.applyDefaults(newConfig, currentConfig); const mergedConfig = deepMerge({ ...currentConfig }, newConfig);
const updatedConfig = this.validateAndApplyDefaults(mergedConfig);
if ( await this.writeConfig(configPath, updatedConfig);
await fs this.WebUiConfigData = updatedConfig;
.access(configPath, constants.W_OK)
.then(() => true)
.catch(() => false)
) {
await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 4));
this.WebUiConfigData = updatedConfig;
} else {
console.warn(`文件: ${configPath} 没有写入权限, 配置的更改部分可能会在重启后还原.`);
}
} }
async UpdateToken(oldToken: string, newToken: string): Promise<void> { async UpdateToken(oldToken: string, newToken: string): Promise<void> {
@@ -173,53 +92,53 @@ export class WebUiConfigWrapper {
} }
// 获取日志文件夹路径 // 获取日志文件夹路径
public static async GetLogsPath(): Promise<string> { async GetLogsPath(): Promise<string> {
return resolve(webUiPathWrapper.logsPath); return resolve(webUiPathWrapper.logsPath);
} }
// 获取日志列表 // 获取日志列表
public static async GetLogsList(): Promise<string[]> { async GetLogsList(): Promise<string[]> {
if ( const logsPath = resolve(webUiPathWrapper.logsPath);
await fs const logsExist = await fs
.access(webUiPathWrapper.logsPath, constants.F_OK) .access(logsPath, constants.F_OK)
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false);
) { if (logsExist) {
return (await fs.readdir(webUiPathWrapper.logsPath)) return (await fs.readdir(logsPath))
.filter((file) => file.endsWith('.log')) .filter((file) => file.endsWith('.log'))
.map((file) => file.replace('.log', '')); .map((file) => file.replace('.log', ''));
} }
return []; return [];
} }
// 获取指定日志文件内容 // 获取指定日志文件内容
public static async GetLogContent(filename: string): Promise<string> { async GetLogContent(filename: string): Promise<string> {
const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`); const logPath = resolve(webUiPathWrapper.logsPath, `${filename}.log`);
if ( const logExists = await fs
await fs .access(logPath, constants.R_OK)
.access(logPath, constants.R_OK) .then(() => true)
.then(() => true) .catch(() => false);
.catch(() => false) if (logExists) {
) {
return await fs.readFile(logPath, 'utf-8'); return await fs.readFile(logPath, 'utf-8');
} }
return ''; return '';
} }
// 获取字体文件夹内的字体列表 // 获取字体文件夹内的字体列表
public static async GetFontList(): Promise<string[]> { async GetFontList(): Promise<string[]> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
if ( const fontsExist = await fs
await fs .access(fontsPath, constants.F_OK)
.access(fontsPath, constants.F_OK) .then(() => true)
.then(() => true) .catch(() => false);
.catch(() => false) if (fontsExist) {
) {
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf')); return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
} }
return []; return [];
} }
// 判断字体是否存在webui.woff // 判断字体是否存在webui.woff
public static async CheckWebUIFontExist(): Promise<boolean> { async CheckWebUIFontExist(): Promise<boolean> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
return await fs return await fs
.access(resolve(fontsPath, './webui.woff'), constants.F_OK) .access(resolve(fontsPath, './webui.woff'), constants.F_OK)
@@ -228,7 +147,33 @@ export class WebUiConfigWrapper {
} }
// 获取webui字体文件路径 // 获取webui字体文件路径
public static GetWebUIFontPath(): string { GetWebUIFontPath(): string {
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff'); return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
} }
getAutoLoginAccount(): string | undefined {
return this.WebUiConfigData?.autoLoginAccount;
}
// 获取自动登录账号
async GetAutoLoginAccount(): Promise<string> {
return (await this.GetWebUIConfig()).autoLoginAccount;
}
// 更新自动登录账号
async UpdateAutoLoginAccount(uin: string): Promise<void> {
await this.UpdateWebUIConfig({ autoLoginAccount: uin });
}
// 获取主题内容
async GetTheme(): Promise<WebUiConfigType['theme']> {
const config = await this.GetWebUIConfig();
return config.theme;
}
// 更新主题内容
async UpdateTheme(theme: WebUiConfigType['theme']): Promise<void> {
await this.UpdateWebUIConfig({ theme: theme });
}
} }

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