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 + ![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto) - + +_Modern protocol-side framework implemented based on NTQQ._ + +> 云起兮风生,心向远方兮路未曾至. +
--- -## 欢迎回家 -NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 -## 特性介绍 -- [x] **安装简单**:就算是笨蛋也能使用 -- [x] **性能友好**:就算是低内存也能使用 -- [x] **接口丰富**:就算是没有也能使用 -- [x] **稳定好用**:就算是被捉也能使用 +## Welcome ++ NapCatQQ is a modern implementation of the Bot protocol based on NTQQ. + - NapCatQQ 是现代化的基于 NTQQ 的 Bot 协议端实现 -## 使用框架 +## Feature + ++ **Easy to Use** + - 作为初学者能够轻松使用. ++ **Quick and Efficient** + - 在低内存操作系统长时运行. ++ **Rich API Interface** + - 完整实现了大部分标准接口. ++ **Stable and Reliable** + - 持续稳定的开发与维护. +## Quick Start 可前往 [Release](https://github.com/NapNeko/NapCatQQ/releases/) 页面下载最新版本 **首次使用**请务必查看如下文档看使用教程 -### 文档地址 +## Link -[Cloudflare.Worker](https://doc.napneko.icu/) +| Docs | [![Github.IO](https://img.shields.io/badge/docs%20on-Github.IO-orange)](https://napneko.github.io/) | [![Cloudflare.Worker](https://img.shields.io/badge/docs%20on-Cloudflare.Worker-black)](https://doc.napneko.icu/) | [![Cloudflare.HKServer](https://img.shields.io/badge/docs%20on-Cloudflare.HKServer-informational)](https://napcat.napneko.icu/) | +|:-:|:-:|:-:|:-:| -[Cloudflare.HKServer](https://napcat.napneko.icu/) +| Docs | [![Cloudflare.Pages](https://img.shields.io/badge/docs%20on-Cloudflare.Pages-blue)](https://napneko.pages.dev/) | [![Server.Other](https://img.shields.io/badge/docs%20on-Server.Other-green)](https://napcat.cyou/) | [![NapCat.Wiki](https://img.shields.io/badge/docs%20on-NapCat.Wiki-red)](https://www.napcat.wiki) | +|:-:|:-:|:-:|:-:| -[Github.IO](https://napneko.github.io/) +| Contact | [![QQ Group#1](https://img.shields.io/badge/QQ%20Group%231-Join-blue)](https://qm.qq.com/q/I6LU87a0Yq) | [![QQ Group#2](https://img.shields.io/badge/QQ%20Group%232-Join-blue)](https://qm.qq.com/q/HaRcfrHpUk) | [![Telegram](https://img.shields.io/badge/Telegram-MelodicMoonlight-blue)](https://t.me/MelodicMoonlight) | +|:-:|:-:|:-:|:-:| -[Cloudflare.Pages](https://napneko.pages.dev/) +## Thanks -[Server.Other](https://docs.napcat.cyou/) ++ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权 -[NapCat.Wiki](https://www.napcat.wiki) - -## 回家旅途 -[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'],