Compare commits

..

71 Commits

Author SHA1 Message Date
手瓜一十雪
1823a17bf9 fix 2025-03-27 12:12:12 +08:00
手瓜一十雪
111ec4c18e fix 2025-03-26 23:03:55 +08:00
手瓜一十雪
4faddb9e38 refactor 2025-03-26 22:50:37 +08:00
手瓜一十雪
41199ec88e fix 2025-03-26 22:40:33 +08:00
手瓜一十雪
c141975804 fix 2025-03-26 22:40:14 +08:00
手瓜一十雪
673a175ddf fix: 烘焙raw 2025-03-26 21:51:51 +08:00
手瓜一十雪
12eacd3530 feat: 移除ws 2025-03-21 20:54:59 +08:00
手瓜一十雪
030f0551fd feat: 移除部分淘汰代码 2025-03-21 20:47:38 +08:00
Mlikiowa
47f5947410 release: v4.7.8 2025-03-21 12:43:58 +00:00
手瓜一十雪
aaefa2e83c fix: error 2025-03-21 20:43:41 +08:00
Mlikiowa
f8e92f7c8d release: v4.7.7 2025-03-21 11:48:29 +00:00
手瓜一十雪
b6430e6eb6 fix: 优化 2025-03-21 19:44:31 +08:00
手瓜一十雪
e60605c7bb feat: 简化代码逻辑 2025-03-21 19:40:47 +08:00
手瓜一十雪
58d2bd3c81 Revert "fix: #883"
This reverts commit 79aa1dc67f.
2025-03-20 10:57:57 +08:00
手瓜一十雪
6534d05b76 Revert "fix"
This reverts commit 2d7de174c5.
2025-03-20 10:57:54 +08:00
手瓜一十雪
2d7de174c5 fix 2025-03-20 10:32:30 +08:00
手瓜一十雪
79aa1dc67f fix: #883 2025-03-20 10:32:13 +08:00
Mlikiowa
7792ad9ea0 release: v4.7.6 2025-03-19 07:35:12 +00:00
手瓜一十雪
be6671923b feat: 33139 2025-03-19 15:34:33 +08:00
手瓜一十雪
0fa1b3f044 feat: 33139 2025-03-19 15:26:55 +08:00
手瓜一十雪
4ab751696b Revert "fix: image size"
This reverts commit 2759a34d96.
2025-03-19 11:58:19 +08:00
手瓜一十雪
dce4eedf7d Revert "fix: moduleResolution"
This reverts commit 9ab776d53a.
2025-03-19 11:58:16 +08:00
手瓜一十雪
129b67b751 Revert "chore(deps-dev): bump image-size from 1.2.0 to 2.0.1"
This reverts commit e3feb6a73c.
2025-03-19 11:57:55 +08:00
手瓜一十雪
9ab776d53a fix: moduleResolution 2025-03-19 11:54:29 +08:00
手瓜一十雪
2759a34d96 fix: image size 2025-03-19 11:09:57 +08:00
手瓜一十雪
2f9f42750e Merge branch 'main' of https://github.com/NapNeko/NapCatQQ 2025-03-19 10:45:59 +08:00
手瓜一十雪
30abd1f904 fix: #884 2025-03-19 10:45:56 +08:00
手瓜一十雪
008075466e Merge pull request #887 from NapNeko/dependabot/npm_and_yarn/eslint-import-resolver-typescript-4.0.0
chore(deps-dev): bump eslint-import-resolver-typescript from 3.9.1 to 4.0.0
2025-03-19 10:39:33 +08:00
手瓜一十雪
5b4035c320 Merge pull request #888 from NapNeko/dependabot/npm_and_yarn/image-size-2.0.1
chore(deps-dev): bump image-size from 1.2.0 to 2.0.1
2025-03-19 10:39:21 +08:00
dependabot[bot]
e3feb6a73c chore(deps-dev): bump image-size from 1.2.0 to 2.0.1
Bumps [image-size](https://github.com/image-size/image-size) from 1.2.0 to 2.0.1.
- [Release notes](https://github.com/image-size/image-size/releases)
- [Commits](https://github.com/image-size/image-size/compare/v1.2.0...v2.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 09:23:32 +00:00
dependabot[bot]
40fe73317d chore(deps-dev): bump eslint-import-resolver-typescript
Bumps [eslint-import-resolver-typescript](https://github.com/import-js/eslint-import-resolver-typescript) from 3.9.1 to 4.0.0.
- [Release notes](https://github.com/import-js/eslint-import-resolver-typescript/releases)
- [Changelog](https://github.com/import-js/eslint-import-resolver-typescript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-import-resolver-typescript/compare/v3.9.1...v4.0.0)

---
updated-dependencies:
- dependency-name: eslint-import-resolver-typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 09:21:48 +00:00
pk5ls20
073745030c fix: #843
- maybe a temporary solution
2025-03-16 20:07:47 +08:00
Mlikiowa
c523437506 release: v4.7.5 2025-03-16 08:08:15 +00:00
手瓜一十雪
9eef570d37 fix: network prepare 2025-03-16 16:07:51 +08:00
Mlikiowa
be37b8cbbd release: v4.7.4 2025-03-16 07:57:45 +00:00
手瓜一十雪
c635496677 fix: msf Status 2025-03-16 15:57:27 +08:00
Mlikiowa
8753ecfd92 release: v4.7.3 2025-03-16 03:57:48 +00:00
手瓜一十雪
5eda1f2870 fix: quick login 2025-03-16 11:57:28 +08:00
Mlikiowa
d5a60074f7 release: v4.7.2 2025-03-16 03:55:17 +00:00
手瓜一十雪
91df57d932 fix: async error 2025-03-16 11:55:00 +08:00
Mlikiowa
e27d4c4302 release: v4.7.1 2025-03-16 03:40:48 +00:00
手瓜一十雪
55847f6e10 fix: quick login 延迟问题 2025-03-16 11:39:38 +08:00
手瓜一十雪
b39d8bae27 fix: login timer / add:不规范的promise 2025-03-16 10:42:15 +08:00
手瓜一十雪
b0cf23f775 doc: security 2025-03-16 09:36:27 +08:00
手瓜一十雪
c641246056 doc: code of conduct 2025-03-16 09:33:06 +08:00
Mlikiowa
1e5bc9bbea release: v4.7.0 2025-03-16 01:13:17 +00:00
手瓜一十雪
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
60 changed files with 726 additions and 463 deletions

View File

@@ -6,5 +6,7 @@
"tsconfig.json": "tsconfig.*.json, env.d.ts, vite.config.ts",
"package.json": "package-lock.json, eslint*, .prettier*, .editorconfig, manifest.json, logo.png, .gitignore, LICENSE"
},
"css.customData": [".vscode/tailwindcss.json"],
"css.customData": [
".vscode/tailwindcss.json"
],
}

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
nanaeonn@outlook.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,67 +1,62 @@
<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)
_Modern protocol-side framework implemented based on NTQQ._
> 云起兮风生,心向远方兮路未曾至.
</div>
---
## 欢迎回家
NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 特性介绍
- [x] **安装简单**:就算是笨蛋也能使用
- [x] **性能友好**:就算是低内存也能使用
- [x] **接口丰富**:就算是没有也能使用
- [x] **稳定好用**:就算是被捉也能使用
## Welcome
+ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ.
- NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现
## 使用框架
## Feature
+ **Easy to Use**
- 作为初学者能够轻松使用.
+ **Quick and Efficient**
- 在低内存操作系统长时运行.
+ **Rich API Interface**
- 完整实现了大部分标准接口.
+ **Stable and Reliable**
- 持续稳定的开发与维护.
## Quick Start
可前往 [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
不过最最重要的 还是需要感谢屏幕前的你哦~
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
---
## 特殊感谢
[LLOneBot](https://github.com/LLOneBot/LLOneBot) 相关的开发曾参与本项目
## License
本项目采用 混合协议 开源,因此使用本项目时,你需要注意以下几点:
1. 第三方库代码或修改部分遵循其原始开源许可.
2. 本项目获取部分项目授权而不受部分约束
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
## 开源附加
任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 4.0 | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
you should open an issue

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.17-30899",
"verHash": "ececf273",
"linuxVersion": "3.2.15-30899",
"linuxVerHash": "63c751e8",
"type": "module",
"version": "9.9.18-32793",
"verHash": "d43f097e",
"linuxVersion": "3.2.16-32793",
"linuxVerHash": "ee4bd910",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -17,10 +16,27 @@
"bin": {
"qd": "externals/devtools/cli/index.js"
},
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"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,
"isByteCodeShell": true,
"platform": "win32",
"eleArch": "x64"
}
}

View File

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

View File

@@ -136,7 +136,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
</div>
<Card
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">
<span className="mr-2"></span>

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.6.1",
"version": "4.7.8",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -45,7 +45,7 @@
"cors": "^2.8.5",
"esbuild": "0.25.0",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0",
"fast-xml-parser": "^4.3.6",

View File

@@ -1 +1 @@
export const napCatVersion = '4.6.1';
export const napCatVersion = '4.7.8';

View File

@@ -5,6 +5,9 @@ export async function runTask<T, R>(workerScript: string, taskData: T): Promise<
try {
return await new Promise<R>((resolve, reject) => {
worker.on('message', (result: R) => {
if ((result as any)?.error) {
reject(new Error((result as { error: string }).error));
}
resolve(result);
});

View File

@@ -42,7 +42,7 @@ export class NTQQFileApi {
this.core = core;
this.rkeyManager = new RkeyManager([
'https://ss.xingzhige.com/music_card/rkey', // 国内
'https://rkey.napneko.icu/rkeys' // Cloudflare
'https://secret-service.bietiaop.com/rkeys',//国内
],
this.context.logger
);

View File

@@ -27,6 +27,9 @@ export class NTQQGroupApi {
this.core = core;
}
async setGroupRemark(groupCode: string, remark: string) {
return this.context.session.getGroupService().modifyGroupRemark(groupCode, remark);
}
async fetchGroupDetail(groupCode: string) {
const [, detailInfo] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelGroupService/getGroupDetailInfo',
@@ -41,13 +44,22 @@ export class NTQQGroupApi {
}
async initApi() {
this.initCache().then().catch(e => this.context.logger.logError(e));
await this.initCache().then().catch(e => this.context.logger.logError(e));
}
async initCache() {
let promises: Promise<void>[] = [];
for (const group of await this.getGroups(true)) {
this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
let user = await this.refreshGroupMemberCache(group.groupCode, false).then().catch(e => this.context.logger.logError(e));
if (user) {
for (const member of user) {
let promise = this.core.apis.UserApi.fetchUserDetailInfoV3(member[1].uid).then(_ => void 0).catch(e => this.context.logger.logError(e));
promises.push(promise);
}
}
}
await Promise.all(promises);
this.context.logger.logDebug('[NapCat] [Mark] 群成员缓存初始化完成');
}
async fetchGroupEssenceList(groupCode: string) {
@@ -345,9 +357,9 @@ export class NTQQGroupApi {
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(
false,
doubt,
{
operateType: operateType,
targetMsg: {

View File

@@ -136,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) {
return this.context.session.getMsgService().setMsgRead(peer);
}

View File

@@ -1,4 +1,4 @@
import { ModifyProfileParams, User, UserDetailSource } from '@/core/types';
import { ModifyProfileParams, UserDetailSource } from '@/core/types';
import { RequestUtil } from '@/common/request';
import { InstanceContext, NapCatCore, ProfileBizType } from '..';
import { solveAsyncProblem } from '@/common/helper';
@@ -77,7 +77,7 @@ export class NTQQUserApi {
return this.context.session.getGroupService().setHeader(gc, filePath);
}
async fetchUserDetailInfo(uid: string, mode: UserDetailSource = UserDetailSource.KDB) {
async fetchUserDetailInfoV2(uid: string, mode: UserDetailSource = UserDetailSource.KDB) {
const [, profile] = await this.core.eventWrapper.callNormalEventV2(
'NodeIKernelProfileService/fetchUserDetailInfo',
'NodeIKernelProfileListener/onUserDetailInfoChanged',
@@ -90,46 +90,31 @@ export class NTQQUserApi {
() => true,
(profile) => profile.uid === uid,
);
const RetUser: User = {
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
...profile.simpleInfo.baseInfo,
...profile.simpleInfo.coreInfo,
qqLevel: profile.commonExt?.qqLevel,
age: profile.simpleInfo.baseInfo.age,
pendantId: '',
nick: profile.simpleInfo.coreInfo.nick || '',
};
return RetUser;
return profile;
}
async getUserDetailInfo(uid: string): Promise<User> {
let retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(uid, UserDetailSource.KDB), uid);
async fetchUserDetailInfoV3(uid: string) {
let cache = await this.fetchUserDetailInfoV2(uid, UserDetailSource.KDB);
if (!cache.commonExt) {
cache = await this.fetchUserDetailInfoV2(uid, UserDetailSource.KSERVER);
}
return cache;
}
async getUserDetailInfoV2(uid: string) {
let retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfoV2(uid, UserDetailSource.KDB), uid);
if (retUser && retUser.uin !== '0') {
return retUser;
}
this.context.logger.logDebug('[NapCat] [Mark] getUserDetailInfo Mode1 Failed.');
retUser = await this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER);
retUser = await this.fetchUserDetailInfoV2(uid, UserDetailSource.KSERVER);
if (retUser && retUser.uin === '0') {
retUser.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return retUser;
}
async getUserDetailInfoV2(uid: string): Promise<User> {
const fallback = new Fallback<User>((user) => FallbackUtil.boolchecker(user, user !== undefined && user.uin !== '0'))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KDB))
.add(() => this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER));
const retUser = await fallback.run().then(async (user) => {
if (user && user.uin === '0') {
user.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
}
return user;
});
return retUser;
}
async modifySelfProfile(param: ModifyProfileParams) {
return this.context.session.getProfileService().modifyDesktopMiniProfile(param);
}
@@ -216,7 +201,7 @@ export class NTQQUserApi {
.add(() => this.context.session.getUixConvertService().getUin([uid]).then((data) => data.uinInfo.get(uid)))
.add(() => this.context.session.getProfileService().getUinByUid('FriendsServiceImpl', [uid]).get(uid))
.add(() => this.context.session.getGroupService().getUinByUids([uid]).then((data) => data.uins.get(uid)))
.add(() => this.getUserDetailInfo(uid).then((data) => data.uin));
.add(() => this.fetchUserDetailInfoV2(uid).then((data) => data.uin));
const uin = await fallback.run().catch(() => '0');
return uin ?? '0';

View File

@@ -202,5 +202,29 @@
"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"
},
"3.2.16-33139": {
"appid": 537273909,
"qua": "V1_LNX_NQ_3.2.16_33139_GW_B"
},
"9.9.18-33139": {
"appid": 537273874,
"qua": "V1_WIN_NQ_9.9.18_33139_GW_B"
}
}

View File

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

View File

@@ -266,5 +266,41 @@
"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"
},
"9.9.18-33139-x64": {
"send": "39F5870",
"recv": "39FA070"
},
"3.2.16-33139-x64": {
"send": "A634F60",
"recv": "A638980"
},
"3.2.16-33139-arm64": {
"send": "7262BB0",
"recv": "72664E0"
}
}

View File

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

View File

@@ -1,5 +1,5 @@
export class NodeIKernelLoginListener {
onLoginConnected(...args: any[]): any {
onLoginConnected(): Promise<void> | void {
}
onLoginDisConnected(...args: any[]): any {

View File

@@ -1,71 +1,71 @@
import { User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]): any {
onProfileSimpleChanged(..._args: unknown[]): any {
}
onProfileDetailInfoChanged(profile: User): any {
onProfileDetailInfoChanged(_profile: User): any {
}
onStatusUpdate(...args: unknown[]): any {
onStatusUpdate(..._args: unknown[]): any {
}
onSelfStatusChanged(...args: unknown[]): any {
onSelfStatusChanged(..._args: unknown[]): any {
}
onStrangerRemarkChanged(...args: unknown[]): any {
onStrangerRemarkChanged(..._args: unknown[]): any {
}
onMemberListChange(...args: unknown[]): any {
onMemberListChange(..._args: unknown[]): any {
}
onMemberInfoChange(...args: unknown[]): any {
onMemberInfoChange(..._args: unknown[]): any {
}
onGroupListUpdate(...args: unknown[]): any {
onGroupListUpdate(..._args: unknown[]): any {
}
onGroupAllInfoChange(...args: unknown[]): any {
onGroupAllInfoChange(..._args: unknown[]): any {
}
onGroupDetailInfoChange(...args: unknown[]): any {
onGroupDetailInfoChange(..._args: unknown[]): any {
}
onGroupConfMemberChange(...args: unknown[]): any {
onGroupConfMemberChange(..._args: unknown[]): any {
}
onGroupExtListUpdate(...args: unknown[]): any {
onGroupExtListUpdate(..._args: unknown[]): any {
}
onGroupNotifiesUpdated(...args: unknown[]): any {
onGroupNotifiesUpdated(..._args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
}
onGroupMemberLevelInfoChange(...args: unknown[]): any {
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
}
onGroupBulletinChange(...args: unknown[]): any {
onGroupBulletinChange(..._args: unknown[]): any {
}
}

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core';
import { ChatType, RawMessage } from '@/core';
export interface SearchGroupInfo {
groupCode: string;
ownerUid: string;
@@ -56,7 +56,7 @@ export interface GroupSearchResult {
nextPos: number;
}
export interface NodeIKernelSearchListener {
onSearchGroupResult(params: GroupSearchResult): any;
onSearchFileKeywordsResult(params: {
@@ -94,4 +94,27 @@ export interface NodeIKernelSearchListener {
}[]
}[]
}): 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 @@ import { PacketLogger } from '@/core/packet/context/loggerContext';
// 0 send 1 recv
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;
}
@@ -38,11 +38,12 @@ export class NativePacketClient extends IPacketClient {
return true;
}
async init(pid: number, recv: string, send: string): Promise<void> {
async init(_pid: number, recv: string, send: string): Promise<void> {
const platform = process.platform + '.' + process.arch;
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/MoeHoo.' + platform + '.node');
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');
if (type === 0 && this.cb.get(trace_id + 'recv')) {
//此时为send 提取seq
@@ -55,7 +56,7 @@ export class NativePacketClient extends IPacketClient {
// console.log('callback:', callback, trace_id);
callback?.({ seq, cmd, hex_data });
}
});
}, this.napcore.config.o3HookMode == 1);
this.available = true;
}

View File

@@ -1,113 +0,0 @@
import { Data, WebSocket, ErrorEvent } from 'ws';
import { IPacketClient, RecvPacket } from '@/core/packet/client/baseClient';
import { LogStack } from '@/core/packet/context/clientContext';
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
import { PacketLogger } from '@/core/packet/context/loggerContext';
export class WsPacketClient extends IPacketClient {
private websocket: WebSocket | null = null;
private reconnectAttempts: number = 0;
private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置
private readonly clientUrl: string;
private readonly clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
private isInitialized: boolean = false;
private initPayload: { pid: number, recv: string, send: string } | null = null;
constructor(napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) {
super(napCore, logger, logStack);
this.clientUrl = this.napcore.config.packetServer
? this.clientUrlWrap(this.napcore.config.packetServer)
: this.clientUrlWrap('127.0.0.1:8083');
}
check(): boolean {
if (!this.napcore.config.packetServer) {
this.logStack.pushLogWarn('wsPacketClient 未配置服务器地址');
return false;
}
return true;
}
async init(pid: number, recv: string, send: string): Promise<void> {
this.initPayload = { pid, recv, send };
await this.connectWithRetry();
}
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify({
action: 'send',
cmd,
data,
trace_id
}));
} else {
this.logStack.pushLogWarn(`WebSocket 未连接,无法发送命令: ${cmd}`);
}
}
private async connectWithRetry(): Promise<void> {
while (this.reconnectAttempts < this.maxReconnectAttempts) {
try {
await this.connect();
return;
} catch {
this.reconnectAttempts++;
this.logStack.pushLogWarn(`${this.reconnectAttempts}/${this.maxReconnectAttempts} 次尝试重连失败!`);
await this.delay(5000);
}
}
this.logStack.pushLogError(`wsPacketClient 在 ${this.clientUrl} 达到最大重连次数 (${this.maxReconnectAttempts})`);
throw new Error(`无法连接到 WebSocket 服务器:${this.clientUrl}`);
}
private connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.websocket = new WebSocket(this.clientUrl);
this.websocket.onopen = () => {
this.available = true;
this.reconnectAttempts = 0;
this.logger.info(`wsPacketClient 已连接到 ${this.clientUrl}`);
if (!this.isInitialized && this.initPayload) {
this.websocket!.send(JSON.stringify({
action: 'init',
...this.initPayload
}));
this.isInitialized = true;
}
resolve();
};
this.websocket.onclose = () => {
this.available = false;
this.logger.warn('WebSocket 连接关闭,尝试重连...');
reject(new Error('WebSocket 连接关闭'));
};
this.websocket.onmessage = (event) => this.handleMessage(event.data).catch(err => {
this.logger.error(`处理消息时出错: ${err}`);
});
this.websocket.onerror = (event: ErrorEvent) => {
this.available = false;
this.logger.error(`WebSocket 出错: ${event.message}`);
this.websocket?.close();
reject(new Error(`WebSocket 出错: ${event.message}`));
};
});
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
private async handleMessage(message: Data): Promise<void> {
try {
const json: RecvPacket = JSON.parse(message.toString());
const trace_id_md5 = json.trace_id_md5;
const action = json?.type ?? 'init';
const event = this.cb.get(`${trace_id_md5}${action}`);
if (event) await event(json.data);
} catch (error) {
this.logger.error(`解析ws消息时出错: ${(error as Error).message}`);
}
}
}

View File

@@ -1,6 +1,5 @@
import { IPacketClient } from '@/core/packet/client/baseClient';
import { NativePacketClient } from '@/core/packet/client/nativeClient';
import { WsPacketClient } from '@/core/packet/client/wsClient';
import { OidbPacket } from '@/core/packet/transformer/base';
import { PacketLogger } from '@/core/packet/context/loggerContext';
import { NapCoreContext } from '@/core/packet/context/napCoreContext';
@@ -10,8 +9,7 @@ type clientPriorityType = {
}
const clientPriority: clientPriorityType = {
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack),
1: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new WsPacketClient(napCore, logger, logStack),
10: (napCore: NapCoreContext, logger: PacketLogger, logStack: LogStack) => new NativePacketClient(napCore, logger, logStack)
};
export class LogStack {
@@ -88,10 +86,6 @@ export class PacketClientContext {
this.logger.info('使用指定的 NativePacketClient 作为后端');
client = new NativePacketClient(this.napCore, this.logger, this.logStack);
break;
case 'frida':
this.logger.info('[Core] [Packet] 使用指定的 FridaPacketClient 作为后端');
client = new WsPacketClient(this.napCore, this.logger, this.logStack);
break;
case 'auto':
case undefined:
client = this.judgeClient();

View File

@@ -9,15 +9,14 @@ class SetSpecialTitle extends PacketTransformer<typeof proto.OidbSvcTrpcTcpBase>
}
build(groupCode: number, uid: string, tittle: string): OidbPacket {
const oidb_0x8FC_2_body = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2_Body).encode({
targetUid: uid,
specialTitle: tittle,
expiredTime: -1,
uinName: tittle
});
const oidb_0x8FC_2 = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2).encode({
groupUin: +groupCode,
body: oidb_0x8FC_2_body
body: {
targetUid: uid,
specialTitle: tittle,
expiredTime: -1,
uinName: tittle
}
});
return OidbBase.build(0x8FC, 2, oidb_0x8FC_2, false, false);
}

View File

@@ -4,12 +4,12 @@ import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
//设置群头衔 OidbSvcTrpcTcp.0x8fc_2
export const OidbSvcTrpcTcp0X8FC_2_Body = {
targetUid: ProtoField(1, ScalarType.STRING),
specialTitle: ProtoField(5, ScalarType.STRING),
expiredTime: ProtoField(6, ScalarType.SINT32),
uinName: ProtoField(7, ScalarType.STRING),
specialTitle: ProtoField(5, ScalarType.STRING, true),
expiredTime: ProtoField(6, ScalarType.INT32),
uinName: ProtoField(7, ScalarType.STRING, true),
targetName: ProtoField(8, ScalarType.STRING),
};
export const OidbSvcTrpcTcp0X8FC_2 = {
groupUin: ProtoField(1, ScalarType.UINT32),
body: ProtoField(3, ScalarType.BYTES),
body: ProtoField(3, () => OidbSvcTrpcTcp0X8FC_2_Body),
};

View File

@@ -16,7 +16,7 @@ export interface NodeIKernelBuddyService {
getBuddyListFromCache(reqType: BuddyListReqType): Promise<Array<
{
categoryId: number,//9999应该跳过 那是兜底数据吧
categoryId: number,//9999为特别关心
categorySortId: number,//排序方式
categroyName: string,//分类名
categroyMbCount: number,//不懂

View File

@@ -165,7 +165,7 @@ export interface NodeIKernelGroupService {
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;

View File

@@ -60,7 +60,10 @@ export interface QuickLoginResult {
}
export interface NodeIKernelLoginService {
getMsfStatus: () => number;
setLoginMiscData(arg0: string, value: string): unknown;
getMachineGuid(): string;
get(): NodeIKernelLoginService;

View File

@@ -1,4 +1,4 @@
import { ChatType } from '@/core/types';
import { ChatType, Peer } from '@/core/types';
import { GeneralCallResult } from './common';
export interface NodeIKernelSearchService {
@@ -54,7 +54,7 @@ export interface NodeIKernelSearchService {
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

View File

@@ -1,5 +1,3 @@
import { QQLevel, NTSex } from './user';
export interface KickMemberInfo {
optFlag: number;
optOperate: number;
@@ -274,24 +272,42 @@ export enum NTGroupMemberRole {
KOWNER = 4
}
export interface GroupMember {
memberRealLevel: number | undefined;
memberSpecialTitle?: string;
avatarPath: string;
cardName: string;
cardType: number;
isDelete: boolean;
nick: string;
qid: string;
remark: string;
role: NTGroupMemberRole;
shutUpTime: number; // 禁言时间(S)
uid: string;
qid: string;
uin: string;
nick: string;
remark: string;
cardType: number;
cardName: string;
role: NTGroupMemberRole;
avatarPath: string;
shutUpTime: number;
isDelete: boolean;
isSpecialConcerned: boolean;
isSpecialShield: boolean;
isRobot: boolean;
sex?: NTSex;
age?: number;
qqLevel?: QQLevel;
isChangeRole: boolean;
joinTime: string;
lastSpeakTime: string;
groupHonor: Uint8Array;
memberRealLevel: number;
memberLevel: number;
globalGroupLevel: number;
globalGroupPoint: number;
memberTitleId: number;
memberSpecialTitle: string;
specialTitleExpireTime: string;
userShowFlag: number;
userShowFlagNew: number;
richFlag: number;
mssVipType: number;
bigClubLevel: number;
bigClubFlag: number;
autoRemark: string;
creditLevel: number;
joinTime: number;
lastSpeakTime: number;
memberFlag: number;
memberFlagExt: number;
memberMobileFlag: number;
memberFlagExt2: number;
isSpecialShielded: boolean;
cardNameId: number;
}

View File

@@ -207,8 +207,8 @@ interface PhotoWall {
// 简单信息
export interface SimpleInfo {
uid?: string;
uin?: string;
uid: string;
uin: string;
coreInfo: CoreInfo;
baseInfo: BaseInfo;
status: UserStatus | null;
@@ -233,13 +233,15 @@ export interface SelfStatusInfo {
setTime: string;
}
export type UserV2 = UserDetailInfoListenerArg;
// 用户详细信息监听参数
export interface UserDetailInfoListenerArg {
uid: string;
uin: string;
simpleInfo: SimpleInfo;
commonExt: CommonExt;
photoWall: PhotoWall;
simpleInfo?: SimpleInfo;
commonExt?: CommonExt;
photoWall?: PhotoWall;
}
// 修改个人资料参数
@@ -332,13 +334,12 @@ export interface User {
}
// 自身信息
export interface SelfInfo extends User {
online?: boolean;
export interface SelfInfo extends Partial<UserV2> {
uid: string;
uin: string;
online: boolean;
}
// 好友类型
export type Friend = User;
// 业务键枚举
export enum BizKey {
KPRIVILEGEICON = 0,

View File

@@ -7,13 +7,13 @@ import { SelfInfo } from '@/core/types';
import { NodeIKernelLoginListener } from '@/core/listeners';
import { NodeIKernelLoginService } from '@/core/services';
import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper';
import { InitWebUi, WebUiConfig } from '@/webui';
import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui';
import { NapCatOneBot11Adapter } from '@/onebot';
//Framework ES入口文件
export async function getWebUiUrl() {
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(
@@ -46,7 +46,6 @@ export async function NCoreInitFramework(
resolveSelfInfo({
uid: loginResult.uid,
uin: loginResult.uin,
nick: '', // 获取不到
online: true,
});
};

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

@@ -19,7 +19,7 @@ export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11U
const extendData = await this.core.apis.UserApi.getUserDetailInfoByUin(user_id);
let uid = (await this.core.apis.UserApi.getUidByUinV2(user_id));
if (!uid) uid = extendData.detail.uid;
const info = (await this.core.apis.UserApi.getUserDetailInfo(uid));
const info = (await this.core.apis.UserApi.fetchUserDetailInfoV2(uid));
return {
...extendData.detail.simpleInfo.coreInfo,
...extendData.detail.commonExt ?? {},
@@ -29,17 +29,17 @@ export default class GoCQHTTPGetStrangerInfo extends OneBotAction<Payload, OB11U
user_id: parseInt(extendData.detail.uin) ?? 0,
uid: info.uid ?? uid,
nickname: extendData.detail.simpleInfo.coreInfo.nick ?? '',
age: extendData.detail.simpleInfo.baseInfo.age ?? info.age,
age: extendData.detail.simpleInfo.baseInfo.age ?? info.simpleInfo?.baseInfo.age,
qid: extendData.detail.simpleInfo.baseInfo.qid,
qqLevel: calcQQLevel(extendData.detail.commonExt?.qqLevel ?? info.qqLevel),
qqLevel: calcQQLevel(extendData.detail.commonExt?.qqLevel ?? info.commonExt?.qqLevel),
sex: OB11Construct.sex(extendData.detail.simpleInfo.baseInfo.sex) ?? OB11UserSex.unknown,
long_nick: extendData.detail.simpleInfo.baseInfo.longNick ?? info.longNick,
reg_time: extendData.detail.commonExt?.regTime ?? info.regTime,
long_nick: extendData.detail.simpleInfo.baseInfo.longNick ?? info.simpleInfo?.baseInfo.longNick,
reg_time: extendData.detail.commonExt?.regTime ?? info.commonExt?.regTime,
is_vip: extendData.detail.simpleInfo.vasInfo?.svipFlag,
is_years_vip: extendData.detail.simpleInfo.vasInfo?.yearVipFlag,
vip_level: extendData.detail.simpleInfo.vasInfo?.vipLevel,
remark: extendData.detail.simpleInfo.coreInfo.remark ?? info.remark,
status: extendData.detail.simpleInfo.status?.status ?? info.status,
remark: extendData.detail.simpleInfo.coreInfo.remark ?? info.simpleInfo?.coreInfo.remark,
status: extendData.detail.simpleInfo.status?.status ?? info.simpleInfo?.status?.status,
login_days: 0,//失效
};
}

View File

@@ -16,15 +16,15 @@ export class SetQQProfile extends OneBotAction<Payload, Awaited<ReturnType<NTQQU
async _handle(payload: Payload) {
const self = this.core.selfInfo;
const OldProfile = await this.core.apis.UserApi.getUserDetailInfo(self.uid);
const OldProfile = await this.core.apis.UserApi.fetchUserDetailInfoV2(self.uid);
return await this.core.apis.UserApi.modifySelfProfile({
nick: payload.nickname,
longNick: (payload?.personal_note ?? OldProfile?.longNick) || '',
sex: parseInt(payload?.sex ? payload?.sex.toString() : OldProfile?.sex!.toString()),
longNick: (payload?.personal_note ?? OldProfile?.simpleInfo!.baseInfo.longNick) || '',
sex: parseInt(payload?.sex ? payload?.sex.toString() : OldProfile?.simpleInfo!.baseInfo.sex!.toString()),
birthday: {
birthday_year: OldProfile?.birthday_year!.toString(),
birthday_month: OldProfile?.birthday_month!.toString(),
birthday_day: OldProfile?.birthday_day!.toString(),
birthday_year: OldProfile?.simpleInfo!.baseInfo.birthday_year!.toString(),
birthday_month: OldProfile?.simpleInfo!.baseInfo.birthday_month!.toString(),
birthday_day: OldProfile?.simpleInfo!.baseInfo.birthday_day!.toString(),
},
location: undefined,
});

View File

@@ -20,6 +20,7 @@ class GetGroupInfo extends OneBotAction<Payload, OB11Group> {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,
member_count: data.memberNum,

View File

@@ -32,24 +32,20 @@ class GetGroupMemberInfo extends OneBotAction<Payload, OB11GroupMember> {
const [member, info] = await Promise.all([
this.core.apis.GroupApi.getGroupMemberEx(payload.group_id.toString(), uid, isNocache),
this.core.apis.UserApi.getUserDetailInfo(uid),
this.core.apis.UserApi.fetchUserDetailInfoV2(uid),
]);
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
return info ? { ...groupMember, ...member, ...info } : member;
return { member, info, groupMember };
}
async _handle(payload: Payload) {
const isNocache = this.parseBoolean(payload.no_cache ?? true);
const uid = await this.getUid(payload.user_id);
const member = await this.getGroupMemberInfo(payload, uid, isNocache);
if (!member) {
this.core.context.logger.logDebug('获取群成员详细信息失败, 只能返回基础信息');
}
return OB11Construct.groupMember(payload.group_id.toString(), member);
const { member, info } = await this.getGroupMemberInfo(payload, uid, isNocache);
console.log('member', JSON.stringify(member, null, 2), 'info', JSON.stringify(info, null, 2));
return OB11Construct.groupMember(payload.group_id.toString(), member, info);
}
}

View File

@@ -16,16 +16,35 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
override actionName = ActionName.GetGroupMemberList;
override payloadSchema = SchemaData;
/**
* 处理获取群成员列表请求
*/
async _handle(payload: Payload) {
const groupIdStr = payload.group_id.toString();
const noCache = this.parseBoolean(payload.no_cache ?? false);
// 获取群成员基本信息
const groupMembers = await this.getGroupMembers(groupIdStr, noCache);
const _groupMembers = await Promise.all(
Array.from(groupMembers.values()).map(item =>
OB11Construct.groupMember(groupIdStr, item)
)
const memberArray = Array.from(groupMembers.values());
// 批量并行获取用户详情
const userDetailsPromises = memberArray.map(member =>
this.core.apis.UserApi.fetchUserDetailInfoV2(member.uid)
.catch(_ => {
return { uin: member.uin, uid: member.uid };
})
);
return Array.from(new Map(_groupMembers.map(member => [member.user_id, member])).values());
const userDetails = await Promise.all(userDetailsPromises);
// 并行构建 OneBot 格式的群成员数据
const groupMembersList = memberArray.map((member, index) => {
// 确保用户详情不会是undefined
const userDetail = userDetails[index] || { uin: member.uin, uid: member.uid };
return OB11Construct.groupMember(groupIdStr, member, userDetail);
});
// 直接返回处理后的成员列表,不进行去重
return groupMembersList;
}
private parseBoolean(value: boolean | string): boolean {
@@ -37,13 +56,18 @@ export class GetGroupMemberList extends OneBotAction<Payload, OB11GroupMember[]>
let groupMembers = memberCache.get(groupIdStr);
if (noCache || !groupMembers) {
const data = this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr, true).then().catch();
groupMembers = memberCache.get(groupIdStr) || (await data);
if (!groupMembers) {
throw new Error(`Failed to get group member list for group ${groupIdStr}`);
try {
const refreshPromise = this.core.apis.GroupApi.refreshGroupMemberCache(groupIdStr, true);
groupMembers = memberCache.get(groupIdStr) || (await refreshPromise);
if (!groupMembers) {
throw new Error(`无法获取群 ${groupIdStr} 的成员列表`);
}
} catch (error) {
throw new Error(`获取群 ${groupIdStr} 成员列表失败: ${error instanceof Error ? error.message : String(error)}`);
}
}
return groupMembers;
}
}

View File

@@ -20,11 +20,12 @@ export default class SetGroupAddRequest extends OneBotAction<Payload, null> {
const approve = payload.approve?.toString() !== 'false';
const reason = payload.reason ?? ' ';
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) {
throw new Error('No such request');
}
await this.core.apis.GroupApi.handleGroupRequest(
doubt,
notify,
approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
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);
if (!notify) {
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

@@ -18,7 +18,6 @@ export default class SetGroupBan extends OneBotAction<Payload, null> {
if (!uid) throw new Error('uid error');
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');
if (member_role === 3) throw new Error('cannot ban admin');
// 例如无管理员权限时 result为 120101005 errMsg为 'ERR_NOT_GROUP_ADMIN'
let ret = await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
[{ uid: uid, timeStamp: +payload.duration }]);

View File

@@ -108,10 +108,12 @@ 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) {
const actionHandlers = [
new SetGroupRemark(obContext, core),
new GetGroupInfoEx(obContext, core),
new FetchEmojiLike(obContext, core),
new GetFile(obContext, core),
@@ -227,8 +229,8 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new GetGroupSystemMsg(obContext, core),
new BotExit(obContext, core),
new ClickInlineKeyboardButton(obContext, core),
new GetPrivateFileUrl(obContext,core),
new GetUnidirectionalFriendList(obContext,core),
new GetPrivateFileUrl(obContext, core),
new GetUnidirectionalFriendList(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];

View File

@@ -10,6 +10,7 @@ export interface InvalidCheckResult {
}
export const ActionName = {
SetGroupRemark: 'set_group_remark',
NapCat_GetPrivateFileUrl: 'get_private_file_url',
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
GetUnidirectionalFriendList: 'get_unidirectional_friend_list',

View File

@@ -14,8 +14,25 @@ export default class GetFriendList extends OneBotAction<Payload, OB11User[]> {
override actionName = ActionName.GetFriendList;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
//全新逻辑
return OB11Construct.friends(await this.core.apis.FriendApi.getBuddy(typeof payload.no_cache === 'string' ? payload.no_cache === 'true' : !!payload.no_cache));
async _handle(_payload: Payload) {
// 获取好友列表
let buddyList = await this.core.apis.FriendApi.getBuddyV2SimpleInfoMap();
const buddyArray = Array.from(buddyList.values());
// 批量并行获取用户详情
const userDetailsPromises = buddyArray.map(member =>
this.core.apis.UserApi.fetchUserDetailInfoV2(member.uid)
.catch(_ => {
return { uin: member.uin, uid: member.uid };
})
);
const userDetails = await Promise.all(userDetailsPromises);
const friendList = buddyArray.map((friend, index) => {
const userDetail = userDetails[index] || { uin: friend.uin, uid: friend.uid };
return OB11Construct.friend(friend, userDetail);
});
return friendList;
}
}
}

View File

@@ -444,8 +444,8 @@ export class OneBotMsgApi {
}
const uid = await this.core.apis.UserApi.getUidByUinV2(`${atQQ}`);
if (!uid) throw new Error('Get Uid Error');
const info = await this.core.apis.UserApi.getUserDetailInfo(uid);
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.nick || '');
const info = await this.core.apis.UserApi.fetchUserDetailInfoV2(uid);
return at(atQQ, uid, NTMsgAtType.ATTYPEONE, info.simpleInfo?.coreInfo.nick || '');
},
[OB11MessageDataType.reply]: async ({ data: { id } }) => {
@@ -809,6 +809,7 @@ export class OneBotMsgApi {
message_id: msg.id!,
message_seq: msg.id!,
real_id: msg.id!,
real_seq: msg.msgSeq,
message_type: msg.chatType == ChatType.KCHATTYPEGROUP ? 'group' : 'private',
sender: {
user_id: +(msg.senderUin ?? 0),
@@ -844,7 +845,7 @@ export class OneBotMsgApi {
return;
}
}
resMsg.sender.nickname = (await this.core.apis.UserApi.getUserDetailInfo(msg.senderUid)).nick;
resMsg.sender.nickname = (await this.core.apis.UserApi.fetchUserDetailInfoV2(msg.senderUid)).simpleInfo?.coreInfo.nick || '';
}
private async handleTempGroupMessage(resMsg: OB11Message, msg: RawMessage) {

View File

@@ -84,17 +84,19 @@ export class OneBotQuickActionApi {
let notify = (await this.core.apis.GroupApi.getSingleScreenNotifies(false, 100)).find(e => e.seq == flag);
if (!notify) {
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) {
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) {
this.core.apis.GroupApi.handleGroupRequest(
doubt,
notify,
quickAction.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE,
quickAction.reason,

View File

@@ -1,6 +1,6 @@
import { calcQQLevel } from '@/common/helper';
import { FileNapCatOneBotUUID } from '@/common/file-uuid';
import { FriendV2, Group, GroupFileInfoUpdateParamType, GroupMember, SelfInfo, NTSex } from '@/core';
import { FriendV2, Group, GroupFileInfoUpdateParamType, GroupMember, SelfInfo, NTSex, UserV2 } from '@/core';
import {
OB11Group,
OB11GroupFile,
@@ -14,22 +14,43 @@ export class OB11Construct {
static selfInfo(selfInfo: SelfInfo): OB11User {
return {
user_id: +selfInfo.uin,
nickname: selfInfo.nick,
nickname: selfInfo.simpleInfo?.coreInfo.nick ?? '',
};
}
static friends(friends: FriendV2[]): OB11User[] {
return friends.map(rawFriend => ({
...rawFriend.baseInfo,
...rawFriend.coreInfo,
birthday_year: rawFriend.baseInfo.birthday_year,
birthday_month: rawFriend.baseInfo.birthday_month,
birthday_day: rawFriend.baseInfo.birthday_day,
user_id: parseInt(rawFriend.coreInfo.uin),
age: rawFriend.baseInfo.age,
phone_num: rawFriend.baseInfo.phoneNum,
email: rawFriend.baseInfo.eMail,
category_id: rawFriend.baseInfo.categoryId,
nickname: rawFriend.coreInfo.nick ?? '',
remark: rawFriend.coreInfo.remark ?? rawFriend.coreInfo.nick,
sex: this.sex(rawFriend.baseInfo.sex),
level: 0,
}));
}
static friend(friends: FriendV2,info:UserV2): OB11User {
return {
birthday_year: friends.baseInfo.birthday_year,
birthday_month: friends.baseInfo.birthday_month,
birthday_day: friends.baseInfo.birthday_day,
user_id: parseInt(friends.coreInfo.uin),
age: friends.baseInfo.age,
phone_num: friends.baseInfo.phoneNum,
email: friends.baseInfo.eMail,
category_id: friends.baseInfo.categoryId,
nickname: friends.coreInfo.nick ?? '',
remark: friends.coreInfo.remark ?? friends.coreInfo.nick,
sex: this.sex(friends.baseInfo.sex),
level: calcQQLevel(info?.commonExt?.qqLevel) || 0,
qid: friends.baseInfo.qid,
};
}
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return {
4: OB11GroupMemberRole.owner,
@@ -48,20 +69,20 @@ export class OB11Construct {
}[sex] || OB11UserSex.unknown;
}
static groupMember(group_id: string, member: GroupMember): OB11GroupMember {
static groupMember(group_id: string, member: GroupMember, info: UserV2): OB11GroupMember {
return {
group_id: +group_id,
user_id: +member.uin,
nickname: member.nick,
card: member.cardName,
sex: this.sex(member.sex),
age: member.age ?? 0,
area: '',
sex: this.sex(info?.simpleInfo?.baseInfo.sex),
age: info?.simpleInfo?.baseInfo.age ?? 0,
area: info?.commonExt?.address,
level: member.memberRealLevel?.toString() ?? '0',
qq_level: member.qqLevel && calcQQLevel(member.qqLevel) || 0,
qq_level: info?.commonExt?.qqLevel && calcQQLevel(info.commonExt.qqLevel) || 0,
join_time: +member.joinTime,
last_sent_time: +member.lastSpeakTime,
title_expire_time: 0,
title_expire_time: +member.specialTitleExpireTime,
unfriendly: false,
card_changeable: true,
is_robot: member.isRobot,
@@ -73,6 +94,7 @@ export class OB11Construct {
static group(group: Group): OB11Group {
return {
group_remark: group.remarkName,
group_id: +group.groupCode,
group_name: group.groupName,
member_count: group.memberCount,

View File

@@ -97,13 +97,13 @@ export class NapCatOneBot11Adapter {
return log;
}
async InitOneBot() {
const selfInfo = this.core.selfInfo;
const ob11Config = this.configLoader.configData;
this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid)
this.core.apis.UserApi.fetchUserDetailInfoV2(this.core.selfInfo.uid)
.then((user) => {
selfInfo.nick = user.nick;
this.context.logger.setLogSelfInfo(selfInfo);
this.core.selfInfo = { ...user, online: this.core.selfInfo.online };
this.context.logger.setLogSelfInfo({ nick: this.core.selfInfo.simpleInfo?.coreInfo.nick ?? '', uid: this.core.selfInfo.uid });
})
.catch(e => this.context.logger.logError(e));
@@ -170,7 +170,7 @@ export class NapCatOneBot11Adapter {
this.initGroupListener();
WebUiDataRuntime.setQQVersion(this.core.context.basicInfoWrapper.getFullQQVesion());
WebUiDataRuntime.setQQLoginInfo(selfInfo);
WebUiDataRuntime.setQQLoginInfo(this.core.selfInfo);
WebUiDataRuntime.setQQLoginStatus(true);
WebUiDataRuntime.setOnOB11ConfigChanged(async (newConfig) => {
const prev = this.configLoader.configData;

View File

@@ -1,4 +1,10 @@
export interface OB11User {
birthday_year?: number;
birthday_month?: number;
birthday_day?: number;
phone_num?: string; // 手机号
email?: string; // 邮箱
category_id?: number; // 分组ID
user_id: number; // 用户ID
nickname: string; // 昵称
remark?: string; // 备注
@@ -57,6 +63,7 @@ export interface OB11GroupMember {
}
export interface OB11Group {
group_remark: string; // 群备注
group_id: number; // 群ID
group_name: string; // 群名称
member_count?: number; // 成员数量

View File

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

View File

@@ -30,6 +30,8 @@ import { InitWebUi } from '@/webui';
import { WebUiDataRuntime } from '@/webui/src/helper/Data';
import { napCatVersion } from '@/common/version';
import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener';
import { sleep } from '@/common/helper';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
@@ -113,120 +115,119 @@ async function handleLogin(
quickLoginUin: string | undefined,
historyLoginList: LoginListItem[]
): Promise<SelfInfo> {
return new Promise<SelfInfo>((resolve) => {
const loginListener = new NodeIKernelLoginListener();
let isLogined = false;
let context = { isLogined: false };
let inner_resolve: (value: SelfInfo) => void;
let selfInfo: Promise<SelfInfo> = new Promise((resolve) => {
inner_resolve = resolve;
});
// 连接服务
loginListener.onUserLoggedIn = (userid: string) => {
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
};
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
isLogined = true;
resolve({
uid: loginResult.uid,
uin: loginResult.uin,
nick: '',
online: true,
});
};
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(realBase64, 'base64');
logger.logWarn('请扫描下面的二维码然后在手Q上授权登录');
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
logger.logWarn([
'\n',
res,
'二维码解码URL: ' + qrcodeUrl,
'如果控制台二维码无法扫码可以复制解码url到二维码生成网站生成二维码再扫码也可以打开下方的二维码路径图片进行扫码。',
].join('\n'));
fs.writeFile(qrcodePath, buffer, {}, () => {
logger.logWarn('二维码已保存到', qrcodePath);
});
});
};
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number) => {
if (!isLogined) {
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
loginService.getQRCodePicture();
}
};
loginListener.onLoginFailed = (...args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
const isConnect = loginService.connect();
if (!isConnect) {
logger.logError('核心登录服务连接失败!');
return;
}
logger.log('核心登录服务连接成功!');
loginService.getLoginList().then((res) => {
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
WebUiDataRuntime.setQQNewLoginList(list);
const loginListener = new NodeIKernelLoginListener();
loginListener.onUserLoggedIn = (userid: string) => {
logger.logError(`当前账号(${userid})已登录,无法重复登录`);
};
loginListener.onQRCodeLoginSucceed = async (loginResult) => {
context.isLogined = true;
inner_resolve({
uid: loginResult.uid,
uin: loginResult.uin,
online: true,
});
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
return await new Promise((resolve) => {
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
resolve({ result: false, message: res.loginErrorInfo.errMsg });
}
resolve({ result: true, message: '' });
}).catch((e) => {
logger.logError(e);
resolve({ result: false, message: '快速登录发生错误' });
});
} else {
resolve({ result: false, message: '快速登录失败' });
}
};
loginListener.onLoginConnected = () => {
waitForNetworkConnection(loginService, logger).then(() => {
handleLoginInner(context, logger, loginService, quickLoginUin, historyLoginList).then().catch(e => logger.logError(e));
});
}
loginListener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
WebUiDataRuntime.setQQLoginQrcodeURL(qrcodeUrl);
const realBase64 = pngBase64QrcodeData.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(realBase64, 'base64');
logger.logWarn('请扫描下面的二维码然后在手Q上授权登录');
const qrcodePath = path.join(pathWrapper.cachePath, 'qrcode.png');
qrcode.generate(qrcodeUrl, { small: true }, (res) => {
logger.logWarn([
'\n',
res,
'二维码解码URL: ' + qrcodeUrl,
'如果控制台二维码无法扫码可以复制解码url到二维码生成网站生成二维码再扫码也可以打开下方的二维码路径图片进行扫码。',
].join('\n'));
fs.writeFile(qrcodePath, buffer, {}, () => {
logger.logWarn('二维码已保存到', qrcodePath);
});
});
};
if (quickLoginUin) {
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
logger.log('正在快速登录 ', quickLoginUin);
setTimeout(() => {
loginService.quickLoginWithUin(quickLoginUin)
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
if (!isLogined) loginService.getQRCodePicture();
}
})
.catch();
}, 1000);
} else {
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
if (!isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (historyLoginList.length > 0) {
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
loginListener.onQRCodeSessionFailed = (errType: number, errCode: number) => {
if (!context.isLogined) {
logger.logError('[Core] [Login] Login Error,ErrType: ', errType, ' ErrCode:', errCode);
if (errType == 1 && errCode == 3) {
// 二维码过期刷新
}
loginService.getQRCodePicture();
}
};
loginListener.onLoginFailed = (...args) => {
logger.logError('[Core] [Login] Login Error , ErrInfo: ', JSON.stringify(args));
};
loginService.addKernelLoginListener(proxiedListenerOf(loginListener, logger));
loginService.connect();
return await selfInfo;
}
async function handleLoginInner(context: { isLogined: boolean }, logger: LogWrapper, loginService: NodeIKernelLoginService, quickLoginUin: string | undefined, historyLoginList: LoginListItem[]) {
WebUiDataRuntime.setQuickLoginCall(async (uin: string) => {
return await new Promise((resolve) => {
if (uin) {
logger.log('正在快速登录 ', uin);
loginService.quickLoginWithUin(uin).then(res => {
if (res.loginErrorInfo.errMsg) {
resolve({ result: false, message: res.loginErrorInfo.errMsg });
}
resolve({ result: true, message: '' });
}).catch((e) => {
logger.logError(e);
resolve({ result: false, message: '快速登录发生错误' });
});
} else {
resolve({ result: false, message: '快速登录失败' });
}
});
});
if (quickLoginUin) {
if (historyLoginList.some(u => u.uin === quickLoginUin)) {
logger.log('正在快速登录 ', quickLoginUin);
loginService.quickLoginWithUin(quickLoginUin)
.then(result => {
if (result.loginErrorInfo.errMsg) {
logger.logError('快速登录错误:', result.loginErrorInfo.errMsg);
if (!context.isLogined) loginService.getQRCodePicture();
}
})
.catch();
} else {
logger.logError('快速登录失败,未找到该 QQ 历史登录记录,将使用二维码登录方式');
if (!context.isLogined) loginService.getQRCodePicture();
}
} else {
logger.log('没有 -q 指令指定快速登录,将使用二维码登录方式');
if (historyLoginList.length > 0) {
logger.log(`可用于快速登录的 QQ\n${historyLoginList
.map((u, index) => `${index + 1}. ${u.uin} ${u.nickName}`)
.join('\n')
}`);
}
loginService.getQRCodePicture();
}
loginService.getLoginList().then((res) => {
// 遍历 res.LocalLoginInfoList[x].isQuickLogin是否可以 res.LocalLoginInfoList[x].uin 转为string 加入string[] 最后遍历完成调用WebUiDataRuntime.setQQQuickLoginList
const list = res.LocalLoginInfoList.filter((item) => item.isQuickLogin);
WebUiDataRuntime.setQQQuickLoginList(list.map((item) => item.uin.toString()));
WebUiDataRuntime.setQQNewLoginList(list);
});
}
@@ -284,6 +285,20 @@ async function handleProxy(session: NodeIQQNTWrapperSession, logger: LogWrapper)
});
}
}
async function waitForNetworkConnection(loginService: NodeIKernelLoginService, logger: LogWrapper) {
let network_ok = false;
let tryCount = 0;
while (!network_ok) {
network_ok = loginService.getMsfStatus() !== 3;// win 11 0连接 1未连接
logger.log('等待网络连接...');
await sleep(500);
tryCount++;
}
logger.log('网络已连接');
return network_ok;
}
export async function NCoreInitShell() {
console.log('NapCat Shell App Loading...');
const pathWrapper = new NapCatPathWrapper();
@@ -330,8 +345,8 @@ export async function NCoreInitShell() {
guid,
basicInfoWrapper.QQVersionAppid,
basicInfoWrapper.getFullQQVesion(),
selfInfo.uin,
selfInfo.uid,
selfInfo.uin ?? '',
selfInfo.uid ?? '',
dataPath,
);
@@ -364,7 +379,6 @@ export async function NCoreInitShell() {
export class NapCatShell {
readonly core: NapCatCore;
readonly context: InstanceContext;
constructor(
wrapper: WrapperNodeApi,
session: NodeIQQNTWrapperSession,

View File

@@ -29,7 +29,7 @@ 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);
@@ -45,6 +45,7 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
webUiPathWrapper = pathWrapper;
WebUiConfig = new WebUiConfigWrapper();
const [host, port, token] = await InitPort(await WebUiConfig.GetWebUIConfig());
webUiRuntimePort = port;
if (port == 0) {
logger.log('[NapCat] [WebUi] Current WebUi is not run.');
return;