diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..f30e5309
--- /dev/null
+++ b/.vscode/launch.json
@@ -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"
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ca143742..48eac328 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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"
+ ],
}
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..cca4546e
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -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.
diff --git a/README.md b/README.md
index 3b8fc828..c842eb60 100644
--- a/README.md
+++ b/README.md
@@ -1,67 +1,64 @@
-
+
+# NapCat
+

-
+
+_Modern protocol-side framework implemented based on NTQQ._
+
+> 云起兮风生,心向远方兮路未曾至.
+
---
-## 欢迎回家
-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 | [](https://napneko.github.io/) | [](https://doc.napneko.icu/) | [](https://napcat.napneko.icu/) |
+|:-:|:-:|:-:|:-:|
-[Cloudflare.HKServer](https://napcat.napneko.icu/)
+| Docs | [](https://napneko.pages.dev/) | [](https://napcat.cyou/) | [](https://www.napcat.wiki) |
+|:-:|:-:|:-:|:-:|
-[Github.IO](https://napneko.github.io/)
+| Contact | [](https://qm.qq.com/q/I6LU87a0Yq) | [](https://qm.qq.com/q/HaRcfrHpUk) | [](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)
-
-## 回家旅途
-[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).
-## 开源附加
+**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
-任何使用本仓库代码的地方,都应当严格遵守[本仓库开源许可](./LICENSE)。**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
+## Warnings
+
+[某框架抄袭部分分析](https://napneko.github.io/other/about-copy)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..faee4747
--- /dev/null
+++ b/SECURITY.md
@@ -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
diff --git a/external/LiteLoaderWrapper.zip b/external/LiteLoaderWrapper.zip
index d586a609..9861a5a1 100644
Binary files a/external/LiteLoaderWrapper.zip and b/external/LiteLoaderWrapper.zip differ
diff --git a/launcher/NapCatWinBootHook.dll b/launcher/NapCatWinBootHook.dll
index d7393fac..b1692a73 100644
Binary files a/launcher/NapCatWinBootHook.dll and b/launcher/NapCatWinBootHook.dll differ
diff --git a/launcher/NapCatWinBootMain.exe b/launcher/NapCatWinBootMain.exe
index 9501691c..66f69e09 100644
Binary files a/launcher/NapCatWinBootMain.exe and b/launcher/NapCatWinBootMain.exe differ
diff --git a/launcher/launcher-user.bat b/launcher/launcher-user.bat
index 86410c9b..224ea4eb 100644
--- a/launcher/launcher-user.bat
+++ b/launcher/launcher-user.bat
@@ -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
)
diff --git a/launcher/launcher-win10-user.bat b/launcher/launcher-win10-user.bat
index debf0acb..84050e0b 100644
--- a/launcher/launcher-win10-user.bat
+++ b/launcher/launcher-win10-user.bat
@@ -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
)
diff --git a/launcher/launcher-win10.bat b/launcher/launcher-win10.bat
index 0ab1677d..8e458178 100644
--- a/launcher/launcher-win10.bat
+++ b/launcher/launcher-win10.bat
@@ -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
)
diff --git a/launcher/launcher.bat b/launcher/launcher.bat
index a263f2eb..970a7edd 100644
--- a/launcher/launcher.bat
+++ b/launcher/launcher.bat
@@ -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
)
diff --git a/launcher/qqnt.json b/launcher/qqnt.json
index 837b4979..74d47b93 100644
--- a/launcher/qqnt.json
+++ b/launcher/qqnt.json
@@ -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-32869",
+ "verHash": "e735296c",
+ "linuxVersion": "3.2.16-32869",
+ "linuxVerHash": "4c192ba9",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -17,8 +16,25 @@
"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": "32869",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",
diff --git a/manifest.json b/manifest.json
index cbd5033c..05296edc 100644
--- a/manifest.json
+++ b/manifest.json
@@ -4,7 +4,7 @@
"name": "NapCatQQ",
"slug": "NapCat.Framework",
"description": "高性能的 OneBot 11 协议实现",
- "version": "4.5.22",
+ "version": "4.7.43",
"icon": "./logo.png",
"authors": [
{
diff --git a/napcat.webui/package.json b/napcat.webui/package.json
index bd3c4239..abe025ed 100644
--- a/napcat.webui/package.json
+++ b/napcat.webui/package.json
@@ -55,6 +55,7 @@
"ahooks": "^3.8.4",
"axios": "^1.7.9",
"clsx": "^2.1.1",
+ "crypto-js": "^4.2.0",
"echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6",
@@ -88,6 +89,7 @@
"@eslint/js": "^9.19.0",
"@react-types/shared": "^3.26.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
+ "@types/crypto-js": "^4.2.2",
"@types/event-source-polyfill": "^1.0.5",
"@types/fabric": "^5.3.9",
"@types/node": "^22.12.0",
diff --git a/napcat.webui/src/components/onebot/api/debug.tsx b/napcat.webui/src/components/onebot/api/debug.tsx
index c699edf6..f857bb9b 100644
--- a/napcat.webui/src/components/onebot/api/debug.tsx
+++ b/napcat.webui/src/components/onebot/api/debug.tsx
@@ -136,7 +136,7 @@ const OneBotApiDebug: React.FC = (props) => {
请求体
diff --git a/napcat.webui/src/controllers/webui_manager.ts b/napcat.webui/src/controllers/webui_manager.ts
index ac472126..dfd3e741 100644
--- a/napcat.webui/src/controllers/webui_manager.ts
+++ b/napcat.webui/src/controllers/webui_manager.ts
@@ -3,7 +3,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill'
import { LogLevel } from '@/const/enum'
import { serverRequest } from '@/utils/request'
-
+import CryptoJS from "crypto-js";
export interface Log {
level: LogLevel
message: string
@@ -17,9 +17,10 @@ export default class WebUIManager {
}
public static async loginWithToken(token: string) {
+ const sha256 = CryptoJS.SHA256(token + '.napcat').toString();
const { data } = await serverRequest.post>(
'/auth/login',
- { token }
+ { hash: sha256 }
)
return data.data.Credential
}
diff --git a/napcat.webui/src/pages/web_login.tsx b/napcat.webui/src/pages/web_login.tsx
index e2b4ca44..c171bc75 100644
--- a/napcat.webui/src/pages/web_login.tsx
+++ b/napcat.webui/src/pages/web_login.tsx
@@ -47,6 +47,22 @@ export default function WebLoginPage() {
}
}
+ // 处理全局键盘事件
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Enter' && !isLoading) {
+ onSubmit()
+ }
+ }
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyDown)
+
+ // 清理函数
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [tokenValue, isLoading]) // 依赖项包含用于登录的状态
+
useEffect(() => {
if (token) {
onSubmit()
@@ -79,6 +95,7 @@ export default function WebLoginPage() {
{
+export function recvTask(cb: (taskData: T) => Promise) {
+ 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(async ({ input, sampleRate }) => {
return await encode(input, sampleRate);
-};
+});
\ No newline at end of file
diff --git a/src/common/audio.ts b/src/common/audio.ts
index 07a69fe3..190a8a4d 100644
--- a/src/common/audio.ts
+++ b/src/common/audio.ts
@@ -1,4 +1,3 @@
-import Piscina from 'piscina';
import fsPromise from 'fs/promises';
import path from 'node:path';
import { randomUUID } from 'crypto';
@@ -6,16 +5,16 @@ import { EncodeResult, getDuration, getWavFileInfo, isSilk, isWav } from 'silk-w
import { LogWrapper } from '@/common/log';
import { EncodeArgs } from '@/common/audio-worker';
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];
-async function getWorkerPath() {
- return new URL(/* @vite-ignore */ './audio-worker.mjs', import.meta.url).href;
+function getWorkerPath() {
+ //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({
- filename: await getWorkerPath(),
-});
async function guessDuration(pttPath: string, logger: LogWrapper) {
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)
? await handleWavFile(file, filePath, pcmPath)
: { input: await FFmpegService.convert(filePath, pcmPath), sampleRate: 24000 };
- const silk = await piscina.run({ input: input, sampleRate: sampleRate });
+ const silk = await runTask(getWorkerPath(), { input: input, sampleRate: sampleRate });
fsPromise.unlink(pcmPath).catch((e) => logger.logError('删除临时文件失败', pcmPath, e));
await fsPromise.writeFile(pttPath, Buffer.from(silk.data));
logger.log(`语音文件${filePath}转换成功!`, pttPath, '时长:', silk.duration);
diff --git a/src/common/clean-task.ts b/src/common/clean-task.ts
new file mode 100644
index 00000000..56c8567e
--- /dev/null
+++ b/src/common/clean-task.ts
@@ -0,0 +1,229 @@
+import fs from 'fs';
+// generate Claude 3.7 Sonet Thinking
+
+interface FileRecord {
+ filePath: string;
+ addedTime: number;
+ retries: number;
+}
+
+interface CleanupTask {
+ fileRecord: FileRecord;
+ timer: NodeJS.Timeout;
+}
+
+class CleanupQueue {
+ private tasks: Map = new Map();
+ private readonly MAX_RETRIES = 3;
+ private isProcessing: boolean = false;
+ private pendingOperations: Array<() => void> = [];
+
+ /**
+ * 执行队列中的待处理操作,确保异步安全
+ */
+ private executeNextOperation(): void {
+ if (this.pendingOperations.length === 0) {
+ this.isProcessing = false;
+ return;
+ }
+
+ this.isProcessing = true;
+ const operation = this.pendingOperations.shift();
+ operation?.();
+
+ // 使用 setImmediate 允许事件循环继续,防止阻塞
+ setImmediate(() => this.executeNextOperation());
+ }
+
+ /**
+ * 安全执行操作,防止竞态条件
+ * @param operation 要执行的操作
+ */
+ private safeExecute(operation: () => void): void {
+ this.pendingOperations.push(operation);
+ if (!this.isProcessing) {
+ this.executeNextOperation();
+ }
+ }
+
+ /**
+ * 检查文件是否存在
+ * @param filePath 文件路径
+ * @returns 文件是否存在
+ */
+ private fileExists(filePath: string): boolean {
+ try {
+ return fs.existsSync(filePath);
+ } catch (error) {
+ //console.log(`检查文件存在出错: ${filePath}`, error);
+ return false;
+ }
+ }
+
+ /**
+ * 添加文件到清理队列
+ * @param filePath 文件路径
+ * @param cleanupDelay 清理延迟时间(毫秒)
+ */
+ addFile(filePath: string, cleanupDelay: number): void {
+ this.safeExecute(() => {
+ // 如果文件已在队列中,取消原来的计时器
+ if (this.tasks.has(filePath)) {
+ this.cancelCleanup(filePath);
+ }
+
+ // 创建新的文件记录
+ const fileRecord: FileRecord = {
+ filePath,
+ addedTime: Date.now(),
+ retries: 0
+ };
+
+ // 设置计时器
+ const timer = setTimeout(() => {
+ this.cleanupFile(fileRecord, cleanupDelay);
+ }, cleanupDelay);
+
+ // 添加到任务队列
+ this.tasks.set(filePath, { fileRecord, timer });
+ });
+ }
+
+ /**
+ * 批量添加文件到清理队列
+ * @param filePaths 文件路径数组
+ * @param cleanupDelay 清理延迟时间(毫秒)
+ */
+ addFiles(filePaths: string[], cleanupDelay: number): void {
+ this.safeExecute(() => {
+ for (const filePath of filePaths) {
+ // 内部直接处理,不通过 safeExecute 以保证批量操作的原子性
+ if (this.tasks.has(filePath)) {
+ // 取消已有的计时器,但不使用 cancelCleanup 方法以避免重复的安全检查
+ const existingTask = this.tasks.get(filePath);
+ if (existingTask) {
+ clearTimeout(existingTask.timer);
+ }
+ }
+
+ const fileRecord: FileRecord = {
+ filePath,
+ addedTime: Date.now(),
+ retries: 0
+ };
+
+ const timer = setTimeout(() => {
+ this.cleanupFile(fileRecord, cleanupDelay);
+ }, cleanupDelay);
+
+ this.tasks.set(filePath, { fileRecord, timer });
+ }
+ });
+ }
+
+ /**
+ * 清理文件
+ * @param record 文件记录
+ * @param delay 延迟时间,用于重试
+ */
+ private cleanupFile(record: FileRecord, delay: number): void {
+ this.safeExecute(() => {
+ // 首先检查文件是否存在,不存在则视为清理成功
+ if (!this.fileExists(record.filePath)) {
+ //console.log(`文件已不存在,跳过清理: ${record.filePath}`);
+ this.tasks.delete(record.filePath);
+ return;
+ }
+
+ try {
+ // 尝试删除文件
+ fs.unlinkSync(record.filePath);
+ // 删除成功,从队列中移除任务
+ this.tasks.delete(record.filePath);
+ } catch (error) {
+ const err = error as NodeJS.ErrnoException;
+
+ // 明确处理文件不存在的情况
+ if (err.code === 'ENOENT') {
+ //console.log(`文件在删除时不存在,视为清理成功: ${record.filePath}`);
+ this.tasks.delete(record.filePath);
+ return;
+ }
+
+ // 文件没有访问权限等情况
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
+ //console.error(`没有权限删除文件: ${record.filePath}`, err);
+ }
+
+ // 其他删除失败情况,考虑重试
+ if (record.retries < this.MAX_RETRIES - 1) {
+ // 还有重试机会,增加重试次数
+ record.retries++;
+ //console.log(`清理文件失败,将重试(${record.retries}/${this.MAX_RETRIES}): ${record.filePath}`);
+
+ // 设置相同的延迟时间再次尝试
+ const timer = setTimeout(() => {
+ this.cleanupFile(record, delay);
+ }, delay);
+
+ // 更新任务
+ this.tasks.set(record.filePath, { fileRecord: record, timer });
+ } else {
+ // 已达到最大重试次数,从队列中移除任务
+ this.tasks.delete(record.filePath);
+ //console.error(`清理文件失败,已达最大重试次数(${this.MAX_RETRIES}): ${record.filePath}`, error);
+ }
+ }
+ });
+ }
+
+ /**
+ * 取消文件的清理任务
+ * @param filePath 文件路径
+ * @returns 是否成功取消
+ */
+ cancelCleanup(filePath: string): boolean {
+ let cancelled = false;
+ this.safeExecute(() => {
+ const task = this.tasks.get(filePath);
+ if (task) {
+ clearTimeout(task.timer);
+ this.tasks.delete(filePath);
+ cancelled = true;
+ }
+ });
+ return cancelled;
+ }
+
+ /**
+ * 获取队列中的文件数量
+ * @returns 文件数量
+ */
+ getQueueSize(): number {
+ return this.tasks.size;
+ }
+
+ /**
+ * 获取所有待清理的文件
+ * @returns 文件路径数组
+ */
+ getPendingFiles(): string[] {
+ return Array.from(this.tasks.keys());
+ }
+
+ /**
+ * 清空所有清理任务
+ */
+ clearAll(): void {
+ this.safeExecute(() => {
+ // 取消所有定时器
+ for (const task of this.tasks.values()) {
+ clearTimeout(task.timer);
+ }
+ this.tasks.clear();
+ //console.log('已清空所有清理任务');
+ });
+ }
+}
+
+export const cleanTaskQueue = new CleanupQueue();
\ No newline at end of file
diff --git a/src/common/download-ffmpeg.ts b/src/common/download-ffmpeg.ts
new file mode 100644
index 00000000..1eb6dd27
--- /dev/null
+++ b/src/common/download-ffmpeg.ts
@@ -0,0 +1,352 @@
+// 更正导入语句
+import * as fs from 'fs';
+import * as path from 'path';
+import * as https from 'https';
+import * as os from 'os';
+import * as compressing from 'compressing'; // 修正导入方式
+import { pipeline } from 'stream/promises';
+import { fileURLToPath } from 'url';
+import { LogWrapper } from './log';
+
+const downloadOri = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2025-04-16-12-54/ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1.zip"
+const urls = [
+ "https://github.moeyy.xyz/" + downloadOri,
+ "https://ghp.ci/" + downloadOri,
+ "https://gh.api.99988866.xyz/" + downloadOri,
+ downloadOri
+];
+
+/**
+ * 测试URL是否可用
+ * @param url 待测试的URL
+ * @returns 如果URL可访问返回true,否则返回false
+ */
+async function testUrl(url: string): Promise {
+ return new Promise((resolve) => {
+ const req = https.get(url, { timeout: 5000 }, (res) => {
+ // 检查状态码是否表示成功
+ const statusCode = res.statusCode || 0;
+ if (statusCode >= 200 && statusCode < 300) {
+ // 终止请求并返回true
+ req.destroy();
+ resolve(true);
+ } else {
+ req.destroy();
+ resolve(false);
+ }
+ });
+
+ req.on('error', () => {
+ resolve(false);
+ });
+
+ req.on('timeout', () => {
+ req.destroy();
+ resolve(false);
+ });
+ });
+}
+
+/**
+ * 查找第一个可用的URL
+ * @returns 返回第一个可用的URL,如果都不可用则返回null
+ */
+async function findAvailableUrl(): Promise {
+ for (const url of urls) {
+ try {
+ const available = await testUrl(url);
+ if (available) {
+ return url;
+ }
+ } catch (error) {
+ // 忽略错误
+ }
+ }
+
+ return null;
+}
+/**
+ * 下载文件
+ * @param url 下载URL
+ * @param destPath 目标保存路径
+ * @returns 成功返回true,失败返回false
+ */
+async function downloadFile(url: string, destPath: string, progressCallback?: (percent: number) => void): Promise {
+ return new Promise((resolve) => {
+ const file = fs.createWriteStream(destPath);
+
+ const req = https.get(url, (res) => {
+ const statusCode = res.statusCode || 0;
+
+ if (statusCode >= 200 && statusCode < 300) {
+ // 获取文件总大小
+ const totalSize = parseInt(res.headers['content-length'] || '0', 10);
+ let downloadedSize = 0;
+ let lastReportedPercent = -1; // 上次报告的百分比
+ let lastReportTime = 0; // 上次报告的时间戳
+
+ // 如果有内容长度和进度回调,则添加数据监听
+ if (totalSize > 0 && progressCallback) {
+ // 初始报告 0%
+ progressCallback(0);
+ lastReportTime = Date.now();
+
+ res.on('data', (chunk) => {
+ downloadedSize += chunk.length;
+ const currentPercent = Math.floor((downloadedSize / totalSize) * 100);
+ const now = Date.now();
+
+ // 只在以下条件触发回调:
+ // 1. 百分比变化至少为1%
+ // 2. 距离上次报告至少500毫秒
+ // 3. 确保报告100%完成
+ if ((currentPercent !== lastReportedPercent &&
+ (currentPercent - lastReportedPercent >= 1 || currentPercent === 100)) &&
+ (now - lastReportTime >= 1000 || currentPercent === 100)) {
+
+ progressCallback(currentPercent);
+ lastReportedPercent = currentPercent;
+ lastReportTime = now;
+ }
+ });
+ }
+
+ pipeline(res, file)
+ .then(() => {
+ // 确保最后报告100%
+ if (progressCallback && lastReportedPercent !== 100) {
+ progressCallback(100);
+ }
+ resolve(true);
+ })
+ .catch(() => resolve(false));
+ } else {
+ file.close();
+ fs.unlink(destPath, () => { });
+ resolve(false);
+ }
+ });
+
+ req.on('error', () => {
+ file.close();
+ fs.unlink(destPath, () => { });
+ resolve(false);
+ });
+ });
+}
+
+/**
+ * 解压缩zip文件中的特定内容
+ * 只解压bin目录中的文件到目标目录
+ * @param zipPath 压缩文件路径
+ * @param extractDir 解压目标路径
+ */
+async function extractBinDirectory(zipPath: string, extractDir: string): Promise {
+ try {
+ // 确保目标目录存在
+ if (!fs.existsSync(extractDir)) {
+ fs.mkdirSync(extractDir, { recursive: true });
+ }
+
+ // 解压文件
+ const zipStream = new compressing.zip.UncompressStream({ source: zipPath });
+
+ return new Promise((resolve, reject) => {
+ // 监听条目事件
+ zipStream.on('entry', (header, stream, next) => {
+ // 获取文件路径
+ const filePath = header.name;
+
+ // 匹配内层bin目录中的文件
+ // 例如:ffmpeg-n7.1.1-6-g48c0f071d4-win64-lgpl-7.1/bin/ffmpeg.exe
+ if (filePath.includes('/bin/') && filePath.endsWith('.exe')) {
+ // 提取文件名
+ const fileName = path.basename(filePath);
+ const targetPath = path.join(extractDir, fileName);
+
+ // 创建写入流
+ const writeStream = fs.createWriteStream(targetPath);
+
+ // 将流管道连接到文件
+ stream.pipe(writeStream);
+
+ // 监听写入完成事件
+ writeStream.on('finish', () => {
+ next();
+ });
+
+ writeStream.on('error', () => {
+ next();
+ });
+ } else {
+ // 跳过不需要的文件
+ stream.resume();
+ next();
+ }
+ });
+
+ zipStream.on('error', (err) => {
+ reject(err);
+ });
+
+ zipStream.on('finish', () => {
+ resolve();
+ });
+ });
+ } catch (err) {
+ throw err;
+ }
+}
+
+/**
+ * 下载并设置FFmpeg
+ * @param destDir 目标安装目录,默认为用户临时目录下的ffmpeg文件夹
+ * @param tempDir 临时文件目录,默认为系统临时目录
+ * @returns 返回ffmpeg可执行文件的路径,如果失败则返回null
+ */
+export async function downloadFFmpeg(
+ destDir?: string,
+ tempDir?: string,
+ progressCallback?: (percent: number, stage: string) => void
+): Promise {
+ // 仅限Windows
+ if (os.platform() !== 'win32') {
+ return null;
+ }
+
+ const destinationDir = destDir || path.join(os.tmpdir(), 'ffmpeg');
+ const tempDirectory = tempDir || os.tmpdir();
+ const zipFilePath = path.join(tempDirectory, 'ffmpeg.zip'); // 临时下载到指定临时目录
+ const ffmpegExePath = path.join(destinationDir, 'ffmpeg.exe');
+
+ // 确保目录存在
+ if (!fs.existsSync(destinationDir)) {
+ fs.mkdirSync(destinationDir, { recursive: true });
+ }
+
+ // 确保临时目录存在
+ if (!fs.existsSync(tempDirectory)) {
+ fs.mkdirSync(tempDirectory, { recursive: true });
+ }
+
+ // 如果ffmpeg已经存在,直接返回路径
+ if (fs.existsSync(ffmpegExePath)) {
+ if (progressCallback) progressCallback(100, '已找到FFmpeg');
+ return ffmpegExePath;
+ }
+
+ // 查找可用URL
+ if (progressCallback) progressCallback(0, '查找可用下载源');
+ const availableUrl = await findAvailableUrl();
+ if (!availableUrl) {
+ return null;
+ }
+
+ // 下载文件
+ if (progressCallback) progressCallback(5, '开始下载FFmpeg');
+ const downloaded = await downloadFile(
+ availableUrl,
+ zipFilePath,
+ (percent) => {
+ // 下载占总进度的70%
+ if (progressCallback) progressCallback(5 + Math.floor(percent * 0.7), '下载FFmpeg');
+ }
+ );
+
+ if (!downloaded) {
+ return null;
+ }
+
+ try {
+ // 直接解压bin目录文件到目标目录
+ if (progressCallback) progressCallback(75, '解压FFmpeg');
+ await extractBinDirectory(zipFilePath, destinationDir);
+
+ // 清理下载文件
+ if (progressCallback) progressCallback(95, '清理临时文件');
+ try {
+ fs.unlinkSync(zipFilePath);
+ } catch (err) {
+ // 忽略清理临时文件失败的错误
+ }
+
+ // 检查ffmpeg.exe是否成功解压
+ if (fs.existsSync(ffmpegExePath)) {
+ if (progressCallback) progressCallback(100, 'FFmpeg安装完成');
+ return ffmpegExePath;
+ } else {
+ return null;
+ }
+ } catch (err) {
+ return null;
+ }
+}
+
+/**
+ * 检查系统PATH环境变量中是否存在指定可执行文件
+ * @param executable 可执行文件名
+ * @returns 如果找到返回完整路径,否则返回null
+ */
+function findExecutableInPath(executable: string): string | null {
+ // 仅适用于Windows系统
+ if (os.platform() !== 'win32') return null;
+
+ // 获取PATH环境变量
+ const pathEnv = process.env['PATH'] || '';
+ const pathDirs = pathEnv.split(';');
+
+ // 检查每个目录
+ for (const dir of pathDirs) {
+ if (!dir) continue;
+ try {
+ const filePath = path.join(dir, executable);
+ if (fs.existsSync(filePath)) {
+ return filePath;
+ }
+ } catch (error) {
+ continue;
+ }
+ }
+
+ return null;
+}
+
+export async function downloadFFmpegIfNotExists(log: LogWrapper) {
+ // 仅限Windows
+ if (os.platform() !== 'win32') {
+ return {
+ path: null,
+ reset: false
+ };
+ }
+ const ffmpegInPath = findExecutableInPath('ffmpeg.exe');
+ const ffprobeInPath = findExecutableInPath('ffprobe.exe');
+
+ if (ffmpegInPath && ffprobeInPath) {
+ const ffmpegDir = path.dirname(ffmpegInPath);
+ return {
+ path: ffmpegDir,
+ reset: true
+ };
+ }
+
+ // 如果环境变量中没有,检查项目目录中是否存在
+ const currentPath = path.dirname(fileURLToPath(import.meta.url));
+ const ffmpeg_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffmpeg.exe'));
+ const ffprobe_exist = fs.existsSync(path.join(currentPath, 'ffmpeg', 'ffprobe.exe'));
+
+ if (!ffmpeg_exist || !ffprobe_exist) {
+ await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
+ log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
+ });
+ return {
+ path: path.join(currentPath, 'ffmpeg'),
+ reset: true
+ }
+ }
+
+ return {
+ path: path.join(currentPath, 'ffmpeg'),
+ reset: true
+ }
+}
\ No newline at end of file
diff --git a/src/common/ffmpeg-worker.ts b/src/common/ffmpeg-worker.ts
deleted file mode 100644
index 38b0afa8..00000000
--- a/src/common/ffmpeg-worker.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { FFmpeg } from '@ffmpeg.wasm/main';
-import { randomUUID } from 'crypto';
-import { readFileSync, statSync, writeFileSync } from 'fs';
-import type { VideoInfo } from './video';
-import { fileTypeFromFile } from 'file-type';
-import imageSize from 'image-size';
-class FFmpegService {
- public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise {
- const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
- const videoFileName = `${randomUUID()}.mp4`;
- const outputFileName = `${randomUUID()}.jpg`;
- try {
- ffmpegInstance.fs.writeFile(videoFileName, readFileSync(videoPath));
- const code = await ffmpegInstance.run('-i', videoFileName, '-ss', '00:00:01.000', '-vframes', '1', outputFileName);
- if (code !== 0) {
- throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
- }
- const thumbnail = ffmpegInstance.fs.readFile(outputFileName);
- writeFileSync(thumbnailPath, thumbnail);
- } catch (error) {
- console.error('Error extracting thumbnail:', error);
- throw error;
- } finally {
- try {
- ffmpegInstance.fs.unlink(outputFileName);
- } catch (unlinkError) {
- console.error('Error unlinking output file:', unlinkError);
- }
- try {
- ffmpegInstance.fs.unlink(videoFileName);
- } catch (unlinkError) {
- console.error('Error unlinking video file:', unlinkError);
- }
- }
- }
-
- public static async convertFile(inputFile: string, outputFile: string, format: string): Promise {
- const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
- const inputFileName = `${randomUUID()}.pcm`;
- const outputFileName = `${randomUUID()}.${format}`;
- try {
- ffmpegInstance.fs.writeFile(inputFileName, readFileSync(inputFile));
- const params = format === 'amr'
- ? ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, '-ar', '8000', '-b:a', '12.2k', outputFileName]
- : ['-f', 's16le', '-ar', '24000', '-ac', '1', '-i', inputFileName, outputFileName];
- const code = await ffmpegInstance.run(...params);
- if (code !== 0) {
- throw new Error('Error extracting thumbnail: FFmpeg process exited with code ' + code);
- }
- const outputData = ffmpegInstance.fs.readFile(outputFileName);
- writeFileSync(outputFile, outputData);
- } catch (error) {
- console.error('Error converting file:', error);
- throw error;
- } finally {
- try {
- ffmpegInstance.fs.unlink(outputFileName);
- } catch (unlinkError) {
- console.error('Error unlinking output file:', unlinkError);
- }
- try {
- ffmpegInstance.fs.unlink(inputFileName);
- } catch (unlinkError) {
- console.error('Error unlinking input file:', unlinkError);
- }
- }
- }
-
- public static async convert(filePath: string, pcmPath: string): Promise {
- const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
- const inputFileName = `${randomUUID()}.input`;
- const outputFileName = `${randomUUID()}.pcm`;
- try {
- ffmpegInstance.fs.writeFile(inputFileName, readFileSync(filePath));
- const params = ['-y', '-i', inputFileName, '-ar', '24000', '-ac', '1', '-f', 's16le', outputFileName];
- const code = await ffmpegInstance.run(...params);
- if (code !== 0) {
- throw new Error('FFmpeg process exited with code ' + code);
- }
- const outputData = ffmpegInstance.fs.readFile(outputFileName);
- writeFileSync(pcmPath, outputData);
- return Buffer.from(outputData);
- } catch (error: any) {
- throw new Error('FFmpeg处理转换出错: ' + error.message);
- } finally {
- try {
- ffmpegInstance.fs.unlink(outputFileName);
- } catch (unlinkError) {
- console.error('Error unlinking output file:', unlinkError);
- }
- try {
- ffmpegInstance.fs.unlink(inputFileName);
- } catch (unlinkError) {
- console.error('Error unlinking output file:', unlinkError);
- }
- }
- }
-
- public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise {
- await FFmpegService.extractThumbnail(videoPath, thumbnailPath);
- const fileType = (await fileTypeFromFile(videoPath))?.ext ?? 'mp4';
- const inputFileName = `${randomUUID()}.${fileType}`;
- const ffmpegInstance = await FFmpeg.create({ core: '@ffmpeg.wasm/core-mt' });
- ffmpegInstance.fs.writeFile(inputFileName, readFileSync(videoPath));
- ffmpegInstance.setLogging(true);
- let duration = 60;
- ffmpegInstance.setLogger((_level, ...msg) => {
- const message = msg.join(' ');
- const durationMatch = message.match(/Duration: (\d+):(\d+):(\d+\.\d+)/);
- if (durationMatch) {
- const hours = parseInt(durationMatch[1] ?? '0', 10);
- const minutes = parseInt(durationMatch[2] ?? '0', 10);
- const seconds = parseFloat(durationMatch[3] ?? '0');
- duration = hours * 3600 + minutes * 60 + seconds;
- }
- });
- await ffmpegInstance.run('-i', inputFileName);
- const image = imageSize(thumbnailPath);
- ffmpegInstance.fs.unlink(inputFileName);
- const fileSize = statSync(videoPath).size;
- return {
- width: image.width ?? 100,
- height: image.height ?? 100,
- time: duration,
- format: fileType,
- size: fileSize,
- filePath: videoPath
- };
- }
-}
-type FFmpegMethod = 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
-
-interface FFmpegTask {
- method: FFmpegMethod;
- args: any[];
-}
-export default async function handleFFmpegTask({ method, args }: FFmpegTask): Promise {
- switch (method) {
- case 'extractThumbnail':
- return await FFmpegService.extractThumbnail(...args as [string, string]);
- case 'convertFile':
- return await FFmpegService.convertFile(...args as [string, string, string]);
- case 'convert':
- return await FFmpegService.convert(...args as [string, string]);
- case 'getVideoInfo':
- return await FFmpegService.getVideoInfo(...args as [string, string]);
- default:
- throw new Error(`Unknown method: ${method}`);
- }
-}
\ No newline at end of file
diff --git a/src/common/ffmpeg.ts b/src/common/ffmpeg.ts
index dbb543f4..40385d23 100644
--- a/src/common/ffmpeg.ts
+++ b/src/common/ffmpeg.ts
@@ -1,50 +1,195 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import Piscina from 'piscina';
-import { VideoInfo } from './video';
-
-type EncodeArgs = {
- method: 'extractThumbnail' | 'convertFile' | 'convert' | 'getVideoInfo';
- args: any[];
+import { readFileSync, statSync, existsSync, mkdirSync } from 'fs';
+import path, { dirname } from 'path';
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import type { VideoInfo } from './video';
+import { fileTypeFromFile } from 'file-type';
+import imageSize from 'image-size';
+import { fileURLToPath } from 'node:url';
+import { platform } from 'node:os';
+import { LogWrapper } from './log';
+const currentPath = dirname(fileURLToPath(import.meta.url));
+const execFileAsync = promisify(execFile);
+const getFFmpegPath = (tool: string): string => {
+ if (process.platform === 'win32') {
+ const exeName = `${tool}.exe`;
+ const isLocalExeExists = existsSync(path.join(currentPath, 'ffmpeg', exeName));
+ return isLocalExeExists ? path.join(currentPath, 'ffmpeg', exeName) : exeName;
+ }
+ return tool;
};
-
-type EncodeResult = any;
-
-async function getWorkerPath() {
- return new URL(/* @vite-ignore */ './ffmpeg-worker.mjs', import.meta.url).href;
-}
-
+export let FFMPEG_CMD = getFFmpegPath('ffmpeg');
+export let FFPROBE_CMD = getFFmpegPath('ffprobe');
export class FFmpegService {
+ // 确保目标目录存在
+ public static setFfmpegPath(ffmpegPath: string,logger:LogWrapper): void {
+ if (platform() === 'win32') {
+ FFMPEG_CMD = path.join(ffmpegPath, 'ffmpeg.exe');
+ FFPROBE_CMD = path.join(ffmpegPath, 'ffprobe.exe');
+ logger.log('[Check] ffmpeg:', FFMPEG_CMD);
+ logger.log('[Check] ffprobe:', FFPROBE_CMD);
+ }
+ }
+ private static ensureDirExists(filePath: string): void {
+ const dir = dirname(filePath);
+ if (!existsSync(dir)) {
+ mkdirSync(dir, { recursive: true });
+ }
+ }
+
public static async extractThumbnail(videoPath: string, thumbnailPath: string): Promise {
- const piscina = new Piscina({
- filename: await getWorkerPath(),
- });
- await piscina.run({ method: 'extractThumbnail', args: [videoPath, thumbnailPath] });
- await piscina.destroy();
+ try {
+ this.ensureDirExists(thumbnailPath);
+
+ const { stderr } = await execFileAsync(FFMPEG_CMD, [
+ '-i', videoPath,
+ '-ss', '00:00:01.000',
+ '-vframes', '1',
+ '-y', // 覆盖输出文件
+ thumbnailPath
+ ]);
+
+ if (!existsSync(thumbnailPath)) {
+ throw new Error(`提取缩略图失败,输出文件不存在: ${stderr}`);
+ }
+ } catch (error) {
+ console.error('Error extracting thumbnail:', error);
+ throw new Error(`提取缩略图失败: ${(error as Error).message}`);
+ }
}
public static async convertFile(inputFile: string, outputFile: string, format: string): Promise {
- const piscina = new Piscina({
- filename: await getWorkerPath(),
- });
- await piscina.run({ method: 'convertFile', args: [inputFile, outputFile, format] });
- await piscina.destroy();
+ try {
+ this.ensureDirExists(outputFile);
+
+ const params = format === 'amr'
+ ? [
+ '-f', 's16le',
+ '-ar', '24000',
+ '-ac', '1',
+ '-i', inputFile,
+ '-ar', '8000',
+ '-b:a', '12.2k',
+ '-y',
+ outputFile
+ ]
+ : [
+ '-f', 's16le',
+ '-ar', '24000',
+ '-ac', '1',
+ '-i', inputFile,
+ '-y',
+ outputFile
+ ];
+
+ await execFileAsync(FFMPEG_CMD, params);
+
+ if (!existsSync(outputFile)) {
+ throw new Error('转换失败,输出文件不存在');
+ }
+ } catch (error) {
+ console.error('Error converting file:', error);
+ throw new Error(`文件转换失败: ${(error as Error).message}`);
+ }
}
public static async convert(filePath: string, pcmPath: string): Promise {
- const piscina = new Piscina({
- filename: await getWorkerPath(),
- });
- const result = await piscina.run({ method: 'convert', args: [filePath, pcmPath] });
- await piscina.destroy();
- return result;
+ try {
+ this.ensureDirExists(pcmPath);
+
+ await execFileAsync(FFMPEG_CMD, [
+ '-y',
+ '-i', filePath,
+ '-ar', '24000',
+ '-ac', '1',
+ '-f', 's16le',
+ pcmPath
+ ]);
+
+ if (!existsSync(pcmPath)) {
+ throw new Error('转换PCM失败,输出文件不存在');
+ }
+
+ return readFileSync(pcmPath);
+ } catch (error: any) {
+ throw new Error(`FFmpeg处理转换出错: ${error.message}`);
+ }
}
public static async getVideoInfo(videoPath: string, thumbnailPath: string): Promise {
- const piscina = new Piscina({
- filename: await getWorkerPath(),
- });
- const result = await piscina.run({ method: 'getVideoInfo', args: [videoPath, thumbnailPath] });
- await piscina.destroy();
- return result;
+ try {
+ // 并行执行获取文件信息和提取缩略图
+ const [fileInfo, duration] = await Promise.all([
+ this.getFileInfo(videoPath, thumbnailPath),
+ this.getVideoDuration(videoPath)
+ ]);
+
+ const result: VideoInfo = {
+ width: fileInfo.width,
+ height: fileInfo.height,
+ time: duration,
+ format: fileInfo.format,
+ size: fileInfo.size,
+ filePath: videoPath
+ };
+ return result;
+ } catch (error) {
+ throw error;
+ }
}
-}
+
+ private static async getFileInfo(videoPath: string, thumbnailPath: string): Promise<{
+ format: string,
+ size: number,
+ width: number,
+ height: number
+ }> {
+
+ // 获取文件大小和类型
+ const [fileType, fileSize] = await Promise.all([
+ fileTypeFromFile(videoPath).catch(() => {
+ return null;
+ }),
+ Promise.resolve(statSync(videoPath).size)
+ ]);
+
+
+ try {
+ await this.extractThumbnail(videoPath, thumbnailPath);
+ // 获取图片尺寸
+ const dimensions = imageSize(thumbnailPath);
+
+ return {
+ format: fileType?.ext ?? 'mp4',
+ size: fileSize,
+ width: dimensions.width ?? 100,
+ height: dimensions.height ?? 100
+ };
+ } catch (error) {
+ return {
+ format: fileType?.ext ?? 'mp4',
+ size: fileSize,
+ width: 100,
+ height: 100
+ };
+ }
+ }
+
+ private static async getVideoDuration(videoPath: string): Promise {
+ try {
+ // 使用FFprobe获取时长
+ const { stdout } = await execFileAsync(FFPROBE_CMD, [
+ '-v', 'error',
+ '-show_entries', 'format=duration',
+ '-of', 'default=noprint_wrappers=1:nokey=1',
+ videoPath
+ ]);
+
+ const duration = parseFloat(stdout.trim());
+
+ return isNaN(duration) ? 60 : duration;
+ } catch (error) {
+ return 60; // 默认时长
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/common/file.ts b/src/common/file.ts
index 4133cf7b..180cf6d1 100644
--- a/src/common/file.ts
+++ b/src/common/file.ts
@@ -76,7 +76,7 @@ export function calculateFileMD5(filePath: string): Promise {
const stream = fs.createReadStream(filePath);
const hash = crypto.createHash('md5');
- stream.on('data', (data: Buffer) => {
+ stream.on('data', (data) => {
// 当读取到数据时,更新哈希对象的状态
hash.update(data);
});
@@ -115,7 +115,7 @@ async function tryDownload(options: string | HttpDownloadOptions, useReferer: bo
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
- const fetchRes = await fetch(url, { headers }).catch((err) => {
+ const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
if (err.cause) {
throw err.cause;
}
@@ -182,28 +182,28 @@ export async function uriToLocalFile(dir: string, uri: string, filename: string
const filePath = path.join(dir, filename);
switch (UriType) {
- case FileUriType.Local: {
- const fileExt = path.extname(HandledUri);
- const localFileName = path.basename(HandledUri, fileExt) + fileExt;
- const tempFilePath = path.join(dir, filename + fileExt);
- fs.copyFileSync(HandledUri, tempFilePath);
- return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
- }
+ case FileUriType.Local: {
+ const fileExt = path.extname(HandledUri);
+ const localFileName = path.basename(HandledUri, fileExt) + fileExt;
+ const tempFilePath = path.join(dir, filename + fileExt);
+ fs.copyFileSync(HandledUri, tempFilePath);
+ return { success: true, errMsg: '', fileName: localFileName, path: tempFilePath };
+ }
- case FileUriType.Remote: {
- const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
- fs.writeFileSync(filePath, buffer);
- return { success: true, errMsg: '', fileName: filename, path: filePath };
- }
+ case FileUriType.Remote: {
+ const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
+ fs.writeFileSync(filePath, buffer);
+ return { success: true, errMsg: '', fileName: filename, path: filePath };
+ }
- case FileUriType.Base64: {
- const base64 = HandledUri.replace(/^base64:\/\//, '');
- const base64Buffer = Buffer.from(base64, 'base64');
- fs.writeFileSync(filePath, base64Buffer);
- return { success: true, errMsg: '', fileName: filename, path: filePath };
- }
+ case FileUriType.Base64: {
+ const base64 = HandledUri.replace(/^base64:\/\//, '');
+ const base64Buffer = Buffer.from(base64, 'base64');
+ fs.writeFileSync(filePath, base64Buffer);
+ return { success: true, errMsg: '', fileName: filename, path: filePath };
+ }
- default:
- return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
+ default:
+ return { success: false, errMsg: `识别URL失败, uri= ${uri}`, fileName: '', path: '' };
}
}
diff --git a/src/common/log.ts b/src/common/log.ts
index 5d29867a..441179d0 100644
--- a/src/common/log.ts
+++ b/src/common/log.ts
@@ -232,7 +232,7 @@ export function rawMessageToText(msg: RawMessage, recursiveLevel = 0): string {
tokens.push(`群聊 [${msg.peerName}(${msg.peerUin})]`);
}
if (msg.senderUin !== '0') {
- tokens.push(`[${msg.sendMemberName ?? msg.sendRemarkName ?? msg.sendNickName}(${msg.senderUin})]`);
+ tokens.push(`[${msg.sendMemberName || msg.sendRemarkName || msg.sendNickName}(${msg.senderUin})]`);
}
} else if (msg.chatType == ChatType.KCHATTYPEDATALINE) {
tokens.push('移动设备');
diff --git a/src/common/store.ts b/src/common/store.ts
index 6c6b21c8..c88fb57a 100644
--- a/src/common/store.ts
+++ b/src/common/store.ts
@@ -163,7 +163,7 @@ class Store {
const current = this.get(key);
if (current === null) {
- this.set(key, 1);
+ this.set(key, 1, 60);
return 1;
}
@@ -180,7 +180,7 @@ class Store {
}
const newValue = numericValue + 1;
- this.set(key, newValue);
+ this.set(key, newValue, 60);
return newValue;
}
}
diff --git a/src/common/version.ts b/src/common/version.ts
index 0498c616..269e7f2b 100644
--- a/src/common/version.ts
+++ b/src/common/version.ts
@@ -1 +1 @@
-export const napCatVersion = '4.5.22';
+export const napCatVersion = '4.7.43';
diff --git a/src/common/worker.ts b/src/common/worker.ts
new file mode 100644
index 00000000..da5c1321
--- /dev/null
+++ b/src/common/worker.ts
@@ -0,0 +1,35 @@
+import { Worker } from 'worker_threads';
+
+export async function runTask(workerScript: string, taskData: T): Promise {
+ let worker = new Worker(workerScript);
+ try {
+ return await new Promise((resolve, reject) => {
+ worker.on('message', (result: R) => {
+ if ((result as any)?.log) {
+ console.error('Worker Log--->:', (result as { log: string }).log);
+ }
+ if ((result as any)?.error) {
+ reject(new Error("Worker error: " + (result as { error: string }).error));
+ }
+ 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();
+ }
+}
+
diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts
index a6847ac7..209d6844 100644
--- a/src/core/apis/file.ts
+++ b/src/core/apis/file.ts
@@ -41,9 +41,10 @@ export class NTQQFileApi {
this.context = context;
this.core = core;
this.rkeyManager = new RkeyManager([
- 'https://rkey.napneko.icu/rkeys'
+ 'https://secret-service.bietiaop.com/rkeys',
+ 'http://ss.xingzhige.com/music_card/rkey',
],
- this.context.logger
+ this.context.logger
);
}
@@ -181,23 +182,30 @@ export class NTQQFileApi {
filePath = newFilePath;
const { fileName: _fileName, path, fileSize, md5 } = await this.core.apis.FileApi.uploadFile(filePath, ElementType.VIDEO);
+ context.deleteAfterSentFiles.push(path);
if (fileSize === 0) {
throw new Error('文件异常,大小为0');
}
const thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`);
fs.mkdirSync(pathLib.dirname(thumbDir), { recursive: true });
const thumbPath = pathLib.join(pathLib.dirname(thumbDir), `${md5}_0.png`);
- try {
- videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
- } catch {
- fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
- }
if (_diyThumbPath) {
try {
await this.copyFile(_diyThumbPath, thumbPath);
} catch (e) {
this.context.logger.logError('复制自定义缩略图失败', e);
}
+ } else {
+ try {
+ videoInfo = await FFmpegService.getVideoInfo(filePath, thumbPath);
+ if (!fs.existsSync(thumbPath)) {
+ this.context.logger.logError('获取视频缩略图失败', new Error('缩略图不存在'));
+ throw new Error('获取视频缩略图失败');
+ }
+ } catch (e) {
+ this.context.logger.logError('获取视频信息失败', e);
+ fs.writeFileSync(thumbPath, Buffer.from(defaultVideoThumbB64, 'base64'));
+ }
}
context.deleteAfterSentFiles.push(thumbPath);
const thumbSize = (await fsPromises.stat(thumbPath)).size;
@@ -223,7 +231,7 @@ export class NTQQFileApi {
},
};
}
- async createValidSendPttElement(pttPath: string): Promise {
+ async createValidSendPttElement(_context: SendMessageContext, pttPath: string): Promise {
const { converted, path: silkPath, duration } = await encodeSilk(pttPath, this.core.NapCatTempPath, this.core.context.logger);
if (!silkPath) {
@@ -300,18 +308,18 @@ export class NTQQFileApi {
element.elementType === ElementType.FILE
) {
switch (element.elementType) {
- case ElementType.PIC:
+ case ElementType.PIC:
element.picElement!.sourcePath = elementResults?.[elementIndex] ?? '';
- break;
- case ElementType.VIDEO:
+ break;
+ case ElementType.VIDEO:
element.videoElement!.filePath = elementResults?.[elementIndex] ?? '';
- break;
- case ElementType.PTT:
+ break;
+ case ElementType.PTT:
element.pttElement!.filePath = elementResults?.[elementIndex] ?? '';
- break;
- case ElementType.FILE:
+ break;
+ case ElementType.FILE:
element.fileElement!.filePath = elementResults?.[elementIndex] ?? '';
- break;
+ break;
}
elementIndex++;
}
@@ -337,6 +345,7 @@ export class NTQQFileApi {
'NodeIKernelMsgListener/onRichMediaDownloadComplete',
[{
fileModelId: '0',
+ downSourceType: 0,
downloadSourceType: 0,
triggerType: 1,
msgId: msgId,
diff --git a/src/core/apis/friend.ts b/src/core/apis/friend.ts
index c03a1999..a8254ec1 100644
--- a/src/core/apis/friend.ts
+++ b/src/core/apis/friend.ts
@@ -86,4 +86,31 @@ export class NTQQFriendApi {
accept,
});
}
+ async handleDoubtFriendRequest(friendUid: string, str1: string = '', str2: string = '') {
+ this.context.session.getBuddyService().approvalDoubtBuddyReq(friendUid, str1, str2);
+ }
+ async getDoubtFriendRequest(count: number) {
+ let date = Date.now().toString();
+ const [, ret] = await this.core.eventWrapper.callNormalEventV2(
+ 'NodeIKernelBuddyService/getDoubtBuddyReq',
+ 'NodeIKernelBuddyListener/onDoubtBuddyReqChange',
+ [date, count, ''],
+ () => true,
+ (data) => data.reqId === date
+ );
+ let requests = Promise.all(ret.doubtList.map(async (item) => {
+ return {
+ flag: item.uid, //注意强制String 非isNumeric 不遵守则不符合设计
+ uin: await this.core.apis.UserApi.getUinByUidV2(item.uid) ?? 0,// 信息字段
+ nick: item.nick, // 信息字段 这个不是nickname 可能是来源的群内的昵称
+ source: item.source, // 信息字段
+ reason: item.reason, // 信息字段
+ msg: item.msg, // 信息字段
+ group_code: item.groupCode, // 信息字段
+ time: item.reqTime, // 信息字段
+ type: 'doubt' //保留字段
+ };
+ }))
+ return requests;
+ }
}
diff --git a/src/core/apis/group.ts b/src/core/apis/group.ts
index 4ac216b7..423e1e2d 100644
--- a/src/core/apis/group.ts
+++ b/src/core/apis/group.ts
@@ -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',
@@ -165,7 +168,13 @@ export class NTQQGroupApi {
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) {
const groupCodeStr = groupCode.toString();
const memberUinOrUidStr = memberUinOrUid.toString();
@@ -209,6 +218,10 @@ export class NTQQGroupApi {
return this.context.session.getRichMediaService().deleteGroupFolder(groupCode, folderId);
}
+ async transGroupFile(groupCode: string, fileId: string) {
+ return this.context.session.getRichMediaService().transGroupFile(groupCode, fileId);
+ }
+
async addGroupEssence(groupCode: string, msgId: string) {
const MsgData = await this.context.session.getMsgService().getMsgsIncludeSelf({
chatType: 2,
@@ -339,9 +352,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: {
diff --git a/src/core/apis/user.ts b/src/core/apis/user.ts
index 64c1f799..1bb2ef42 100644
--- a/src/core/apis/user.ts
+++ b/src/core/apis/user.ts
@@ -90,7 +90,30 @@ export class NTQQUserApi {
() => true,
(profile) => profile.uid === uid,
);
- const RetUser: User = {
+ return profile;
+ }
+
+ async getUserDetailInfo(uid: string, no_cache: boolean = false): Promise {
+ let profile = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(uid, no_cache ? UserDetailSource.KSERVER : UserDetailSource.KDB), uid);
+ if (profile && profile.uin !== '0' && profile.commonExt) {
+ return {
+ ...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 || '',
+ };
+ }
+ this.context.logger.logDebug('[NapCat] [Mark] getUserDetailInfo Mode1 Failed.');
+ profile = await this.fetchUserDetailInfo(uid, UserDetailSource.KSERVER);
+ if (profile && profile.uin === '0') {
+ profile.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
+ }
+ return {
...profile.simpleInfo.status,
...profile.simpleInfo.vasInfo,
...profile.commonExt,
@@ -101,33 +124,6 @@ export class NTQQUserApi {
pendantId: '',
nick: profile.simpleInfo.coreInfo.nick || '',
};
- return RetUser;
- }
-
- async getUserDetailInfo(uid: string): Promise {
- let retUser = await solveAsyncProblem(async (uid) => this.fetchUserDetailInfo(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);
- if (retUser && retUser.uin === '0') {
- retUser.uin = await this.core.apis.UserApi.getUidByUinV2(uid) ?? '0';
- }
- return retUser;
- }
-
- async getUserDetailInfoV2(uid: string): Promise {
- const fallback = new Fallback((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) {
diff --git a/src/core/external/appid.json b/src/core/external/appid.json
index 563b0039..c5956064 100644
--- a/src/core/external/appid.json
+++ b/src/core/external/appid.json
@@ -186,5 +186,93 @@
"9.9.17-31363": {
"appid": 537266500,
"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"
+ },
+ "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"
+ },
+ "9.9.18-33800": {
+ "appid": 537273974,
+ "qua": "V1_WIN_NQ_9.9.18_33800_GW_B"
+ },
+ "3.2.16-33800": {
+ "appid": 537274009,
+ "qua": "V1_LNX_NQ_3.2.16_33800_GW_B"
+ },
+ "9.9.19-34231": {
+ "appid": 537279209,
+ "qua": "V1_WIN_NQ_9.9.19_34231_GW_B"
+ },
+ "3.2.17-34231": {
+ "appid": 537279245,
+ "qua": "V1_LNX_NQ_3.2.17_34231_GW_B"
+ },
+ "9.9.19-34362": {
+ "appid": 537279260,
+ "qua": "V1_WIN_NQ_9.9.19_34362_GW_B"
+ },
+ "3.2.17-34362": {
+ "appid": 537279296,
+ "qua": "V1_LNX_NQ_3.2.17_34362_GW_B"
+ },
+ "9.9.19-34467": {
+ "appid": 537282256,
+ "qua": "V1_WIN_NQ_9.9.19_34467_GW_B"
+ },
+ "3.2.17-34467": {
+ "appid": 537282292,
+ "qua": "V1_LNX_NQ_3.2.17_34467_GW_B"
+ },
+ "9.9.19-34566": {
+ "appid": 537282307,
+ "qua": "V1_WIN_NQ_9.9.19_34566_GW_B"
+ },
+ "3.2.17-34566": {
+ "appid": 537282343,
+ "qua": "V1_LNX_NQ_3.2.17_34566_GW_B"
+ },
+ "3.2.17-34606": {
+ "appid": 537282343,
+ "qua": "V1_LNX_NQ_3.2.17_34606_GW_B"
+ },
+ "9.9.19-34606": {
+ "appid": 537282307,
+ "qua": "V1_WIN_NQ_9.9.19_34606_GW_B"
}
-}
+}
\ No newline at end of file
diff --git a/src/core/external/napcat.json b/src/core/external/napcat.json
index 44952ac2..dcce2174 100644
--- a/src/core/external/napcat.json
+++ b/src/core/external/napcat.json
@@ -4,5 +4,6 @@
"fileLogLevel": "debug",
"consoleLogLevel": "info",
"packetBackend": "auto",
- "packetServer": ""
-}
+ "packetServer": "",
+ "o3HookMode": 1
+ }
\ No newline at end of file
diff --git a/src/core/external/offset.json b/src/core/external/offset.json
index 7b52b619..f76a61be 100644
--- a/src/core/external/offset.json
+++ b/src/core/external/offset.json
@@ -246,5 +246,109 @@
"6.9.65-31363-arm64": {
"send": "422CEF8",
"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"
+ },
+ "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"
+ },
+ "9.9.18-33800-x64": {
+ "send": "39F5870",
+ "recv": "39FA070"
+ },
+ "3.2.16-33800-x64": {
+ "send": "A634F60",
+ "recv": "A638980"
+ },
+ "3.2.16-33800-arm64": {
+ "send": "7262BB0",
+ "recv": "72664E0"
+ },
+ "9.9.19-34231-x64": {
+ "send": "3BD73D0",
+ "recv": "3BDBBD0"
+ },
+ "3.2.17-34231-x64": {
+ "send": "AD787E0",
+ "recv": "AD7C200"
+ },
+ "3.2.17-34231-arm64": {
+ "send": "770CDC0",
+ "recv": "77106F0"
+ },
+ "9.9.19-34362-x64": {
+ "send": "3BD80D0",
+ "recv": "3BDC8D0"
+ },
+ "9.9.19-34467-x64": {
+ "send": "3BD8690",
+ "recv": "3BDCE90"
+ },
+ "9.9.19-34566-x64": {
+ "send": "3BDA110",
+ "recv": "3BDE910"
+ },
+ "9.9.19-34606-x64": {
+ "send": "3BDA110",
+ "recv": "3BDE910"
+ },
+ "3.2.17-34606-x64": {
+ "send": "AD7DC60",
+ "recv": "AD81680"
+ },
+ "3.2.17-34606-arm64": {
+ "send": "7711270",
+ "recv": "7714BA0"
}
-}
+}
\ No newline at end of file
diff --git a/src/core/helper/config.ts b/src/core/helper/config.ts
index bc6781da..0c2540c1 100644
--- a/src/core/helper/config.ts
+++ b/src/core/helper/config.ts
@@ -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;
diff --git a/src/core/helper/rkey.ts b/src/core/helper/rkey.ts
index ecc3634d..47f748d4 100644
--- a/src/core/helper/rkey.ts
+++ b/src/core/helper/rkey.ts
@@ -6,6 +6,17 @@ interface ServerRkeyData {
private_rkey: string;
expired_time: number;
}
+interface OneBotApiRet {
+ status: string,
+ retcode: number,
+ data: ServerRkeyData,
+ message: string,
+ wording: string,
+}
+interface UrlFailureInfo {
+ count: number;
+ lastTimestamp: number;
+}
export class RkeyManager {
serverUrl: string[] = [];
@@ -15,9 +26,8 @@ export class RkeyManager {
private_rkey: '',
expired_time: 0,
};
- private failureCount: number = 0;
- private lastFailureTimestamp: number = 0;
- private readonly FAILURE_LIMIT: number = 8;
+ private urlFailures: Map = new Map();
+ private readonly FAILURE_LIMIT: number = 4;
private readonly ONE_DAY: number = 24 * 60 * 60 * 1000;
constructor(serverUrl: string[], logger: LogWrapper) {
@@ -26,50 +36,92 @@ export class RkeyManager {
}
async getRkey() {
- const now = new Date().getTime();
- if (now - this.lastFailureTimestamp > this.ONE_DAY) {
- this.failureCount = 0; // 重置失败计数器
- }
-
- if (this.failureCount >= this.FAILURE_LIMIT) {
- this.logger.logError('[Rkey] 服务存在异常, 图片使用FallBack机制');
- throw new Error('获取rkey失败次数过多,请稍后再试');
+ const availableUrls = this.getAvailableUrls();
+ if (availableUrls.length === 0) {
+ this.logger.logError('[Rkey] 所有服务均已禁用, 图片使用FallBack机制');
+ throw new Error('获取rkey失败:所有服务URL均已被禁用');
}
if (this.isExpired()) {
try {
await this.refreshRkey();
} catch (e) {
- throw new Error(`${e}`);//外抛
+ throw new Error(`${e}`);
}
}
return this.rkeyData;
}
+ private getAvailableUrls(): string[] {
+ return this.serverUrl.filter(url => !this.isUrlDisabled(url));
+ }
+
+ private isUrlDisabled(url: string): boolean {
+ const failureInfo = this.urlFailures.get(url);
+ if (!failureInfo) return false;
+
+ const now = new Date().getTime();
+ // 如果已经过了一天,重置失败计数
+ if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
+ failureInfo.count = 0;
+ this.urlFailures.set(url, failureInfo);
+ return false;
+ }
+
+ return failureInfo.count >= this.FAILURE_LIMIT;
+ }
+
+ private updateUrlFailure(url: string) {
+ const now = new Date().getTime();
+ const failureInfo = this.urlFailures.get(url) || { count: 0, lastTimestamp: 0 };
+
+ // 如果已经过了一天,重置失败计数
+ if (now - failureInfo.lastTimestamp > this.ONE_DAY) {
+ failureInfo.count = 1;
+ } else {
+ failureInfo.count++;
+ }
+
+ failureInfo.lastTimestamp = now;
+ this.urlFailures.set(url, failureInfo);
+
+ if (failureInfo.count >= this.FAILURE_LIMIT) {
+ this.logger.logError(`[Rkey] URL ${url} 已被禁用,失败次数达到 ${this.FAILURE_LIMIT} 次`);
+ }
+ }
+
isExpired(): boolean {
const now = new Date().getTime() / 1000;
return now > this.rkeyData.expired_time;
}
async refreshRkey() {
- //刷新rkey
- for (const url of this.serverUrl) {
+ const availableUrls = this.getAvailableUrls();
+
+ if (availableUrls.length === 0) {
+ this.logger.logError('[Rkey] 所有服务均已禁用');
+ throw new Error('获取rkey失败:所有服务URL均已被禁用');
+ }
+
+ for (const url of availableUrls) {
try {
- const temp = await RequestUtil.HttpGetJson(url, 'GET');
+ let temp = await RequestUtil.HttpGetJson(url, 'GET');
+ if ('retcode' in temp) {
+ // 支持Onebot Ret风格
+ temp = (temp as unknown as OneBotApiRet).data;
+ }
this.rkeyData = {
group_rkey: temp.group_rkey.slice(6),
private_rkey: temp.private_rkey.slice(6),
expired_time: temp.expired_time
};
- this.failureCount = 0;
return;
} catch (e) {
this.logger.logError(`[Rkey] 异常服务 ${url} 异常 / `, e);
- this.failureCount++;
- this.lastFailureTimestamp = new Date().getTime();
- //是否为最后一个url
- if (url === this.serverUrl[this.serverUrl.length - 1]) {
- throw new Error(`获取rkey失败: ${e}`);//外抛
+ this.updateUrlFailure(url);
+
+ if (url === availableUrls[availableUrls.length - 1]) {
+ throw new Error(`获取rkey失败: ${e}`);
}
}
}
diff --git a/src/core/listeners/NodeIKernelBuddyListener.ts b/src/core/listeners/NodeIKernelBuddyListener.ts
index 5dcfe243..edf29044 100644
--- a/src/core/listeners/NodeIKernelBuddyListener.ts
+++ b/src/core/listeners/NodeIKernelBuddyListener.ts
@@ -40,12 +40,30 @@ export class NodeIKernelBuddyListener {
}
onDelBatchBuddyInfos(arg: unknown): any {
+ console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
- onDoubtBuddyReqChange(arg: unknown): any {
+ onDoubtBuddyReqChange(_arg:
+ {
+ reqId: string;
+ cookie: string;
+ doubtList: Array<{
+ uid: string;
+ nick: string;
+ age: number,
+ sex: number;
+ commFriendNum: number;
+ reqTime: string;
+ msg: string;
+ source: string;
+ reason: string;
+ groupCode: string;
+ nameMore?: null;
+ }>;
+ }): void | Promise {
}
- onDoubtBuddyReqUnreadNumChange(arg: unknown): any {
+ onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise {
}
onNickUpdated(arg: unknown): any {
diff --git a/src/core/listeners/NodeIKernelLoginListener.ts b/src/core/listeners/NodeIKernelLoginListener.ts
index 91046e0b..51d05711 100644
--- a/src/core/listeners/NodeIKernelLoginListener.ts
+++ b/src/core/listeners/NodeIKernelLoginListener.ts
@@ -1,5 +1,5 @@
export class NodeIKernelLoginListener {
- onLoginConnected(...args: any[]): any {
+ onLoginConnected(): Promise | void {
}
onLoginDisConnected(...args: any[]): any {
diff --git a/src/core/listeners/NodeIKernelMsgListener.ts b/src/core/listeners/NodeIKernelMsgListener.ts
index 379f0289..6518a82a 100644
--- a/src/core/listeners/NodeIKernelMsgListener.ts
+++ b/src/core/listeners/NodeIKernelMsgListener.ts
@@ -21,7 +21,8 @@ export interface OnRichMediaDownloadCompleteParams {
clientMsg: string,
businessId: number,
userTotalSpacePerDay: unknown,
- userUsedSpacePerDay: unknown
+ userUsedSpacePerDay: unknown,
+ chatType: number,
}
export interface GroupFileInfoUpdateParamType {
@@ -97,112 +98,112 @@ export interface TempOnRecvParams {
}
export class NodeIKernelMsgListener {
- onAddSendMsg(msgRecord: RawMessage): any {
+ onAddSendMsg(_msgRecord: RawMessage): any {
}
- onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: unknown): any {
+ onBroadcastHelperDownloadComplete(_broadcastHelperTransNotifyInfo: unknown): any {
}
- onBroadcastHelperProgressUpdate(broadcastHelperTransNotifyInfo: unknown): any {
+ onBroadcastHelperProgressUpdate(_broadcastHelperTransNotifyInfo: unknown): any {
}
- onChannelFreqLimitInfoUpdate(contact: unknown, z: unknown, freqLimitInfo: unknown): any {
+ onChannelFreqLimitInfoUpdate(_contact: unknown, _z: unknown, _freqLimitInfo: unknown): any {
}
- onContactUnreadCntUpdate(hashMap: unknown): any {
+ onContactUnreadCntUpdate(_hashMap: unknown): any {
}
- onCustomWithdrawConfigUpdate(customWithdrawConfig: unknown): any {
+ onCustomWithdrawConfigUpdate(_customWithdrawConfig: unknown): any {
}
- onDraftUpdate(contact: unknown, arrayList: unknown, j2: unknown): any {
+ onDraftUpdate(_contact: unknown, _arrayList: unknown, _j2: unknown): any {
}
- onEmojiDownloadComplete(emojiNotifyInfo: unknown): any {
+ onEmojiDownloadComplete(_emojiNotifyInfo: unknown): any {
}
- onEmojiResourceUpdate(emojiResourceInfo: unknown): any {
+ onEmojiResourceUpdate(_emojiResourceInfo: unknown): any {
}
- onFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
+ onFeedEventUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
}
- onFileMsgCome(arrayList: unknown): any {
+ onFileMsgCome(_arrayList: unknown): any {
}
- onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
+ onFirstViewDirectMsgUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
}
- onFirstViewGroupGuildMapping(arrayList: unknown): any {
+ onFirstViewGroupGuildMapping(_arrayList: unknown): any {
}
- onGrabPasswordRedBag(i2: unknown, str: unknown, i3: unknown, recvdOrder: unknown, msgRecord: unknown): any {
+ onGrabPasswordRedBag(_i2: unknown, _str: unknown, _i3: unknown, _recvdOrder: unknown, _msgRecord: unknown): any {
}
- onGroupFileInfoAdd(groupItem: unknown): any {
+ onGroupFileInfoAdd(_groupItem: unknown): any {
}
- onGroupFileInfoUpdate(groupFileListResult: GroupFileInfoUpdateParamType): any {
+ onGroupFileInfoUpdate(_groupFileListResult: GroupFileInfoUpdateParamType): any {
}
- onGroupGuildUpdate(groupGuildNotifyInfo: unknown): any {
+ onGroupGuildUpdate(_groupGuildNotifyInfo: unknown): any {
}
- onGroupTransferInfoAdd(groupItem: unknown): any {
+ onGroupTransferInfoAdd(_groupItem: unknown): any {
}
- onGroupTransferInfoUpdate(groupFileListResult: unknown): any {
+ onGroupTransferInfoUpdate(_groupFileListResult: unknown): any {
}
- onGuildInteractiveUpdate(guildInteractiveNotificationItem: unknown): any {
+ onGuildInteractiveUpdate(_guildInteractiveNotificationItem: unknown): any {
}
- onGuildMsgAbFlagChanged(guildMsgAbFlag: unknown): any {
+ onGuildMsgAbFlagChanged(_guildMsgAbFlag: unknown): any {
}
- onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: unknown): any {
+ onGuildNotificationAbstractUpdate(_guildNotificationAbstractInfo: unknown): any {
}
- onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: unknown): any {
+ onHitCsRelatedEmojiResult(_downloadRelateEmojiResultInfo: unknown): any {
}
- onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: unknown): any {
+ onHitEmojiKeywordResult(_hitRelatedEmojiWordsResult: unknown): any {
}
- onHitRelatedEmojiResult(relatedWordEmojiInfo: unknown): any {
+ onHitRelatedEmojiResult(_relatedWordEmojiInfo: unknown): any {
}
- onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: unknown): any {
+ onImportOldDbProgressUpdate(_importOldDbMsgNotifyInfo: unknown): any {
}
- onInputStatusPush(inputStatusInfo: {
+ onInputStatusPush(_inputStatusInfo: {
chatType: number;
eventType: number;
fromUin: string;
@@ -215,55 +216,55 @@ export class NodeIKernelMsgListener {
}
- onKickedOffLine(kickedInfo: KickedOffLineInfo): any {
+ onKickedOffLine(_kickedInfo: KickedOffLineInfo): any {
}
- onLineDev(arrayList: unknown): any {
+ onLineDev(_arrayList: unknown): any {
}
- onLogLevelChanged(j2: unknown): any {
+ onLogLevelChanged(_j2: unknown): any {
}
- onMsgAbstractUpdate(arrayList: unknown): any {
+ onMsgAbstractUpdate(_arrayList: unknown): any {
}
- onMsgBoxChanged(arrayList: unknown): any {
+ onMsgBoxChanged(_arrayList: unknown): any {
}
- onMsgDelete(contact: unknown, arrayList: unknown): any {
+ onMsgDelete(_contact: unknown, _arrayList: unknown): any {
}
- onMsgEventListUpdate(hashMap: unknown): any {
+ onMsgEventListUpdate(_hashMap: unknown): any {
}
- onMsgInfoListAdd(arrayList: unknown): any {
+ onMsgInfoListAdd(_arrayList: unknown): any {
}
- onMsgInfoListUpdate(msgList: RawMessage[]): any {
+ onMsgInfoListUpdate(_msgList: RawMessage[]): any {
}
- onMsgQRCodeStatusChanged(i2: unknown): any {
+ onMsgQRCodeStatusChanged(_i2: unknown): any {
}
- onMsgRecall(chatType: ChatType, uid: string, msgSeq: string): any {
+ onMsgRecall(_chatType: ChatType, _uid: string, _msgSeq: string): any {
}
- onMsgSecurityNotify(msgRecord: unknown): any {
+ onMsgSecurityNotify(_msgRecord: unknown): any {
}
- onMsgSettingUpdate(msgSetting: unknown): any {
+ onMsgSettingUpdate(_msgSetting: unknown): any {
}
@@ -279,108 +280,108 @@ export class NodeIKernelMsgListener {
}
- onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: unknown): any {
+ onReadFeedEventUpdate(_firstViewDirectMsgNotifyInfo: unknown): any {
}
- onRecvGroupGuildFlag(i2: unknown): any {
+ onRecvGroupGuildFlag(_i2: unknown): any {
}
- onRecvMsg(arrayList: RawMessage[]): any {
+ onRecvMsg(_arrayList: RawMessage[]): any {
}
- onRecvMsgSvrRspTransInfo(j2: unknown, contact: unknown, i2: unknown, i3: unknown, str: unknown, bArr: unknown): any {
+ onRecvMsgSvrRspTransInfo(_j2: unknown, _contact: unknown, _i2: unknown, _i3: unknown, _str: unknown, _bArr: unknown): any {
}
- onRecvOnlineFileMsg(arrayList: unknown): any {
+ onRecvOnlineFileMsg(_arrayList: unknown): any {
}
- onRecvS2CMsg(arrayList: unknown): any {
+ onRecvS2CMsg(_arrayList: unknown): any {
}
- onRecvSysMsg(arrayList: Array): any {
+ onRecvSysMsg(_arrayList: Array): any {
}
- onRecvUDCFlag(i2: unknown): any {
+ onRecvUDCFlag(_i2: unknown): any {
}
- onRichMediaDownloadComplete(fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
+ onRichMediaDownloadComplete(_fileTransNotifyInfo: OnRichMediaDownloadCompleteParams): any {
}
- onRichMediaProgerssUpdate(fileTransNotifyInfo: unknown): any {
+ onRichMediaProgerssUpdate(_fileTransNotifyInfo: unknown): any {
}
- onRichMediaUploadComplete(fileTransNotifyInfo: unknown): any {
+ onRichMediaUploadComplete(_fileTransNotifyInfo: unknown): any {
}
- onSearchGroupFileInfoUpdate(searchGroupFileResult: unknown): any {
+ onSearchGroupFileInfoUpdate(_searchGroupFileResult: unknown): any {
}
- onSendMsgError(j2: unknown, contact: unknown, i2: unknown, str: unknown): any {
+ onSendMsgError(_j2: unknown, _contact: unknown, _i2: unknown, _str: unknown): any {
}
- onSysMsgNotification(i2: unknown, j2: unknown, j3: unknown, arrayList: unknown): any {
+ onSysMsgNotification(_i2: unknown, _j2: unknown, _j3: unknown, _arrayList: unknown): any {
}
- onTempChatInfoUpdate(tempChatInfo: TempOnRecvParams): any {
+ onTempChatInfoUpdate(_tempChatInfo: TempOnRecvParams): any {
}
- onUnreadCntAfterFirstView(hashMap: unknown): any {
+ onUnreadCntAfterFirstView(_hashMap: unknown): any {
}
- onUnreadCntUpdate(hashMap: unknown): any {
+ onUnreadCntUpdate(_hashMap: unknown): any {
}
- onUserChannelTabStatusChanged(z: unknown): any {
+ onUserChannelTabStatusChanged(_z: unknown): any {
}
- onUserOnlineStatusChanged(z: unknown): any {
+ onUserOnlineStatusChanged(_z: unknown): any {
}
- onUserTabStatusChanged(arrayList: unknown): any {
+ onUserTabStatusChanged(_arrayList: unknown): any {
}
- onlineStatusBigIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
+ onlineStatusBigIconDownloadPush(_i2: unknown, _j2: unknown, _str: unknown): any {
}
- onlineStatusSmallIconDownloadPush(i2: unknown, j2: unknown, str: unknown): any {
+ onlineStatusSmallIconDownloadPush(_i2: unknown, _j2: unknown, _str: unknown): any {
}
// 第一次发现于Linux
- onUserSecQualityChanged(...args: unknown[]): any {
+ onUserSecQualityChanged(..._args: unknown[]): any {
}
- onMsgWithRichLinkInfoUpdate(...args: unknown[]): any {
+ onMsgWithRichLinkInfoUpdate(..._args: unknown[]): any {
}
- onRedTouchChanged(...args: unknown[]): any {
+ onRedTouchChanged(..._args: unknown[]): any {
}
// 第一次发现于Win 9.9.9-23159
- onBroadcastHelperProgerssUpdate(...args: unknown[]): any {
+ onBroadcastHelperProgerssUpdate(..._args: unknown[]): any {
}
}
diff --git a/src/core/listeners/NodeIKernelSearchListener.ts b/src/core/listeners/NodeIKernelSearchListener.ts
index cd7d3a10..e0a34e9d 100644
--- a/src/core/listeners/NodeIKernelSearchListener.ts
+++ b/src/core/listeners/NodeIKernelSearchListener.ts
@@ -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,
+ msgAbstract: unknown,
+ }>
+ }): void | Promise;
}
diff --git a/src/core/packet/client/nativeClient.ts b/src/core/packet/client/nativeClient.ts
index ad3ae995..bb0ec2e2 100644
--- a/src/core/packet/client/nativeClient.ts
+++ b/src/core/packet/client/nativeClient.ts
@@ -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 {
+ async init(_pid: number, recv: string, send: string): Promise {
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;
}
diff --git a/src/core/packet/client/wsClient.ts b/src/core/packet/client/wsClient.ts
deleted file mode 100644
index dc4d9929..00000000
--- a/src/core/packet/client/wsClient.ts
+++ /dev/null
@@ -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 {
- 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 {
- 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 {
- 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 {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
-
- private async handleMessage(message: Data): Promise {
- 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}`);
- }
- }
-}
diff --git a/src/core/packet/context/clientContext.ts b/src/core/packet/context/clientContext.ts
index 9e77bd7e..e4419139 100644
--- a/src/core/packet/context/clientContext.ts
+++ b/src/core/packet/context/clientContext.ts
@@ -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();
diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts
index e9c89cba..0e558058 100644
--- a/src/core/packet/context/operationContext.ts
+++ b/src/core/packet/context/operationContext.ts
@@ -1,22 +1,22 @@
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 {PacketMsg} from '@/core/packet/message/message';
+import { PacketMsg } from '@/core/packet/message/message';
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgVideoElement
} from '@/core/packet/message/element';
-import {ChatType, MsgSourceType, NTMsgType, RawMessage} from '@/core';
-import {MiniAppRawData, MiniAppReqParams} from '@/core/packet/entities/miniApp';
-import {AIVoiceChatType} from '@/core/packet/entities/aiChat';
-import {NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg} from '@napneko/nap-proto-core';
-import {IndexNode, LongMsgResult, MsgInfo} from '@/core/packet/transformer/proto';
-import {OidbPacket} from '@/core/packet/transformer/base';
-import {ImageOcrResult} from '@/core/packet/entities/ocrResult';
-import {gunzipSync} from 'zlib';
-import {PacketMsgConverter} from '@/core/packet/message/converter';
+import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
+import { MiniAppRawData, MiniAppReqParams } from '@/core/packet/entities/miniApp';
+import { AIVoiceChatType } from '@/core/packet/entities/aiChat';
+import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
+import { IndexNode, LongMsgResult, MsgInfo } from '@/core/packet/transformer/proto';
+import { OidbPacket } from '@/core/packet/transformer/base';
+import { ImageOcrResult } from '@/core/packet/entities/ocrResult';
+import { gunzipSync } from 'zlib';
+import { PacketMsgConverter } from '@/core/packet/message/converter';
export class PacketOperationContext {
private readonly context: PacketContext;
@@ -59,17 +59,17 @@ export class PacketOperationContext {
const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value);
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));
- return {status: 10, ext_status: status};
+ return { status: 10, ext_status: status };
} catch {
return undefined;
}
}
- async SetGroupSpecialTitle(groupUin: number, uid: string, tittle: string) {
- const req = trans.SetSpecialTitle.build(groupUin, uid, tittle);
+ async SetGroupSpecialTitle(groupUin: number, uid: string, title: string) {
+ const req = trans.SetSpecialTitle.build(groupUin, uid, title);
await this.context.client.sendOidbPacket(req);
}
@@ -79,13 +79,13 @@ export class PacketOperationContext {
const reqList = msg.flatMap(m =>
m.msg.map(e => {
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) {
- return this.context.highway.uploadVideo({chatType, peerUid}, e);
+ return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} 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) {
- return this.context.highway.uploadFile({chatType, peerUid}, e);
+ return this.context.highway.uploadFile({ chatType, peerUid }, e);
}
return null;
}).filter(Boolean)
@@ -154,12 +154,32 @@ export class PacketOperationContext {
return res.result.resId;
}
+ async MoveGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string) {
+ const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory);
+ const resp = await this.context.client.sendOidbPacket(req, true);
+ const res = trans.MoveGroupFile.parse(resp);
+ return res.move.retCode;
+ }
+
+ async RenameGroupFile(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) {
+ const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName);
+ const resp = await this.context.client.sendOidbPacket(req, true);
+ const res = trans.RenameGroupFile.parse(resp);
+ return res.rename.retCode;
+ }
+
async GetGroupFileUrl(groupUin: number, fileUUID: string) {
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupFile.parse(resp);
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) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
diff --git a/src/core/packet/highway/highwayContext.ts b/src/core/packet/highway/highwayContext.ts
index 76904cfd..21413dae 100644
--- a/src/core/packet/highway/highwayContext.ts
+++ b/src/core/packet/highway/highwayContext.ts
@@ -144,7 +144,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -181,7 +181,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 md5 = Buffer.from(index.info.fileHash, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -219,7 +219,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
@@ -244,16 +244,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadGroupVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
- if (subFile.uKey && subFile.uKey != '') {
- this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
- const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
+ if (subFile!.uKey && subFile!.uKey != '') {
+ this.logger.debug(`[Highway] uploadGroupVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
+ const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
- uKey: subFile.uKey,
+ uKey: subFile!.uKey,
network: {
- ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
+ ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
@@ -269,7 +269,7 @@ export class PacketHighwayContext {
extend
);
} 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;
}
@@ -284,7 +284,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
@@ -309,16 +309,16 @@ export class PacketHighwayContext {
this.logger.debug(`[Highway] uploadC2CVideoReq get upload invalid ukey ${ukey}, don't need upload!`);
}
const subFile = preRespData.upload.subFileInfos[0];
- if (subFile.uKey && subFile.uKey != '') {
- this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile.uKey}, need upload!`);
- const index = preRespData.upload.msgInfo.msgInfoBody[1].index;
+ if (subFile!.uKey && subFile!.uKey != '') {
+ this.logger.debug(`[Highway] uploadC2CVideoReq get upload video thumb ukey: ${subFile!.uKey}, need upload!`);
+ const index = preRespData.upload.msgInfo.msgInfoBody[1]!.index;
const md5 = Buffer.from(index.info.fileHash, 'hex');
const sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
fileUuid: index.fileUuid,
- uKey: subFile.uKey,
+ uKey: subFile!.uKey,
network: {
- ipv4S: oidbIpv4s2HighwayIpv4s(subFile.ipv4S)
+ ipv4S: oidbIpv4s2HighwayIpv4s(subFile!.ipv4S)
},
msgInfoBody: preRespData.upload.msgInfo.msgInfoBody,
blockSize: BlockSize,
@@ -334,7 +334,7 @@ export class PacketHighwayContext {
extend
);
} 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;
}
@@ -347,7 +347,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
@@ -383,7 +383,7 @@ export class PacketHighwayContext {
const ukey = preRespData.upload.uKey;
if (ukey && ukey != '') {
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 sha1 = Buffer.from(index.info.fileSha1, 'hex');
const extend = new NapProtoMsg(proto.NTV2RichMediaHighwayExt).encode({
diff --git a/src/core/packet/transformer/action/MoveGroupFile.ts b/src/core/packet/transformer/action/MoveGroupFile.ts
new file mode 100644
index 00000000..47e1ec47
--- /dev/null
+++ b/src/core/packet/transformer/action/MoveGroupFile.ts
@@ -0,0 +1,35 @@
+import * as proto from '@/core/packet/transformer/proto';
+import { NapProtoMsg } from '@napneko/nap-proto-core';
+import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
+import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
+
+class MoveGroupFile extends PacketTransformer {
+ constructor() {
+ super();
+ }
+
+ build(groupUin: number, fileUUID: string, currentParentDirectory: string, targetParentDirectory: string): OidbPacket {
+ const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
+ move: {
+ groupUin: groupUin,
+ appId: 5,
+ busId: 102,
+ fileId: fileUUID,
+ parentDirectory: currentParentDirectory,
+ targetDirectory: targetParentDirectory,
+ }
+ });
+ return OidbBase.build(0x6D6, 5, body, true, false);
+ }
+
+ parse(data: Buffer) {
+ const oidbBody = OidbBase.parse(data).body;
+ const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
+ if (res.move.retCode !== 0) {
+ throw new Error(`sendGroupFileMoveReq error: ${res.move.clientWording} (code=${res.move.retCode})`);
+ }
+ return res;
+ }
+}
+
+export default new MoveGroupFile();
diff --git a/src/core/packet/transformer/action/RenameGroupFile.ts b/src/core/packet/transformer/action/RenameGroupFile.ts
new file mode 100644
index 00000000..2cbaeacd
--- /dev/null
+++ b/src/core/packet/transformer/action/RenameGroupFile.ts
@@ -0,0 +1,34 @@
+import * as proto from '@/core/packet/transformer/proto';
+import { NapProtoMsg } from '@napneko/nap-proto-core';
+import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
+import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
+
+class RenameGroupFile extends PacketTransformer {
+ constructor() {
+ super();
+ }
+
+ build(groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string): OidbPacket {
+ const body = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6).encode({
+ rename: {
+ groupUin: groupUin,
+ busId: 102,
+ fileId: fileUUID,
+ parentFolder: currentParentDirectory,
+ newFileName: newName,
+ }
+ });
+ return OidbBase.build(0x6D6, 4, body, true, false);
+ }
+
+ parse(data: Buffer) {
+ const oidbBody = OidbBase.parse(data).body;
+ const res = new NapProtoMsg(proto.OidbSvcTrpcTcp0x6D6Response).decode(oidbBody);
+ if (res.rename.retCode !== 0) {
+ throw new Error(`sendGroupFileRenameReq error: ${res.rename.clientWording} (code=${res.rename.retCode})`);
+ }
+ return res;
+ }
+}
+
+export default new RenameGroupFile();
diff --git a/src/core/packet/transformer/action/SetSpecialTitle.ts b/src/core/packet/transformer/action/SetSpecialTitle.ts
index c7dbb2ff..3d75fb36 100644
--- a/src/core/packet/transformer/action/SetSpecialTitle.ts
+++ b/src/core/packet/transformer/action/SetSpecialTitle.ts
@@ -8,16 +8,15 @@ class SetSpecialTitle extends PacketTransformer
super();
}
- 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
- });
+ build(groupCode: number, uid: string, title: string): OidbPacket {
const oidb_0x8FC_2 = new NapProtoMsg(proto.OidbSvcTrpcTcp0X8FC_2).encode({
groupUin: +groupCode,
- body: oidb_0x8FC_2_body
+ body: {
+ targetUid: uid,
+ specialTitle: title,
+ expiredTime: -1,
+ uinName: title
+ }
});
return OidbBase.build(0x8FC, 2, oidb_0x8FC_2, false, false);
}
diff --git a/src/core/packet/transformer/action/index.ts b/src/core/packet/transformer/action/index.ts
index 7f0987d6..03af3f35 100644
--- a/src/core/packet/transformer/action/index.ts
+++ b/src/core/packet/transformer/action/index.ts
@@ -6,3 +6,5 @@ export { default as GetStrangerInfo } from './GetStrangerInfo';
export { default as SendPoke } from './SendPoke';
export { default as SetSpecialTitle } from './SetSpecialTitle';
export { default as ImageOCR } from './ImageOCR';
+export { default as MoveGroupFile } from './MoveGroupFile';
+export { default as RenameGroupFile } from './RenameGroupFile';
diff --git a/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts b/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts
index e96dd951..4f96409b 100644
--- a/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts
+++ b/src/core/packet/transformer/proto/oidb/Oidb.0x8FC_2.ts
@@ -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),
};
diff --git a/src/core/services/NodeIKernelBuddyService.ts b/src/core/services/NodeIKernelBuddyService.ts
index 69dcfb05..dd025c08 100644
--- a/src/core/services/NodeIKernelBuddyService.ts
+++ b/src/core/services/NodeIKernelBuddyService.ts
@@ -16,7 +16,7 @@ export interface NodeIKernelBuddyService {
getBuddyListFromCache(reqType: BuddyListReqType): Promise;
getDoubtBuddyUnreadNum(): number;
- approvalDoubtBuddyReq(uid: number, isAgree: boolean): void;
+ approvalDoubtBuddyReq(uid: string, str1: string, str2: string): void;
delDoubtBuddyReq(uid: number): void;
- delAllDoubtBuddyReq(): void;
+ delAllDoubtBuddyReq(): Promise;
reportDoubtBuddyReqUnread(): void;
diff --git a/src/core/services/NodeIKernelGroupService.ts b/src/core/services/NodeIKernelGroupService.ts
index b991c83b..babcad73 100644
--- a/src/core/services/NodeIKernelGroupService.ts
+++ b/src/core/services/NodeIKernelGroupService.ts
@@ -165,7 +165,7 @@ export interface NodeIKernelGroupService {
modifyGroupName(groupCode: string, groupName: string, isNormalMember: boolean): Promise;
- modifyGroupRemark(groupCode: string, remark: string): void;
+ modifyGroupRemark(groupCode: string, remark: string): Promise;
modifyGroupDetailInfo(groupCode: string, arg: unknown): void;
diff --git a/src/core/services/NodeIKernelLoginService.ts b/src/core/services/NodeIKernelLoginService.ts
index 8c90144a..c3f7602c 100644
--- a/src/core/services/NodeIKernelLoginService.ts
+++ b/src/core/services/NodeIKernelLoginService.ts
@@ -60,7 +60,10 @@ export interface QuickLoginResult {
}
export interface NodeIKernelLoginService {
+ getMsfStatus: () => number;
+
setLoginMiscData(arg0: string, value: string): unknown;
+
getMachineGuid(): string;
get(): NodeIKernelLoginService;
diff --git a/src/core/services/NodeIKernelMsgService.ts b/src/core/services/NodeIKernelMsgService.ts
index 6eb1f5b0..abd6c969 100644
--- a/src/core/services/NodeIKernelMsgService.ts
+++ b/src/core/services/NodeIKernelMsgService.ts
@@ -425,7 +425,20 @@ export interface NodeIKernelMsgService {
switchToOfflineGetRichMediaElement(...args: unknown[]): unknown;
- downloadRichMedia(...args: unknown[]): unknown;
+ downloadRichMedia(args: {
+ fileModelId: string,
+ downSourceType: number,
+ triggerType: number,
+ msgId: string,
+ chatType: number,
+ peerUid: string,
+ elementId: string,
+ thumbSize: number,
+ downloadType: number,
+ filePath: string
+ } & {
+ downloadSourceType: number, //33800左右一下的老版本 新版34606已经完全上面格式
+ }): unknown;
getFirstUnreadMsgSeq(args: {
peerUid: string
@@ -465,14 +478,14 @@ export interface NodeIKernelMsgService {
setMsgEmojiLikesForRole(...args: unknown[]): unknown;
clickInlineKeyboardButton(params: {
- guildId: string,
+ guildId?: string,
peerId: string,
botAppid: string,
msgSeq: string,
buttonId: string,
callback_data: string,
dmFlag: number,
- chatType: number
+ chatType: number // 1私聊 2群
}): Promise;
setCurOnScreenMsg(...args: unknown[]): unknown;
diff --git a/src/core/services/NodeIKernelProfileService.ts b/src/core/services/NodeIKernelProfileService.ts
index 3a017050..8b07773a 100644
--- a/src/core/services/NodeIKernelProfileService.ts
+++ b/src/core/services/NodeIKernelProfileService.ts
@@ -1,5 +1,5 @@
import { AnyCnameRecord } from 'node:dns';
-import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailSource } from '@/core';
+import { BizKey, ModifyProfileParams, NodeIKernelProfileListener, ProfileBizType, SimpleInfo, UserDetailInfoByUin, UserDetailInfoListenerArg, UserDetailSource } from '@/core';
import { GeneralCallResult } from '@/core/services/common';
export interface NodeIKernelProfileService {
@@ -15,7 +15,13 @@ export interface NodeIKernelProfileService {
getCoreAndBaseInfo(callfrom: string, uids: string[]): Promise>;
- fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise;
+ fetchUserDetailInfo(trace: string, uids: string[], source: UserDetailSource, bizType: ProfileBizType[]): Promise detail
+ detail: Map,
+ }
+ >;
addKernelProfileListener(listener: NodeIKernelProfileListener): number;
diff --git a/src/core/services/NodeIKernelRichMediaService.ts b/src/core/services/NodeIKernelRichMediaService.ts
index 83ebeaf8..85c267f3 100644
--- a/src/core/services/NodeIKernelRichMediaService.ts
+++ b/src/core/services/NodeIKernelRichMediaService.ts
@@ -198,9 +198,29 @@ export interface NodeIKernelRichMediaService {
renameGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
- moveGroupFile(arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown, arg5: unknown): unknown;
+ moveGroupFile(groupCode: string, busId: Array, fileList: Array, currentParentDirectory: string, targetParentDirectory: string): Promise,
+ failFileIdList: Array
+ }
+ }>;
- transGroupFile(arg1: unknown, arg2: unknown): unknown;
+ transGroupFile(groupCode: string, fileId: string): Promise;
searchGroupFile(
keywords: Array,
diff --git a/src/core/services/NodeIKernelSearchService.ts b/src/core/services/NodeIKernelSearchService.ts
index 56eeb992..7eda4f8a 100644
--- a/src/core/services/NodeIKernelSearchService.ts
+++ b/src/core/services/NodeIKernelSearchService.ts
@@ -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;
searchMoreMsgWithKeywords(...args: unknown[]): unknown;// needs 1 arguments
diff --git a/src/core/types/user.ts b/src/core/types/user.ts
index f6393ccd..921c709a 100644
--- a/src/core/types/user.ts
+++ b/src/core/types/user.ts
@@ -207,6 +207,7 @@ interface PhotoWall {
// 简单信息
export interface SimpleInfo {
+ qqLevel?: QQLevel;//临时添加
uid?: string;
uin?: string;
coreInfo: CoreInfo;
diff --git a/src/core/types/webapi.ts b/src/core/types/webapi.ts
index c689d860..c9771cba 100644
--- a/src/core/types/webapi.ts
+++ b/src/core/types/webapi.ts
@@ -115,7 +115,7 @@ export interface GroupEssenceMsg {
add_digest_uin: string;
add_digest_nick: string;
add_digest_time: number;
- msg_content: unknown[];
+ msg_content: { msg_type: number, text?: string, image_url?: string }[];
can_be_removed: true;
}
diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts
index b1451dc2..8a8282ee 100644
--- a/src/framework/napcat.ts
+++ b/src/framework/napcat.ts
@@ -7,13 +7,15 @@ 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';
+import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
+import { FFmpegService } from '@/common/ffmpeg';
//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(
@@ -36,6 +38,15 @@ export async function NCoreInitFramework(
const logger = new LogWrapper(pathWrapper.logsPath);
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
+ if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
+ downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
+ if (reset && path) {
+ FFmpegService.setFfmpegPath(path, logger);
+ }
+ }).catch(e => {
+ logger.logError('[Ffmpeg] Error:', e);
+ });
+ }
//直到登录成功后,执行下一步
const selfInfo = await new Promise((resolveSelfInfo) => {
const loginListener = new NodeIKernelLoginListener();
diff --git a/src/native/packet/MoeHoo.linux.arm64.node b/src/native/packet/MoeHoo.linux.arm64.node
index 99c45b31..4f248425 100644
Binary files a/src/native/packet/MoeHoo.linux.arm64.node and b/src/native/packet/MoeHoo.linux.arm64.node differ
diff --git a/src/native/packet/MoeHoo.linux.x64.node b/src/native/packet/MoeHoo.linux.x64.node
index ac4c5b9d..98287ac2 100644
Binary files a/src/native/packet/MoeHoo.linux.x64.node and b/src/native/packet/MoeHoo.linux.x64.node differ
diff --git a/src/native/packet/MoeHoo.win32.x64.node b/src/native/packet/MoeHoo.win32.x64.node
index ab1701c0..0c5db04e 100644
Binary files a/src/native/packet/MoeHoo.win32.x64.node and b/src/native/packet/MoeHoo.win32.x64.node differ
diff --git a/src/onebot/action/OneBotAction.ts b/src/onebot/action/OneBotAction.ts
index e4ec6ebf..818169e2 100644
--- a/src/onebot/action/OneBotAction.ts
+++ b/src/onebot/action/OneBotAction.ts
@@ -3,6 +3,7 @@ import Ajv, { ErrorObject, ValidateFunction } from 'ajv';
import { NapCatCore } from '@/core';
import { NapCatOneBot11Adapter, OB11Return } from '@/onebot';
import { NetworkAdapterConfig } from '../config/config';
+import { TSchema } from '@sinclair/typebox';
export class OB11Response {
private static createResponse(data: T, status: string, retcode: number, message: string = '', echo: unknown = null): OB11Return {
@@ -33,7 +34,7 @@ export abstract class OneBotAction {
actionName: typeof ActionName[keyof typeof ActionName] = ActionName.Unknown;
core: NapCatCore;
private validate?: ValidateFunction = undefined;
- payloadSchema?: unknown = undefined;
+ payloadSchema?: TSchema = undefined;
obContext: NapCatOneBot11Adapter;
constructor(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
@@ -43,7 +44,7 @@ export abstract class OneBotAction {
protected async check(payload: PayloadType): Promise {
if (this.payloadSchema) {
- this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true }).compile(this.payloadSchema);
+ this.validate = new Ajv({ allowUnionTypes: true, useDefaults: true, coerceTypes: true }).compile(this.payloadSchema);
}
if (this.validate && !this.validate(payload)) {
const errors = this.validate.errors as ErrorObject[];
diff --git a/src/onebot/action/extends/ClickInlineKeyboardButton.ts b/src/onebot/action/extends/ClickInlineKeyboardButton.ts
index 124886ce..f27acfb2 100644
--- a/src/onebot/action/extends/ClickInlineKeyboardButton.ts
+++ b/src/onebot/action/extends/ClickInlineKeyboardButton.ts
@@ -7,6 +7,7 @@ const SchemaData = Type.Object({
bot_appid: Type.String(),
button_id: Type.String({ default: '' }),
callback_data: Type.String({ default: '' }),
+ msg_seq: Type.String({ default: '10086' }),
});
type Payload = Static;
@@ -18,13 +19,12 @@ export class ClickInlineKeyboardButton extends OneBotAction {
async _handle(payload: Payload) {
return await this.core.apis.MsgApi.clickInlineKeyboardButton({
buttonId: payload.button_id,
- guildId: '',// 频道使用
peerId: payload.group_id.toString(),
botAppid: payload.bot_appid,
- msgSeq: '10086',
+ msgSeq: payload.msg_seq,
callback_data: payload.callback_data,
dmFlag: 0,
- chatType: 1
+ chatType: 2
})
}
}
diff --git a/src/onebot/action/extends/GetUnidirectionalFriendList.ts b/src/onebot/action/extends/GetUnidirectionalFriendList.ts
new file mode 100644
index 00000000..96de0f7d
--- /dev/null
+++ b/src/onebot/action/extends/GetUnidirectionalFriendList.ts
@@ -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 {
+ override actionName = ActionName.GetUnidirectionalFriendList;
+
+ async pack_data(data: string): Promise {
+ return ProtoBuf(class extends ProtoBufBase {
+ type = PBUint32(2, false, 0);
+ data = PBString(3, false, data);
+ }).encode();
+ }
+
+ async _handle(): Promise {
+ 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()
+ }));
+ }
+}
\ No newline at end of file
diff --git a/src/onebot/action/extends/MoveGroupFile.ts b/src/onebot/action/extends/MoveGroupFile.ts
new file mode 100644
index 00000000..110551bc
--- /dev/null
+++ b/src/onebot/action/extends/MoveGroupFile.ts
@@ -0,0 +1,33 @@
+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({
+ group_id: Type.Union([Type.Number(), Type.String()]),
+ file_id: Type.String(),
+ current_parent_directory: Type.String(),
+ target_parent_directory: Type.String(),
+});
+
+type Payload = Static;
+
+interface MoveGroupFileResponse {
+ ok: boolean;
+}
+
+export class MoveGroupFile extends GetPacketStatusDepends {
+ override actionName = ActionName.MoveGroupFile;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload) {
+ const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
+ if (contextMsgFile?.fileUUID) {
+ await this.core.apis.PacketApi.pkt.operation.MoveGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.target_parent_directory);
+ return {
+ ok: true,
+ };
+ }
+ throw new Error('real fileUUID not found!');
+ }
+}
diff --git a/src/onebot/action/extends/RenameGroupFile.ts b/src/onebot/action/extends/RenameGroupFile.ts
new file mode 100644
index 00000000..a567ea55
--- /dev/null
+++ b/src/onebot/action/extends/RenameGroupFile.ts
@@ -0,0 +1,33 @@
+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({
+ group_id: Type.Union([Type.Number(), Type.String()]),
+ file_id: Type.String(),
+ current_parent_directory: Type.String(),
+ new_name: Type.String(),
+});
+
+type Payload = Static;
+
+interface RenameGroupFileResponse {
+ ok: boolean;
+}
+
+export class RenameGroupFile extends GetPacketStatusDepends {
+ override actionName = ActionName.RenameGroupFile;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload) {
+ const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
+ if (contextMsgFile?.fileUUID) {
+ await this.core.apis.PacketApi.pkt.operation.RenameGroupFile(+payload.group_id, contextMsgFile.fileUUID, payload.current_parent_directory, payload.new_name);
+ return {
+ ok: true,
+ };
+ }
+ throw new Error('real fileUUID not found!');
+ }
+}
diff --git a/src/onebot/action/extends/SetGroupRemark.ts b/src/onebot/action/extends/SetGroupRemark.ts
new file mode 100644
index 00000000..a8dbf5a9
--- /dev/null
+++ b/src/onebot/action/extends/SetGroupRemark.ts
@@ -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;
+
+export default class SetGroupRemark extends OneBotAction {
+ override actionName = ActionName.SetGroupRemark;
+ override payloadSchema = SchemaData;
+ async _handle(payload: Payload): Promise {
+ 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;
+ }
+}
diff --git a/src/onebot/action/extends/SetSpecialTittle.ts b/src/onebot/action/extends/SetSpecialTitle.ts
similarity index 74%
rename from src/onebot/action/extends/SetSpecialTittle.ts
rename to src/onebot/action/extends/SetSpecialTitle.ts
index c512c9bf..7d68ff36 100644
--- a/src/onebot/action/extends/SetSpecialTittle.ts
+++ b/src/onebot/action/extends/SetSpecialTitle.ts
@@ -5,18 +5,18 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
group_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;
-export class SetSpecialTittle extends GetPacketStatusDepends {
- override actionName = ActionName.SetSpecialTittle;
+export class SetSpecialTitle extends GetPacketStatusDepends {
+ override actionName = ActionName.SetSpecialTitle;
override payloadSchema = SchemaData;
async _handle(payload: Payload) {
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);
}
}
diff --git a/src/onebot/action/extends/TransGroupFile.ts b/src/onebot/action/extends/TransGroupFile.ts
new file mode 100644
index 00000000..35b3275a
--- /dev/null
+++ b/src/onebot/action/extends/TransGroupFile.ts
@@ -0,0 +1,34 @@
+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({
+ group_id: Type.Union([Type.Number(), Type.String()]),
+ file_id: Type.String(),
+});
+
+type Payload = Static;
+
+interface TransGroupFileResponse {
+ ok: boolean;
+}
+
+export class TransGroupFile extends GetPacketStatusDepends {
+ override actionName = ActionName.TransGroupFile;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload) {
+ const contextMsgFile = FileNapCatOneBotUUID.decode(payload.file_id) || FileNapCatOneBotUUID.decodeModelId(payload.file_id);
+ if (contextMsgFile?.fileUUID) {
+ const result = await this.core.apis.GroupApi.transGroupFile(payload.group_id.toString(), contextMsgFile.fileUUID);
+ if (result.transGroupFileResult.result.retCode === 0) {
+ return {
+ ok: true
+ };
+ }
+ throw new Error(result.transGroupFileResult.result.retMsg);
+ }
+ throw new Error('real fileUUID not found!');
+ }
+}
diff --git a/src/onebot/action/file/GetPrivateFileUrl.ts b/src/onebot/action/file/GetPrivateFileUrl.ts
new file mode 100644
index 00000000..f592cc08
--- /dev/null
+++ b/src/onebot/action/file/GetPrivateFileUrl.ts
@@ -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;
+
+interface GetPrivateFileUrlResponse {
+ url?: string;
+}
+
+export class GetPrivateFileUrl extends GetPacketStatusDepends {
+ 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!');
+ }
+}
diff --git a/src/onebot/action/go-cqhttp/GetForwardMsg.ts b/src/onebot/action/go-cqhttp/GetForwardMsg.ts
index 5552229b..da7f960f 100644
--- a/src/onebot/action/go-cqhttp/GetForwardMsg.ts
+++ b/src/onebot/action/go-cqhttp/GetForwardMsg.ts
@@ -4,14 +4,14 @@ import { ActionName } from '@/onebot/action/router';
import { MessageUnique } from '@/common/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { ChatType, ElementType, MsgSourceType, NTMsgType, RawMessage } from '@/core';
+import { isNumeric } from '@/common/helper';
const SchemaData = Type.Object({
- message_id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
- id: Type.Optional(Type.Union([Type.Number(), Type.String()])),
+ message_id: Type.Optional(Type.String()),
+ id: Type.Optional(Type.String()),
});
type Payload = Static;
-
export class GoCQHTTPGetForwardMsgAction extends OneBotAction {
@@ -53,19 +53,21 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction {
+ // 2. 定义辅助函数 - 创建伪转发消息对象
+ const createFakeForwardMsg = (resId: string): RawMessage => {
return {
chatType: ChatType.KCHATTYPEGROUP,
elements: [{
elementType: ElementType.MULTIFORWARD,
elementId: '',
multiForwardMsgElement: {
- resId: res_id,
+ resId: resId,
fileName: '',
xmlContent: '',
}
@@ -96,8 +98,9 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction {
- const ob = (await this.obContext.apis.MsgApi.parseMessageV2(fakeForwardMsg(res_id)))?.arrayMsg;
+ // 3. 定义协议回退逻辑函数
+ const protocolFallbackLogic = async (resId: string) => {
+ const ob = (await this.obContext.apis.MsgApi.parseMessageV2(createFakeForwardMsg(resId)))?.arrayMsg;
if (ob) {
return {
messages: (ob?.message?.[0] as OB11MessageForward)?.data?.content
@@ -105,31 +108,37 @@ export class GoCQHTTPGetForwardMsgAction extends OneBotAction 0) {
+ const singleMsg = data.msgList[0];
+ if (!singleMsg) {
+ throw new Error('消息不存在或已过期');
+ }
+ // 6. 解析消息内容
+ const resMsg = (await this.obContext.apis.MsgApi.parseMessageV2(singleMsg))?.arrayMsg;
- // return { message: resMsg };
+ const forwardContent = (resMsg?.message?.[0] as OB11MessageForward)?.data?.content;
+ if (forwardContent) {
+ return { messages: forwardContent };
+ }
+ }
+ }
+ // 说明消息已过期或者为内层消息 NapCat 一次返回不处理内层消息
+ throw new Error('消息已过期或者为内层消息,无法获取转发消息');
}
}
diff --git a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts
index ef3db8d0..28e32c4e 100644
--- a/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts
+++ b/src/onebot/action/go-cqhttp/GetFriendMsgHistory.ts
@@ -11,10 +11,10 @@ interface Response {
messages: OB11Message[];
}
const SchemaData = Type.Object({
- user_id: Type.Union([Type.Number(), Type.String()]),
- message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
- count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
- reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
+ user_id: Type.String(),
+ message_seq: Type.Optional(Type.String()),
+ count: Type.Number({ default: 20 }),
+ reverseOrder: Type.Boolean({ default: false })
});
@@ -27,18 +27,14 @@ export default class GetFriendMsgHistory extends OneBotAction
async _handle(payload: Payload, _adapter: string, config: NetworkAdapterConfig): Promise {
//处理参数
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
-
- const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
if (!uid) throw new Error(`记录${payload.user_id}不存在`);
const friend = await this.core.apis.FriendApi.isBuddy(uid);
const peer = { chatType: friend ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid: uid };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
- (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
+ (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverseOrder)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
- //翻转消息
- if (isReverseOrder) msgList.reverse();
//转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
diff --git a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts
index e2fdf4e8..5dcbedb1 100644
--- a/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts
+++ b/src/onebot/action/go-cqhttp/GetGroupMsgHistory.ts
@@ -11,10 +11,10 @@ interface Response {
}
const SchemaData = Type.Object({
- group_id: Type.Union([Type.Number(), Type.String()]),
- message_seq: Type.Optional(Type.Union([Type.Number(), Type.String()])),
- count: Type.Union([Type.Number(), Type.String()], { default: 20 }),
- reverseOrder: Type.Optional(Type.Union([Type.Boolean(), Type.String()]))
+ group_id: Type.String(),
+ message_seq: Type.Optional(Type.String()),
+ count: Type.Number({ default: 20 }),
+ reverseOrder: Type.Boolean({ default: false })
});
@@ -26,17 +26,13 @@ export default class GoCQHTTPGetGroupMsgHistory extends OneBotAction {
- //处理参数
- const isReverseOrder = typeof payload.reverseOrder === 'string' ? payload.reverseOrder === 'true' : !!payload.reverseOrder;
const peer: Peer = { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
const hasMessageSeq = !payload.message_seq ? !!payload.message_seq : !(payload.message_seq?.toString() === '' || payload.message_seq?.toString() === '0');
//拉取消息
const startMsgId = hasMessageSeq ? (MessageUnique.getMsgIdAndPeerByShortId(+payload.message_seq!)?.MsgId ?? payload.message_seq!.toString()) : '0';
const msgList = hasMessageSeq ?
- (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
+ (await this.core.apis.MsgApi.getMsgHistory(peer, startMsgId, +payload.count, payload.reverseOrder)).msgList : (await this.core.apis.MsgApi.getAioFirstViewLatestMsgs(peer, +payload.count)).msgList;
if (msgList.length === 0) throw new Error(`消息${payload.message_seq}不存在`);
- //翻转消息
- if (isReverseOrder) msgList.reverse();
//转换序号
await Promise.all(msgList.map(async msg => {
msg.id = MessageUnique.createUniqueMsgId({ guildId: '', chatType: msg.chatType, peerUid: msg.peerUid }, msg.msgId);
diff --git a/src/onebot/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts
index c8d53b0a..796f67f3 100644
--- a/src/onebot/action/go-cqhttp/GetStrangerInfo.ts
+++ b/src/onebot/action/go-cqhttp/GetStrangerInfo.ts
@@ -7,6 +7,7 @@ import { Static, Type } from '@sinclair/typebox';
const SchemaData = Type.Object({
user_id: Type.Union([Type.Number(), Type.String()]),
+ no_cache: Type.Union([Type.Boolean(), Type.String()], { default: false }),
});
type Payload = Static;
@@ -16,10 +17,11 @@ export default class GoCQHTTPGetStrangerInfo extends OneBotAction;
diff --git a/src/onebot/action/go-cqhttp/UploadGroupFile.ts b/src/onebot/action/go-cqhttp/UploadGroupFile.ts
index 906d7e91..5c636e16 100644
--- a/src/onebot/action/go-cqhttp/UploadGroupFile.ts
+++ b/src/onebot/action/go-cqhttp/UploadGroupFile.ts
@@ -38,6 +38,7 @@ export default class GoCQHTTPUploadGroupFile extends OneBotAction
deleteAfterSentFiles: []
};
const sendFileEle = await this.core.apis.FileApi.createValidSendFileElement(msgContext, downloadResult.path, payload.name, payload.folder ?? payload.folder_id);
+ msgContext.deleteAfterSentFiles.push(downloadResult.path);
await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, [sendFileEle], msgContext.deleteAfterSentFiles);
return null;
}
diff --git a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts
index f17e3edf..1a37a21f 100644
--- a/src/onebot/action/go-cqhttp/UploadPrivateFile.ts
+++ b/src/onebot/action/go-cqhttp/UploadPrivateFile.ts
@@ -23,7 +23,7 @@ export default class GoCQHTTPUploadPrivateFile extends OneBotAction {
const data = await this.core.apis.GroupApi.fetchGroupDetail(payload.group_id.toString());
return {
...data,
+ group_all_shut: data.shutUpAllTimestamp > 0 ? -1 : 0,
+ group_remark: '',
group_id: +payload.group_id,
group_name: data.groupName,
member_count: data.memberNum,
diff --git a/src/onebot/action/group/GetGroupMemberInfo.ts b/src/onebot/action/group/GetGroupMemberInfo.ts
index 00e277c9..03938f27 100644
--- a/src/onebot/action/group/GetGroupMemberInfo.ts
+++ b/src/onebot/action/group/GetGroupMemberInfo.ts
@@ -32,7 +32,7 @@ class GetGroupMemberInfo extends OneBotAction {
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.getUserDetailInfo(uid, isNocache),
]);
if (!member || !groupMember) throw new Error(`群(${payload.group_id})成员${payload.user_id}不存在`);
diff --git a/src/onebot/action/group/SetGroupAddRequest.ts b/src/onebot/action/group/SetGroupAddRequest.ts
index 5f5f7cd2..8cd69bcd 100644
--- a/src/onebot/action/group/SetGroupAddRequest.ts
+++ b/src/onebot/action/group/SetGroupAddRequest.ts
@@ -20,11 +20,12 @@ export default class SetGroupAddRequest extends OneBotAction {
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 {
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 };
}
}
\ No newline at end of file
diff --git a/src/onebot/action/group/SetGroupBan.ts b/src/onebot/action/group/SetGroupBan.ts
index aa5cdf26..3f30aa1f 100644
--- a/src/onebot/action/group/SetGroupBan.ts
+++ b/src/onebot/action/group/SetGroupBan.ts
@@ -16,6 +16,8 @@ export default class SetGroupBan extends OneBotAction {
async _handle(payload: Payload): Promise {
const uid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
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');
// 例如无管理员权限时 result为 120101005 errMsg为 'ERR_NOT_GROUP_ADMIN'
let ret = await this.core.apis.GroupApi.banMember(payload.group_id.toString(),
[{ uid: uid, timeStamp: +payload.duration }]);
diff --git a/src/onebot/action/index.ts b/src/onebot/action/index.ts
index a9044277..37dc07f5 100644
--- a/src/onebot/action/index.ts
+++ b/src/onebot/action/index.ts
@@ -81,7 +81,7 @@ import { GetGroupSystemMsg } from './system/GetSystemMsg';
import { GroupPoke } from './group/GroupPoke';
import { GetUserStatus } from './extends/GetUserStatus';
import { GetRkey } from './extends/GetRkey';
-import { SetSpecialTittle } from './extends/SetSpecialTittle';
+import { SetSpecialTitle } from './extends/SetSpecialTitle';
import { GetGroupShutList } from './group/GetGroupShutList';
import { GetGroupMemberList } from './group/GetGroupMemberList';
import { GetGroupFileUrl } from '@/onebot/action/file/GetGroupFileUrl';
@@ -106,10 +106,28 @@ import { SendPoke } from '@/onebot/action/packet/SendPoke';
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';
+import { MoveGroupFile } from './extends/MoveGroupFile';
+import { TransGroupFile } from './extends/TransGroupFile';
+import { RenameGroupFile } from './extends/RenameGroupFile';
+import { GetRkeyServer } from './packet/GetRkeyServer';
+import { GetRkeyEx } from './packet/GetRkeyEx';
+import { CleanCache } from './system/CleanCache';
+import SetFriendRemark from './user/SetFriendRemark';
+import { SetDoubtFriendsAddRequest } from './new/SetDoubtFriendsAddRequest';
+import { GetDoubtFriendsAddRequest } from './new/GetDoubtFriendsAddRequest';
export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCore) {
const actionHandlers = [
+ new SetDoubtFriendsAddRequest(obContext, core),
+ new GetDoubtFriendsAddRequest(obContext, core),
+ new SetFriendRemark(obContext, core),
+ new GetRkeyEx(obContext, core),
+ new GetRkeyServer(obContext, core),
+ new SetGroupRemark(obContext, core),
new GetGroupInfoEx(obContext, core),
new FetchEmojiLike(obContext, core),
new GetFile(obContext, core),
@@ -128,6 +146,9 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new SetGroupSign(obContext, core),
new SendGroupSign(obContext, core),
new GetClientkey(obContext, core),
+ new MoveGroupFile(obContext, core),
+ new RenameGroupFile(obContext, core),
+ new TransGroupFile(obContext, core),
// onebot11
new SendLike(obContext, core),
new GetMsg(obContext, core),
@@ -211,7 +232,7 @@ export function createActionMap(obContext: NapCatOneBot11Adapter, core: NapCatCo
new FriendPoke(obContext, core),
new GetUserStatus(obContext, core),
new GetRkey(obContext, core),
- new SetSpecialTittle(obContext, core),
+ new SetSpecialTitle(obContext, core),
new SetDiyOnlineStatus(obContext, core),
// new UploadForwardMsg(obContext, core),
new GetGroupShutList(obContext, core),
@@ -225,6 +246,9 @@ 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 CleanCache(obContext, core),
];
type HandlerUnion = typeof actionHandlers[number];
diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts
index 26a93ad2..bb17807c 100644
--- a/src/onebot/action/msg/SendMsg.ts
+++ b/src/onebot/action/msg/SendMsg.ts
@@ -38,7 +38,7 @@ export function normalize(message: OB11MessageMixType, autoEscape = false): OB11
export async function createContext(core: NapCatCore, payload: OB11PostContext | undefined, contextMode: ContextMode = ContextMode.Normal): Promise {
if (!payload) {
- throw new Error('请指定 group_id 或 user_id');
+ throw new Error('请传递请求内容');
}
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
@@ -48,7 +48,16 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
- if (!Uid) throw new Error('无法获取用户信息');
+ if (!Uid) {
+ if (payload.group_id) {
+ return {
+ chatType: ChatType.KCHATTYPEGROUP,
+ peerUid: payload.group_id.toString(),
+ guildId: ''
+ }
+ }
+ throw new Error('无法获取用户信息');
+ }
const isBuddy = await core.apis.FriendApi.isBuddy(Uid);
if (!isBuddy) {
const ret = await core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, Uid);
@@ -78,7 +87,13 @@ export async function createContext(core: NapCatCore, payload: OB11PostContext |
guildId: '',
};
}
- throw new Error('请指定 group_id 或 user_id');
+ if (contextMode === ContextMode.Private && payload.group_id) {
+ throw new Error('当前私聊发送,请指定 user_id 而不是 group_id');
+ }
+ if (contextMode === ContextMode.Group && payload.user_id) {
+ throw new Error('当前群聊发送,请指定 group_id 而不是 user_id');
+ }
+ throw new Error('请指定正确的 group_id 或 user_id');
}
function getSpecialMsgNum(payload: OB11PostSendMsg, msgType: OB11MessageDataType): number {
diff --git a/src/onebot/action/new/GetDoubtFriendsAddRequest.ts b/src/onebot/action/new/GetDoubtFriendsAddRequest.ts
new file mode 100644
index 00000000..7b8ae921
--- /dev/null
+++ b/src/onebot/action/new/GetDoubtFriendsAddRequest.ts
@@ -0,0 +1,18 @@
+import { OneBotAction } from '@/onebot/action/OneBotAction';
+import { ActionName } from '@/onebot/action/router';
+import { Static, Type } from '@sinclair/typebox';
+
+const SchemaData = Type.Object({
+ count: Type.Number({ default: 50 }),
+});
+
+type Payload = Static;
+
+export class GetDoubtFriendsAddRequest extends OneBotAction {
+ override actionName = ActionName.GetDoubtFriendsAddRequest;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload) {
+ return await this.core.apis.FriendApi.getDoubtFriendRequest(payload.count);
+ }
+}
diff --git a/src/onebot/action/new/SetDoubtFriendsAddRequest.ts b/src/onebot/action/new/SetDoubtFriendsAddRequest.ts
new file mode 100644
index 00000000..990d5607
--- /dev/null
+++ b/src/onebot/action/new/SetDoubtFriendsAddRequest.ts
@@ -0,0 +1,21 @@
+import { OneBotAction } from '@/onebot/action/OneBotAction';
+import { ActionName } from '@/onebot/action/router';
+import { Static, Type } from '@sinclair/typebox';
+
+const SchemaData = Type.Object({
+ flag: Type.String(),
+ //注意强制String 非isNumeric 不遵守则不符合设计
+ approve: Type.Boolean({ default: true }),
+ //该字段没有语义 仅做保留 强制为True
+});
+
+type Payload = Static;
+
+export class SetDoubtFriendsAddRequest extends OneBotAction {
+ override actionName = ActionName.SetDoubtFriendsAddRequest;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload) {
+ return await this.core.apis.FriendApi.handleDoubtFriendRequest(payload.flag);
+ }
+}
diff --git a/src/onebot/action/packet/GetRkeyEx.ts b/src/onebot/action/packet/GetRkeyEx.ts
new file mode 100644
index 00000000..d330b8ee
--- /dev/null
+++ b/src/onebot/action/packet/GetRkeyEx.ts
@@ -0,0 +1,18 @@
+import { ActionName } from '@/onebot/action/router';
+import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
+
+export class GetRkeyEx extends GetPacketStatusDepends {
+ override actionName = ActionName.GetRkeyEx;
+
+ async _handle() {
+ let rkeys = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
+ return rkeys.map(rkey => {
+ return {
+ type: rkey.type === 10 ? "private" : "group",
+ rkey: rkey.rkey,
+ created_at: rkey.time,
+ ttl: rkey.ttl,
+ };
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/onebot/action/packet/GetRkeyServer.ts b/src/onebot/action/packet/GetRkeyServer.ts
new file mode 100644
index 00000000..ebfa7049
--- /dev/null
+++ b/src/onebot/action/packet/GetRkeyServer.ts
@@ -0,0 +1,38 @@
+import { ActionName } from '@/onebot/action/router';
+import { GetPacketStatusDepends } from '@/onebot/action/packet/GetPacketStatus';
+
+export class GetRkeyServer extends GetPacketStatusDepends {
+ override actionName = ActionName.GetRkeyServer;
+
+ private rkeyCache: {
+ private_rkey?: string;
+ group_rkey?: string;
+ expired_time?: number;
+ name: string;
+ } | null = null;
+ private expiryTime: number | null = null;
+
+ async _handle() {
+ // 检查缓存是否有效
+ if (this.expiryTime && this.expiryTime > Math.floor(Date.now() / 1000) && this.rkeyCache) {
+ return this.rkeyCache;
+ }
+
+ // 获取新的 Rkey
+ let rkeys = await this.core.apis.PacketApi.pkt.operation.FetchRkey();
+ let privateRkeyItem = rkeys.filter(rkey => rkey.type === 10)[0];
+ let groupRkeyItem = rkeys.filter(rkey => rkey.type === 20)[0];
+
+ this.expiryTime = Math.floor(Date.now() / 1000) + Math.min(+groupRkeyItem!.ttl.toString(),+privateRkeyItem!.ttl.toString());
+
+ // 更新缓存
+ this.rkeyCache = {
+ private_rkey: privateRkeyItem ? privateRkeyItem.rkey : undefined,
+ group_rkey: groupRkeyItem ? groupRkeyItem.rkey : undefined,
+ expired_time: this.expiryTime,
+ name: "NapCat 4"
+ };
+
+ return this.rkeyCache;
+ }
+}
\ No newline at end of file
diff --git a/src/onebot/action/router.ts b/src/onebot/action/router.ts
index 7e065de9..6abfe642 100644
--- a/src/onebot/action/router.ts
+++ b/src/onebot/action/router.ts
@@ -10,6 +10,14 @@ export interface InvalidCheckResult {
}
export const ActionName = {
+ // new extends 完全差异OneBot类别
+ GetDoubtFriendsAddRequest: 'get_doubt_friends_add_request',
+ SetDoubtFriendsAddRequest: 'set_doubt_friends_add_request',
+ // napcat
+ GetRkeyEx: 'get_rkey',
+ GetRkeyServer: 'get_rkey_server',
+ SetGroupRemark: 'set_group_remark',
+ NapCat_GetPrivateFileUrl: 'get_private_file_url',
ClickInlineKeyboardButton: 'click_inline_keyboard_button',
GetUnidirectionalFriendList: 'get_unidirectional_friend_list',
// onebot 11
@@ -29,8 +37,9 @@ export const ActionName = {
SetGroupCard: 'set_group_card',
SetGroupName: 'set_group_name',
SetGroupLeave: 'set_group_leave',
- SetSpecialTittle: 'set_group_special_title',
+ SetSpecialTitle: 'set_group_special_title',
SetFriendAddRequest: 'set_friend_add_request',
+ SetFriendRemark: 'set_friend_remark',
SetGroupAddRequest: 'set_group_add_request',
GetLoginInfo: 'get_login_info',
GoCQHTTP_GetStrangerInfo: 'get_stranger_info',
@@ -50,7 +59,7 @@ export const ActionName = {
GetStatus: 'get_status',
GetVersionInfo: 'get_version_info',
// Reboot : 'set_restart',
- // CleanCache : 'clean_cache',
+ CleanCache : 'clean_cache',
Exit: 'bot_exit',
// go-cqhttp
SetQQProfile: 'set_qq_profile',
@@ -128,6 +137,10 @@ export const ActionName = {
GetRkey: 'nc_get_rkey',
GetGroupShutList: 'get_group_shut_list',
+ MoveGroupFile: 'move_group_file',
+ TransGroupFile: 'trans_group_file',
+ RenameGroupFile: 'rename_group_file',
+
GetGuildList: 'get_guild_list',
GetGuildProfile: 'get_guild_service_profile',
diff --git a/src/onebot/action/system/CleanCache.ts b/src/onebot/action/system/CleanCache.ts
new file mode 100644
index 00000000..d583c736
--- /dev/null
+++ b/src/onebot/action/system/CleanCache.ts
@@ -0,0 +1,38 @@
+import { OneBotAction } from '@/onebot/action/OneBotAction';
+import { ActionName } from '@/onebot/action/router';
+import { unlink, readdir } from 'fs/promises';
+import { join } from 'path';
+
+export class CleanCache extends OneBotAction {
+ override actionName = ActionName.CleanCache;
+
+ async _handle() {
+ try {
+ // 获取临时文件夹路径
+ const tempPath = this.core.NapCatTempPath;
+
+ // 读取文件夹中的所有文件
+ const files = await readdir(tempPath);
+
+ // 删除每个文件
+ const deletePromises = files.map(async (file) => {
+ const filePath = join(tempPath, file);
+ try {
+ await unlink(filePath);
+ this.core.context.logger.log(`已删除文件: ${filePath}`);
+ } catch (err: unknown) {
+ this.core.context.logger.log(`删除文件 ${filePath} 失败: ${(err as Error).message}`);
+
+ }
+ });
+
+ // 等待所有删除操作完成
+ await Promise.all(deletePromises);
+
+ this.core.context.logger.log(`临时文件夹清理完成: ${tempPath}`);
+ } catch (err: unknown) {
+ this.core.context.logger.log(`清理缓存失败: ${(err as Error).message}`);
+ throw err;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/onebot/action/user/GetFriendList.ts b/src/onebot/action/user/GetFriendList.ts
index 263c189d..6850f0f6 100644
--- a/src/onebot/action/user/GetFriendList.ts
+++ b/src/onebot/action/user/GetFriendList.ts
@@ -14,8 +14,22 @@ export default class GetFriendList extends OneBotAction {
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) {
+ const buddyMap = await this.core.apis.FriendApi.getBuddyV2SimpleInfoMap();
+ const isNocache = typeof _payload.no_cache === 'string' ? _payload.no_cache === 'true' : !!_payload.no_cache;
+ await Promise.all(
+ Array.from(buddyMap.values()).map(async (buddyInfo) => {
+ try {
+ const userDetail = await this.core.apis.UserApi.getUserDetailInfo(buddyInfo.coreInfo.uid, isNocache);
+ const data = buddyMap.get(buddyInfo.coreInfo.uid);
+ if (data) {
+ data.qqLevel = userDetail.qqLevel;
+ }
+ } catch (error) {
+ this.core.context.logger.logError('获取好友详细信息失败', error);
+ }
+ })
+ );
+ return OB11Construct.friends(Array.from(buddyMap.values()));
}
-}
+}
\ No newline at end of file
diff --git a/src/onebot/action/user/SetFriendRemark.ts b/src/onebot/action/user/SetFriendRemark.ts
new file mode 100644
index 00000000..5cc3559c
--- /dev/null
+++ b/src/onebot/action/user/SetFriendRemark.ts
@@ -0,0 +1,25 @@
+import { OneBotAction } from '@/onebot/action/OneBotAction';
+import { ActionName } from '@/onebot/action/router';
+import { Static, Type } from '@sinclair/typebox';
+
+const SchemaData = Type.Object({
+ user_id: Type.String(),
+ remark: Type.String()
+});
+
+type Payload = Static;
+
+export default class SetFriendRemark extends OneBotAction {
+ override actionName = ActionName.SetFriendRemark;
+ override payloadSchema = SchemaData;
+
+ async _handle(payload: Payload): Promise {
+ let friendUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id);
+ let is_friend = await this.core.apis.FriendApi.isBuddy(friendUid);
+ if (!is_friend) {
+ throw new Error(`用户 ${payload.user_id} 不是好友`);
+ }
+ await this.core.apis.FriendApi.setBuddyRemark(friendUid, payload.remark);
+ return null;
+ }
+}
diff --git a/src/onebot/api/group.ts b/src/onebot/api/group.ts
index 0018e05f..0badb5f1 100644
--- a/src/onebot/api/group.ts
+++ b/src/onebot/api/group.ts
@@ -49,6 +49,7 @@ export class OneBotGroupApi {
duration = -1;
}
}
+ await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
if (memberUin && adminUin) {
return new OB11GroupBanEvent(
@@ -119,13 +120,15 @@ export class OneBotGroupApi {
member.cardName = newCardName;
return event;
}
+ if (member && member.nick !== msg.sendNickName) {
+ await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
+ }
}
return undefined;
}
async parsePaiYiPai(msg: RawMessage, jsonStr: string) {
const json = JSON.parse(jsonStr);
-
//判断业务类型
//Poke事件
const pokedetail: Array<{ uid: string }> = json.items;
@@ -146,14 +149,15 @@ export class OneBotGroupApi {
async parseOtherJsonEvent(msg: RawMessage, jsonStr: string, context: InstanceContext) {
const json = JSON.parse(jsonStr);
const type = json.items[json.items.length - 1]?.txt;
+ await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
if (type === '头衔') {
const memberUin = json.items[1].param[0];
const title = json.items[3].txt;
context.logger.logDebug('收到群成员新头衔消息', json);
return new OB11GroupTitleEvent(
this.core,
- parseInt(msg.peerUid),
- parseInt(memberUin),
+ +msg.peerUid,
+ +memberUin,
title,
);
} else if (type === '移出') {
@@ -246,7 +250,34 @@ export class OneBotGroupApi {
'invite'
);
}
-
+ async parse51TypeEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
+ // 神经腾讯 没了妈妈想出来的
+ // Warn 下面存在高并发危险
+ if (grayTipElement.jsonGrayTipElement.jsonStr) {
+ const json: {
+ align: string,
+ items: Array<{ txt: string, type: string }>
+ } = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
+ if (json.items.length === 1 && json.items[0]?.txt.endsWith('加入群')) {
+ let old_members = structuredClone(this.core.apis.GroupApi.groupMemberCache.get(msg.peerUid));
+ if (!old_members) return;
+ let new_members_map = await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
+ if (!new_members_map) return;
+ let new_members = Array.from(new_members_map.values());
+ // 对比members查找新成员
+ let new_member = new_members.find((member) => old_members.get(member.uid) == undefined);
+ if (!new_member) return;
+ return new OB11GroupIncreaseEvent(
+ this.core,
+ +msg.peerUid,
+ +new_member.uin,
+ 0,
+ 'invite',
+ );
+ }
+ }
+ return;
+ }
async parseGrayTipElement(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
// 解析群组事件 由sysmsg解析
@@ -278,6 +309,9 @@ export class OneBotGroupApi {
return await this.parsePaiYiPai(msg, grayTipElement.jsonGrayTipElement.jsonStr);
} else if (grayTipElement.jsonGrayTipElement.busiId == JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP) {
return await this.parseEssenceMsg(msg, grayTipElement.jsonGrayTipElement.jsonStr);
+ } else if (+(grayTipElement.jsonGrayTipElement.busiId ?? 0) == 51) {
+ // 51是什么?{"align":"center","items":[{"txt":"下一秒起床通过王者荣耀加入群","type":"nor"}]
+ return await this.parse51TypeEvent(msg, grayTipElement);
} else {
return await this.parseOtherJsonEvent(msg, grayTipElement.jsonGrayTipElement.jsonStr, this.core.context);
}
diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts
index 4f4a2e7b..fd81f461 100644
--- a/src/onebot/api/msg.ts
+++ b/src/onebot/api/msg.ts
@@ -34,7 +34,7 @@ import { EventType } from '@/onebot/event/OneBotEvent';
import { encodeCQCode } from '@/onebot/helper/cqcode';
import { uriToLocalFile } from '@/common/file';
import { RequestUtil } from '@/common/request';
-import fsPromise, { constants } from 'node:fs/promises';
+import fsPromise from 'node:fs/promises';
import { OB11FriendAddNoticeEvent } from '@/onebot/event/notice/OB11FriendAddNoticeEvent';
import { ForwardMsgBuilder } from '@/common/forward-msg-builder';
import { NapProtoMsg } from '@napneko/nap-proto-core';
@@ -45,6 +45,7 @@ import { OB11GroupAdminNoticeEvent } from '../event/notice/OB11GroupAdminNoticeE
import { GroupChange, GroupChangeInfo, GroupInvite, PushMsgBody } from '@/core/packet/transformer/proto';
import { OB11GroupRequestEvent } from '../event/request/OB11GroupRequest';
import { LRUCache } from '@/common/lru-cache';
+import { cleanTaskQueue } from '@/common/clean-task';
type RawToOb11Converters = {
[Key in keyof MessageElement as Key extends `${string}Element` ? Key : never]: (
@@ -132,7 +133,6 @@ export class OneBotMsgApi {
file: element.fileName,
sub_type: element.picSubType,
url: await this.core.apis.FileApi.getImageUrl(element),
- path: element.filePath,
file_size: element.fileSize,
},
};
@@ -148,13 +148,13 @@ export class OneBotMsgApi {
peerUid: msg.peerUid,
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 {
type: OB11MessageDataType.file,
data: {
- file: file,
- path: element.filePath,
- file_id: file,
+ file: element.fileName,
+ file_id: element.fileUuid,
file_size: element.fileSize,
},
};
@@ -216,7 +216,6 @@ export class OneBotMsgApi {
data: {
summary: _.faceName, // 商城表情名称
file: filename,
- path: url,
url: url,
key: _.key,
emoji_id: _.emojiId,
@@ -339,7 +338,6 @@ export class OneBotMsgApi {
type: OB11MessageDataType.video,
data: {
file: fileCode,
- path: videoDownUrl,
url: videoDownUrl,
file_size: element.fileSize,
},
@@ -357,8 +355,8 @@ export class OneBotMsgApi {
type: OB11MessageDataType.voice,
data: {
file: fileCode,
- path: element.filePath,
file_size: element.fileSize,
+ path: element.filePath,
},
};
},
@@ -375,7 +373,8 @@ export class OneBotMsgApi {
try {
multiMsgs = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsg(element.resId);
} catch (e) {
- this.core.context.logger.logError('Protocol FetchForwardMsg fallback failed!', e);
+ this.core.context.logger.logError(`Protocol FetchForwardMsg fallback failed!
+ element = ${JSON.stringify(element)} , error=${e})`);
return null;
}
}
@@ -557,7 +556,7 @@ export class OneBotMsgApi {
},
[OB11MessageDataType.voice]: async (sendMsg, context) =>
- this.core.apis.FileApi.createValidSendPttElement(
+ this.core.apis.FileApi.createValidSendPttElement(context,
(await this.handleOb11FileLikeMessage(sendMsg, context)).path),
[OB11MessageDataType.json]: async ({ data: { data } }) => ({
@@ -658,6 +657,19 @@ export class OneBotMsgApi {
[OB11MessageDataType.node]: async () => undefined,
[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);
return this.ob11ToRawConverters.json({
data: { data: JSON.stringify(jsonData) },
@@ -702,6 +714,56 @@ export class OneBotMsgApi {
this.obContext = obContext;
this.core = core;
}
+ /**
+ * 解析带有JSON标记的文本
+ * @param text 要解析的文本
+ * @returns 解析后的结果数组,每个元素包含类型(text或json)和内容
+ */
+ parseTextWithJson(text: string) {
+ // 匹配<{...}>格式的JSON
+ const regex = /<(\{.*?\})>/g;
+ const parts: Array<{ type: 'text' | 'json', content: string | object }> = [];
+ let lastIndex = 0;
+ let match;
+
+ // 查找所有匹配项
+ while ((match = regex.exec(text)) !== null) {
+ // 添加匹配前的文本
+ if (match.index > lastIndex) {
+ parts.push({
+ type: 'text',
+ content: text.substring(lastIndex, match.index)
+ });
+ }
+
+ // 添加JSON部分
+ try {
+ const jsonContent = JSON.parse(match[1] ?? '');
+ parts.push({
+ type: 'json',
+ content: jsonContent
+ });
+ } catch (e) {
+ // 如果JSON解析失败,作为普通文本处理
+ parts.push({
+ type: 'text',
+ content: match[0]
+ });
+ }
+
+ lastIndex = regex.lastIndex;
+ }
+
+ // 添加最后一部分文本
+ if (lastIndex < text.length) {
+ parts.push({
+ type: 'text',
+ content: text.substring(lastIndex)
+ });
+ }
+
+ return parts;
+ }
async parsePrivateMsgEvent(msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType == NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
@@ -799,6 +861,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,10 +907,10 @@ export class OneBotMsgApi {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUin, msg.senderUin);
resMsg.group_id = parseInt(ret.tmpChatInfo!.groupCode);
resMsg.sender.nickname = member?.nick ?? member?.cardName ?? '临时会话';
- resMsg.temp_source = resMsg.group_id;
+ resMsg.temp_source = 0;
} else {
resMsg.group_id = 284840486;
- resMsg.temp_source = resMsg.group_id;
+ resMsg.temp_source = 0;
resMsg.sender.nickname = '临时会话';
}
}
@@ -959,7 +1022,6 @@ export class OneBotMsgApi {
});
const timeout = 10000 + (totalSize / 1024 / 256 * 1000);
-
try {
const returnMsg = await this.core.apis.MsgApi.sendMsg(peer, sendElements, timeout);
if (!returnMsg) throw new Error('发送消息失败');
@@ -972,18 +1034,19 @@ export class OneBotMsgApi {
} catch (error) {
throw new Error((error as Error).message);
} finally {
- setTimeout(async () => {
- const deletePromises = deleteAfterSentFiles.map(async file => {
- try {
- if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) {
- await fsPromise.unlink(file);
- }
- } catch (e) {
- this.core.context.logger.logError('发送消息删除文件失败', e);
- }
- });
- await Promise.all(deletePromises);
- }, 60000);
+ cleanTaskQueue.addFiles(deleteAfterSentFiles, timeout);
+ // setTimeout(async () => {
+ // const deletePromises = deleteAfterSentFiles.map(async file => {
+ // try {
+ // if (await fsPromise.access(file, constants.W_OK).then(() => true).catch(() => false)) {
+ // await fsPromise.unlink(file);
+ // }
+ // } catch (e) {
+ // this.core.context.logger.logError('发送消息删除文件失败', e);
+ // }
+ // });
+ // await Promise.all(deletePromises);
+ // }, 60000);
}
}
@@ -1042,6 +1105,8 @@ export class OneBotMsgApi {
return 'kick';
case 3:
return 'kick_me';
+ case 129:
+ return 'disband';
default:
return 'kick';
}
@@ -1202,6 +1267,41 @@ export class OneBotMsgApi {
} else if (SysMessage.contentHead.type == 528 && SysMessage.contentHead.subType == 39 && SysMessage.body?.msgContent) {
return await this.obContext.apis.UserApi.parseLikeEvent(SysMessage.body?.msgContent);
}
+ // else if (SysMessage.contentHead.type == 732 && SysMessage.contentHead.subType == 16 && SysMessage.body?.msgContent) {
+ // let data_wrap = PBString(2);
+ // let user_wrap = PBUint64(5);
+ // let group_wrap = PBUint64(4);
+
+ // ProtoBuf(class extends ProtoBufBase {
+ // group = group_wrap;
+ // content = ProtoBufIn(5, { data: data_wrap, user: user_wrap });
+ // }).decode(SysMessage.body?.msgContent.slice(7));
+ // let xml_data = UnWrap(data_wrap);
+ // let group = UnWrap(group_wrap).toString();
+ // //let user = UnWrap(user_wrap).toString();
+ // const parsedParts = this.parseTextWithJson(xml_data);
+ // //解析JSON
+ // if (parsedParts[1] && parsedParts[3]) {
+ // let set_user_id: string = (parsedParts[1].content as { data: string }).data;
+ // let uid = await this.core.apis.UserApi.getUidByUinV2(set_user_id);
+ // let new_title: string = (parsedParts[3].content as { text: string }).text;
+ // console.log(this.core.apis.GroupApi.groupMemberCache.get(group)?.get(uid)?.memberSpecialTitle, new_title)
+ // if (this.core.apis.GroupApi.groupMemberCache.get(group)?.get(uid)?.memberSpecialTitle == new_title) {
+ // return;
+ // }
+ // await this.core.apis.GroupApi.refreshGroupMemberCachePartial(group, uid);
+ // //let json_data_1_url_search = new URL((parsedParts[3].content as { url: string }).url).searchParams;
+ // //let is_new: boolean = json_data_1_url_search.get('isnew') === '1';
+
+ // //console.log(group, set_user_id, is_new, new_title);
+ // return new GroupMemberTitle(
+ // this.core,
+ // +group,
+ // +set_user_id,
+ // new_title
+ // );
+ // }
+ // }
return undefined;
}
}
diff --git a/src/onebot/api/quick-action.ts b/src/onebot/api/quick-action.ts
index aa81a88d..709c1f20 100644
--- a/src/onebot/api/quick-action.ts
+++ b/src/onebot/api/quick-action.ts
@@ -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,
diff --git a/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts b/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts
index b64e5c7f..a5b58c5d 100644
--- a/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts
+++ b/src/onebot/event/notice/OB11GroupAdminNoticeEvent.ts
@@ -3,7 +3,7 @@ import { NapCatCore } from '@/core';
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
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') {
super(core, group_id, user_id);
diff --git a/src/onebot/event/notice/OB11GroupDecreaseEvent.ts b/src/onebot/event/notice/OB11GroupDecreaseEvent.ts
index 5a52664e..37a38b5e 100644
--- a/src/onebot/event/notice/OB11GroupDecreaseEvent.ts
+++ b/src/onebot/event/notice/OB11GroupDecreaseEvent.ts
@@ -1,7 +1,7 @@
import { OB11GroupNoticeEvent } from './OB11GroupNoticeEvent';
import { NapCatCore } from '@/core';
-export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me';
+export type GroupDecreaseSubType = 'leave' | 'kick' | 'kick_me' | 'disband';
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = 'group_decrease';
@@ -11,7 +11,7 @@ export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
constructor(core: NapCatCore, groupId: number, userId: number, operatorId: number, subType: GroupDecreaseSubType = 'leave') {
super(core, groupId, userId);
this.group_id = groupId;
- this.operator_id = operatorId;
+ this.operator_id = operatorId;
this.user_id = userId;
this.sub_type = subType;
}
diff --git a/src/onebot/helper/data.ts b/src/onebot/helper/data.ts
index 0227661f..d9b0c0fa 100644
--- a/src/onebot/helper/data.ts
+++ b/src/onebot/helper/data.ts
@@ -20,16 +20,36 @@ export class OB11Construct {
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,
+ level: rawFriend.qqLevel && calcQQLevel(rawFriend.qqLevel) || 0,
}));
}
-
+ static friend(friends: FriendV2): 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: 0,
+ };
+ }
static groupMemberRole(role: number): OB11GroupMemberRole | undefined {
return {
4: OB11GroupMemberRole.owner,
@@ -73,6 +93,8 @@ export class OB11Construct {
static group(group: Group): OB11Group {
return {
+ group_all_shut: (+group.groupShutupExpireTime > 0 )? -1 : 0,
+ group_remark: group.remarkName,
group_id: +group.groupCode,
group_name: group.groupName,
member_count: group.memberCount,
diff --git a/src/onebot/index.ts b/src/onebot/index.ts
index 772b2fc6..d932e1a5 100644
--- a/src/onebot/index.ts
+++ b/src/onebot/index.ts
@@ -101,7 +101,7 @@ export class NapCatOneBot11Adapter {
const selfInfo = this.core.selfInfo;
const ob11Config = this.configLoader.configData;
- this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid)
+ this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid, false)
.then((user) => {
selfInfo.nick = user.nick;
this.context.logger.setLogSelfInfo(selfInfo);
diff --git a/src/onebot/network/websocket-client.ts b/src/onebot/network/websocket-client.ts
index 34fd6502..132a868e 100644
--- a/src/onebot/network/websocket-client.ts
+++ b/src/onebot/network/websocket-client.ts
@@ -37,7 +37,13 @@ export class OB11WebSocketClientAdapter extends IOB11NetworkAdapter ', error);
+
+ }
+
}
close() {
diff --git a/src/onebot/types/data.ts b/src/onebot/types/data.ts
index 52b97436..32e9bbb8 100644
--- a/src/onebot/types/data.ts
+++ b/src/onebot/types/data.ts
@@ -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,8 @@ export interface OB11GroupMember {
}
export interface OB11Group {
+ group_all_shut: number; // 群全员禁言
+ group_remark: string; // 群备注
group_id: number; // 群ID
group_name: string; // 群名称
member_count?: number; // 成员数量
diff --git a/src/onebot/types/message.ts b/src/onebot/types/message.ts
index bf572d98..4fba4daf 100644
--- a/src/onebot/types/message.ts
+++ b/src/onebot/types/message.ts
@@ -10,6 +10,7 @@ export enum OB11MessageType {
// 消息接口定义
export interface OB11Message {
+ real_seq?: string;// 自行扩展
temp_source?: number;
message_sent_type?: string;
target_id?: number; // 自己发送消息/私聊消息
diff --git a/src/shell/base.ts b/src/shell/base.ts
index ecd8e0b4..17258697 100644
--- a/src/shell/base.ts
+++ b/src/shell/base.ts
@@ -30,6 +30,10 @@ 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';
+import { downloadFFmpegIfNotExists } from '@/common/download-ffmpeg';
+import { FFmpegService } from '@/common/ffmpeg';
+import { connectToNamedPipe } from '@/shell/pipe';
// NapCat Shell App ES 入口文件
async function handleUncaughtExceptions(logger: LogWrapper) {
process.on('uncaughtException', (err) => {
@@ -113,120 +117,126 @@ async function handleLogin(
quickLoginUin: string | undefined,
historyLoginList: LoginListItem[]
): Promise {
- return new Promise((resolve) => {
- const loginListener = new NodeIKernelLoginListener();
- let isLogined = false;
+ let context = { isLogined: false };
+ let inner_resolve: (value: SelfInfo) => void;
+ let selfInfo: Promise = 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,
+ nick: '',
+ 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.onLoginConnected = () => { };
+ });
+ }
+ 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();
+ try {
+ await WebUiDataRuntime.runWebUiConfigQuickFunction();
+ } catch (error) {
+ logger.logError('WebUi 快速登录失败 执行失败', error);
+ }
+ }
+
+ 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,11 +294,35 @@ 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();
const logger = new LogWrapper(pathWrapper.logsPath);
handleUncaughtExceptions(logger);
+ await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e));
+ if (!process.env['NAPCAT_DISABLE_FFMPEG_DOWNLOAD']) {
+ downloadFFmpegIfNotExists(logger).then(({ path, reset }) => {
+ if (reset && path) {
+ FFmpegService.setFfmpegPath(path, logger);
+ }
+ }).catch(e => {
+ logger.logError('[Ffmpeg] Error:', e);
+ });
+ }
const basicInfoWrapper = new QQBasicInfoWrapper({ logger });
const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVesion());
diff --git a/src/shell/pipe.ts b/src/shell/pipe.ts
new file mode 100644
index 00000000..bde201cd
--- /dev/null
+++ b/src/shell/pipe.ts
@@ -0,0 +1,74 @@
+import { LogWrapper } from '@/common/log';
+import * as net from 'net';
+import * as process from 'process';
+
+/**
+ * 连接到命名管道并重定向stdout
+ * @param logger 日志记录器
+ * @param timeoutMs 连接超时时间(毫秒),默认5000ms
+ * @returns Promise,连接成功时resolve,失败时reject
+ */
+export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000): Promise<{ disconnect: () => void }> {
+ return new Promise((resolve, reject) => {
+ if (process.platform !== 'win32') {
+ logger.log('只有Windows平台支持命名管道');
+ // 非Windows平台不reject,而是返回一个空的disconnect函数
+ return resolve({ disconnect: () => { } });
+ }
+
+ const pid = process.pid;
+ const pipePath = `\\\\.\\pipe\\NapCat_${pid}`;
+
+ // 设置连接超时
+ const timeoutId = setTimeout(() => {
+ reject(new Error(`连接命名管道超时: ${pipePath}`));
+ }, timeoutMs);
+
+ try {
+ let originalStdoutWrite = process.stdout.write.bind(process.stdout);
+ const pipeSocket = net.connect(pipePath, () => {
+ // 清除超时
+ clearTimeout(timeoutId);
+
+ logger.log(`[StdOut] 已重定向到命名管道: ${pipePath}`);
+ process.stdout.write = (
+ chunk: any,
+ encoding?: BufferEncoding | (() => void),
+ cb?: () => void
+ ): boolean => {
+ if (typeof encoding === 'function') {
+ cb = encoding;
+ encoding = undefined;
+ }
+ return pipeSocket.write(chunk, encoding as BufferEncoding, cb);
+ };
+ // 提供断开连接的方法
+ const disconnect = () => {
+ process.stdout.write = originalStdoutWrite;
+ pipeSocket.end();
+ logger.log(`已手动断开命名管道连接: ${pipePath}`);
+ };
+
+ // 返回成功和断开连接的方法
+ resolve({ disconnect });
+ });
+
+ pipeSocket.on('error', (err) => {
+ clearTimeout(timeoutId);
+ process.stdout.write = originalStdoutWrite;
+ logger.log(`连接命名管道 ${pipePath} 时出错:`, err);
+ reject(err);
+ });
+
+ pipeSocket.on('end', () => {
+ process.stdout.write = originalStdoutWrite;
+ logger.log('命名管道连接已关闭');
+ });
+
+ } catch (error) {
+ clearTimeout(timeoutId);
+ logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);
+ reject(error);
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/webui/index.ts b/src/webui/index.ts
index a4eae968..37e69b79 100644
--- a/src/webui/index.ts
+++ b/src/webui/index.ts
@@ -4,6 +4,7 @@
import express from 'express';
import { createServer } from 'http';
+import { createServer as createHttpsServer } from 'https';
import { LogWrapper } from '@/common/log';
import { NapCatPathWrapper } from '@/common/path';
import { WebUiConfigWrapper } from '@webapi/helper/config';
@@ -13,11 +14,10 @@ import { createUrl } from '@webapi/utils/url';
import { sendError } from '@webapi/utils/response';
import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager';
-import multer from 'multer'; // 新增:引入multer用于错误捕获
+import multer from 'multer'; // 引入multer用于错误捕获
// 实例化Express
const app = express();
-const server = createServer(app);
/**
* 初始化并启动WebUI服务。
* 该函数配置了Express服务器以支持JSON解析和静态文件服务,并监听6099端口。
@@ -29,7 +29,8 @@ export let webUiPathWrapper: NapCatPathWrapper;
const MAX_PORT_TRY = 100;
import * as net from 'node:net';
import { WebUiDataRuntime } from './src/helper/Data';
-
+import { existsSync, readFileSync } from 'node:fs';
+export let webUiRuntimePort = 6099;
export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string, number, string]> {
try {
await tryUseHost(parsedConfig.host);
@@ -40,29 +41,47 @@ export async function InitPort(parsedConfig: WebUiConfigType): Promise<[string,
return ['', 0, ''];
}
}
+async function checkCertificates(logger: LogWrapper): Promise<{ key: string, cert: string } | null> {
+ try {
+ const certPath = join(webUiPathWrapper.configPath, 'cert.pem');
+ const keyPath = join(webUiPathWrapper.configPath, 'key.pem');
+ if (existsSync(certPath) && existsSync(keyPath)) {
+ const cert = readFileSync(certPath, 'utf8');
+ const key = readFileSync(keyPath, 'utf8');
+ logger.log('[NapCat] [WebUi] 找到SSL证书,将启用HTTPS模式');
+ return { cert, key };
+ }
+ return null;
+ } catch (error) {
+ logger.log('[NapCat] [WebUi] 检查SSL证书时出错: ' + error);
+ return null;
+ }
+}
export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapper) {
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;
}
- 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);
+ WebUiDataRuntime.setWebUiConfigQuickFunction(
+ 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);
}
- console.log(`[NapCat] [WebUi] Auto login account: ${autoLoginAccount}`);
- } catch (error) {
- console.log(`[NapCat] [WebUi] Auto login account failed.` + error);
}
- }
- }, 30000);
+ });
// ------------注册中间件------------
// 使用express的json中间件
app.use(express.json());
@@ -105,6 +124,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// 挂载静态路由(前端),路径为 /webui
app.use('/webui', express.static(pathWrapper.staticPath));
// 初始化WebSocket服务器
+ const sslCerts = await checkCertificates(logger);
+ const isHttps = !!sslCerts;
+ let server = isHttps && sslCerts ? createHttpsServer(sslCerts, app) : createServer(app);
server.on('upgrade', (request, socket, head) => {
terminalManager.initialize(request, socket, head, logger);
});
diff --git a/src/webui/src/api/Auth.ts b/src/webui/src/api/Auth.ts
index 53e06315..5fb8c1f3 100644
--- a/src/webui/src/api/Auth.ts
+++ b/src/webui/src/api/Auth.ts
@@ -20,25 +20,26 @@ export const CheckDefaultTokenHandler: RequestHandler = async (_, res) => {
export const LoginHandler: RequestHandler = async (req, res) => {
// 获取WebUI配置
const WebUiConfigData = await WebUiConfig.GetWebUIConfig();
- // 获取请求体中的token
- const { token } = req.body;
+ // 获取请求体中的hash
+ const { hash } = req.body;
// 获取客户端IP
const clientIP = req.ip || req.socket.remoteAddress || '';
// 如果token为空,返回错误信息
- if (isEmpty(token)) {
+ if (isEmpty(hash)) {
return sendError(res, 'token is empty');
}
// 检查登录频率
if (!WebUiDataRuntime.checkLoginRate(clientIP, WebUiConfigData.loginRate)) {
return sendError(res, 'login rate limit');
}
- //验证config.token是否等于token
- if (WebUiConfigData.token !== token) {
+ //验证config.token hash是否等于token hash
+ if (!AuthHelper.comparePasswordHash(WebUiConfigData.token, hash)) {
return sendError(res, 'token is invalid');
}
+
// 签发凭证
- const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(WebUiConfigData.token))).toString(
+ const signCredential = Buffer.from(JSON.stringify(AuthHelper.signCredential(hash))).toString(
'base64'
);
// 返回成功信息
diff --git a/src/webui/src/helper/Data.ts b/src/webui/src/helper/Data.ts
index bafc5ba5..c151f003 100644
--- a/src/webui/src/helper/Data.ts
+++ b/src/webui/src/helper/Data.ts
@@ -25,6 +25,9 @@ const LoginRuntime: LoginRuntimeType = {
NewQQLoginList: [],
},
packageJson: packageJson,
+ WebUiConfigQuickFunction: async () => {
+ return;
+ }
};
export const WebUiDataRuntime = {
@@ -118,4 +121,11 @@ export const WebUiDataRuntime = {
getQQVersion() {
return LoginRuntime.QQVersion;
},
+
+ setWebUiConfigQuickFunction(func: LoginRuntimeType['WebUiConfigQuickFunction']): void {
+ LoginRuntime.WebUiConfigQuickFunction = func;
+ },
+ runWebUiConfigQuickFunction: async function () {
+ await LoginRuntime.WebUiConfigQuickFunction();
+ }
};
diff --git a/src/webui/src/helper/SignToken.ts b/src/webui/src/helper/SignToken.ts
index 50865b19..495bad56 100644
--- a/src/webui/src/helper/SignToken.ts
+++ b/src/webui/src/helper/SignToken.ts
@@ -5,13 +5,13 @@ export class AuthHelper {
/**
* 签名凭证方法。
- * @param token 待签名的凭证字符串。
+ * @param hash 待签名的凭证字符串。
* @returns 签名后的凭证对象。
*/
- public static signCredential(token: string): WebUiCredentialJson {
+ public static signCredential(hash: string): WebUiCredentialJson {
const innerJson: WebUiCredentialInnerJson = {
CreatedTime: Date.now(),
- TokenEncoded: token,
+ HashEncoded: hash,
};
const jsonString = JSON.stringify(innerJson);
const hmac = crypto.createHmac('sha256', AuthHelper.secretKey).update(jsonString, 'utf8').digest('hex');
@@ -57,8 +57,7 @@ export class AuthHelper {
const currentTime = Date.now() / 1000;
const createdTime = credentialJson.Data.CreatedTime;
const timeDifference = currentTime - createdTime;
-
- return timeDifference <= 3600 && credentialJson.Data.TokenEncoded === token;
+ return timeDifference <= 3600 && credentialJson.Data.HashEncoded === AuthHelper.generatePasswordHash(token);
}
/**
@@ -85,4 +84,23 @@ export class AuthHelper {
return store.exists(`revoked:${hmac}`) > 0;
}
+
+ /**
+ * 生成密码Hash
+ * @param password 密码
+ * @returns 生成的Hash值
+ */
+ public static generatePasswordHash(password: string): string {
+ return crypto.createHash('sha256').update(password + '.napcat').digest().toString('hex')
+ }
+
+ /**
+ * 对比密码和Hash值
+ * @param password 密码
+ * @param hash Hash值
+ * @returns 布尔值,表示密码是否匹配Hash值
+ */
+ public static comparePasswordHash(password: string, hash: string): boolean {
+ return this.generatePasswordHash(password) === hash;
+ }
}
diff --git a/src/webui/src/middleware/auth.ts b/src/webui/src/middleware/auth.ts
index 67d73ecd..8e2d756c 100644
--- a/src/webui/src/middleware/auth.ts
+++ b/src/webui/src/middleware/auth.ts
@@ -21,17 +21,18 @@ export async function auth(req: Request, res: Response, next: NextFunction) {
return sendError(res, 'Unauthorized');
}
// 获取token
- const token = authorization[1];
+ const hash = authorization[1];
+ if(!hash) return sendError(res, 'Unauthorized');
// 解析token
let Credential: WebUiCredentialJson;
try {
- Credential = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
+ Credential = JSON.parse(Buffer.from(hash, 'base64').toString('utf-8'));
} catch (e) {
return sendError(res, 'Unauthorized');
}
// 获取配置
const config = await WebUiConfig.GetWebUIConfig();
- // 验证凭证在1小时内有效且token与原始token相同
+ // 验证凭证在1小时内有效
const credentialJson = AuthHelper.validateCredentialWithinOneHour(config.token, Credential);
if (credentialJson) {
// 通过验证
diff --git a/src/webui/src/types/data.d.ts b/src/webui/src/types/data.d.ts
index fb9a644e..d881a4b0 100644
--- a/src/webui/src/types/data.d.ts
+++ b/src/webui/src/types/data.d.ts
@@ -9,6 +9,7 @@ interface LoginRuntimeType {
QQLoginUin: string;
QQLoginInfo: SelfInfo;
QQVersion: string;
+ WebUiConfigQuickFunction: () => Promise;
NapCatHelper: {
onQuickLoginRequested: (uin: string) => Promise<{ result: boolean; message: string }>;
onOB11ConfigChanged: (ob11: OneBotConfig) => Promise;
diff --git a/src/webui/src/types/sign_token.d.ts b/src/webui/src/types/sign_token.d.ts
index 5bd79b69..1b6514d1 100644
--- a/src/webui/src/types/sign_token.d.ts
+++ b/src/webui/src/types/sign_token.d.ts
@@ -1,6 +1,6 @@
interface WebUiCredentialInnerJson {
CreatedTime: number;
- TokenEncoded: string;
+ HashEncoded: string;
}
interface WebUiCredentialJson {
diff --git a/vite.config.ts b/vite.config.ts
index 2b930a60..fe30de1b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,8 +8,6 @@ const external = [
'silk-wasm',
'ws',
'express',
- '@ffmpeg.wasm/core-mt',
- 'piscina',
'openai'
];
const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat();
@@ -99,7 +97,6 @@ const UniversalBaseConfig = () =>
entry: {
napcat: 'src/universal/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
- 'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
'worker/conoutSocketWorker': 'src/pty/worker/conoutSocketWorker.ts',
},
formats: ['es'],
@@ -129,7 +126,6 @@ const ShellBaseConfig = () =>
entry: {
napcat: 'src/shell/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
- 'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
'worker/conoutSocketWorker': 'src/pty/worker/conoutSocketWorker.ts',
},
formats: ['es'],
@@ -159,7 +155,6 @@ const FrameworkBaseConfig = () =>
entry: {
napcat: 'src/framework/napcat.ts',
'audio-worker': 'src/common/audio-worker.ts',
- 'ffmpeg-worker': 'src/common/ffmpeg-worker.ts',
'worker/conoutSocketWorker': 'src/pty/worker/conoutSocketWorker.ts',
},
formats: ['es'],