Compare commits

...

51 Commits

Author SHA1 Message Date
手瓜一十雪
beae21c4fd feat: 更新依赖项并移除速率限制中间件 2025-05-16 14:51:35 +08:00
手瓜一十雪
34db3af48d fix: #1007 2025-05-15 21:10:21 +08:00
Mlikiowa
198da960dd release: v4.7.62 2025-05-12 12:37:01 +00:00
手瓜一十雪
cb83918fb3 fix 2025-05-12 20:36:39 +08:00
Mlikiowa
f59a48540b release: v4.7.61 2025-05-12 12:34:18 +00:00
手瓜一十雪
ccf9c1a5fb Merge pull request #1005 from NapNeko/fix-1001
refactor: remove image-size
2025-05-12 20:22:35 +08:00
手瓜一十雪
ba6a85142a fix 2025-05-12 19:29:40 +08:00
手瓜一十雪
440baccd2a fix 2025-05-12 19:27:46 +08:00
手瓜一十雪
690c073328 fix 2025-05-12 19:27:01 +08:00
手瓜一十雪
3f0730ed4f fix 2025-05-12 19:25:03 +08:00
手瓜一十雪
01d5663bc8 fix: image size 2025-05-12 19:18:33 +08:00
手瓜一十雪
49806cd00e Create index.ts 2025-05-12 19:17:17 +08:00
手瓜一十雪
935b0848e5 Merge pull request #1004 from NapNeko/dependabot/npm_and_yarn/esbuild-0.25.4
build(deps-dev): bump esbuild from 0.25.0 to 0.25.4
2025-05-12 18:45:23 +08:00
dependabot[bot]
5ca20a89a2 build(deps-dev): bump esbuild from 0.25.0 to 0.25.4
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.0 to 0.25.4.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.0...v0.25.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 08:37:11 +00:00
Mlikiowa
e89a2266ec release: v4.7.60 2025-05-11 03:32:29 +00:00
手瓜一十雪
6607533311 fix: #996 2025-05-11 11:31:52 +08:00
Mlikiowa
4057054220 release: v4.7.58 2025-05-11 03:24:02 +00:00
手瓜一十雪
055e43845e feat: ffmpeg下载来源更换 2025-05-11 11:23:43 +08:00
Mlikiowa
d67270f2f8 release: v4.7.57 2025-05-10 13:15:37 +00:00
手瓜一十雪
d061b6c190 feat: 34958 2025-05-10 21:15:07 +08:00
Mlikiowa
945f87d77f release: v4.7.56 2025-05-09 11:28:05 +00:00
手瓜一十雪
6c9be52d39 feat: 34740 2025-05-09 19:16:25 +08:00
手瓜一十雪
98e347f010 fix: 过滤掉已读 2025-05-09 18:51:00 +08:00
手瓜一十雪
607e367bb1 fix 2025-05-09 13:17:47 +08:00
Mlikiowa
7a25dc1ef1 release: v4.7.55 2025-05-08 10:11:36 +00:00
手瓜一十雪
e22ec4be09 Merge pull request #991 from NapNeko/fix-upload-foward-cahce-del
fix: upload-foward-cache-del
2025-05-08 18:10:45 +08:00
手瓜一十雪
51a06622f9 fix: typo
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-08 18:04:40 +08:00
手瓜一十雪
22faf5b831 fix 2025-05-08 17:58:12 +08:00
手瓜一十雪
e781c662b2 fix 2025-05-08 15:20:55 +08:00
手瓜一十雪
5744698d24 docs: 清空不好的影响&推荐一下 2025-05-08 15:20:06 +08:00
Mlikiowa
2c2ab3cd48 release: v4.7.51 2025-05-07 15:16:50 +00:00
手瓜一十雪
cfae4f5acd fix: 增强 2025-05-07 22:26:25 +08:00
Mlikiowa
de541e3249 release: v4.5.50 2025-05-07 14:10:15 +00:00
手瓜一十雪
f5187c5c01 Merge pull request #989 from NapNeko/file-url-feat
feat: 消息上报Url重构
2025-05-07 22:09:29 +08:00
手瓜一十雪
9936279443 fix 2025-05-07 22:03:00 +08:00
手瓜一十雪
2818773fd4 fix 2025-05-07 20:53:36 +08:00
手瓜一十雪
b9293cbcd0 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:50 +08:00
手瓜一十雪
5b9e44ddfc fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:28 +08:00
手瓜一十雪
1791accab7 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:51:12 +08:00
手瓜一十雪
08081360f3 fix
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-07 20:50:51 +08:00
手瓜一十雪
e933a95e97 fix 2025-05-07 18:10:58 +08:00
手瓜一十雪
4ef457fe6f feat: fileUrl Get 2025-05-07 18:10:49 +08:00
Mlikiowa
bd9cae8921 release: v4.7.49 2025-05-07 09:16:28 +00:00
手瓜一十雪
303a74f8fd feat: 背压问题 2025-05-07 17:14:57 +08:00
Mlikiowa
0b7f126ce1 release: v4.7.48 2025-05-07 08:21:34 +00:00
Clansty
308b5c027f fix: at 变成负数 2025-05-07 03:46:17 +08:00
手瓜一十雪
ed3abc4b43 feat 2025-05-04 21:11:34 +08:00
Mlikiowa
87ecb3b380 release: v4.7.47 2025-05-03 14:27:49 +00:00
手瓜一十雪
7e31763a25 fix 2025-05-03 22:26:41 +08:00
Mlikiowa
c9df57d16a release: v4.7.46 2025-05-03 08:08:25 +00:00
手瓜一十雪
3d0f8ee657 fix 2025-05-03 16:06:51 +08:00
36 changed files with 1380 additions and 158 deletions

View File

@@ -1,8 +1,9 @@
<img src="https://napneko.github.io/assets/newnewlogo.png" width = "305" height = "411" alt="NapCat" align=right />
<div align="center">
# NapCat
![NapCatQQ](https://socialify.git.ci/NapNeko/NapCatQQ/image?font=Jost&logo=https%3A%2F%2Fnapneko.github.io%2Fassets%2Fnewlogo.png&name=1&owner=1&pattern=Diagonal+Stripes&stargazers=1&theme=Auto)
_Modern protocol-side framework implemented based on NTQQ._
@@ -50,6 +51,8 @@ _Modern protocol-side framework implemented based on NTQQ._
+ [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) 对本项目的大力支持 参考部分代码 已获授权
+ [AstrBot](https://github.com/AstrBotDevs/AstrBot) 是完美适配本项目的LLM Bot框架 在此推荐一下
+ 不过最最重要的 还是需要感谢屏幕前的你哦~
---
@@ -61,7 +64,3 @@ _Modern protocol-side framework implemented based on NTQQ._
2. 项目其余逻辑代码采用[本仓库开源许可](./LICENSE).
**本仓库仅用于提高易用性,实现消息推送类功能,此外,禁止任何项目未经仓库主作者授权基于 NapCat 代码开发。使用请遵守当地法律法规,由此造成的问题由使用者和提供违规使用教程者负责。**
## Warnings
[某框架抄袭部分分析](https://napneko.github.io/other/about-copy)

Binary file not shown.

BIN
external/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

View File

@@ -1,9 +1,9 @@
{
"name": "qq-chat",
"version": "9.9.18-32869",
"verHash": "e735296c",
"linuxVersion": "3.2.16-32869",
"linuxVerHash": "4c192ba9",
"version": "9.9.19-34740",
"verHash": "f31348f2",
"linuxVersion": "3.2.17-34740",
"linuxVerHash": "5aa2d8d6",
"private": true,
"description": "QQ",
"productName": "QQ",
@@ -16,25 +16,8 @@
"bin": {
"qd": "externals/devtools/cli/index.js"
},
"appid": {
"win32": "537258389",
"darwin": "537258412",
"linux": "537258424"
},
"main": "./loadNapCat.js",
"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",
"buildVersion": "34740",
"isPureShell": true,
"isByteCodeShell": true,
"platform": "win32",

View File

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

View File

@@ -2,7 +2,7 @@
"name": "napcat",
"private": true,
"type": "module",
"version": "4.7.45",
"version": "4.7.62",
"scripts": {
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
"build:framework": "npm run build:webui && vite build --mode framework || exit 1",
@@ -41,30 +41,31 @@
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.0",
"esbuild": "0.25.4",
"eslint": "^9.14.0",
"eslint-import-resolver-typescript": "^4.0.0",
"eslint-plugin-import": "^2.29.1",
"express-rate-limit": "^7.5.0",
"express": "npm:@karinjs/express@1.0.3",
"fast-xml-parser": "^4.3.6",
"file-type": "^20.0.0",
"globals": "^16.0.0",
"image-size": "^1.1.1",
"json5": "^2.2.3",
"multer": "^1.4.5-lts.1",
"napcat.protobuf": "^1.1.4",
"on-finished": "^2.4.1",
"silk-wasm": "^3.6.1",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"vite-plugin-cp": "^6.0.0",
"vite-plugin-wasm": "^3.4.1",
"vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.4",
"winston": "^3.17.0",
"compressing": "^1.10.1"
"ws": "npm:@karinjs/ws@1.2.0"
},
"dependencies": {
"express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
"type-is": "^2.0.1"
}
}

View File

@@ -8,11 +8,12 @@ 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 downloadOri = "https://github.com/NapNeko/ffmpeg-build/releases/download/v1.0.0/ffmpeg-7.1.1-win64.zip"
const urls = [
"https://github.moeyy.xyz/" + downloadOri,
"https://ghp.ci/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
"https://gh.api.99988866.xyz/" + downloadOri,
downloadOri
];
@@ -336,9 +337,16 @@ export async function downloadFFmpegIfNotExists(log: LogWrapper) {
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) => {
let url = await downloadFFmpeg(path.join(currentPath, 'ffmpeg'), path.join(currentPath, 'cache'), (percentage: number, message: string) => {
log.log(`[FFmpeg] [Download] ${percentage}% - ${message}`);
});
if (!url) {
log.log('[FFmpeg] [Error] 下载FFmpeg失败');
return {
path: null,
reset: false
};
}
return {
path: path.join(currentPath, 'ffmpeg'),
reset: true

View File

@@ -4,10 +4,10 @@ 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';
import { imageSizeFallBack } from '@/image-size';
const currentPath = dirname(fileURLToPath(import.meta.url));
const execFileAsync = promisify(execFile);
const getFFmpegPath = (tool: string): string => {
@@ -157,7 +157,7 @@ export class FFmpegService {
try {
await this.extractThumbnail(videoPath, thumbnailPath);
// 获取图片尺寸
const dimensions = imageSize(thumbnailPath);
const dimensions = await imageSizeFallBack(thumbnailPath);
return {
format: fileType?.ext ?? 'mp4',

View File

@@ -1 +1 @@
export const napCatVersion = '4.7.45';
export const napCatVersion = '4.7.62';

View File

@@ -17,8 +17,6 @@ import fs from 'fs';
import fsPromises from 'fs/promises';
import { InstanceContext, NapCatCore, SearchResultItem } from '@/core';
import { fileTypeFromFile } from 'file-type';
import imageSize from 'image-size';
import { ISizeCalculationResult } from 'image-size/dist/types/interface';
import { RkeyManager } from '@/core/helper/rkey';
import { calculateFileMD5 } from '@/common/file';
import pathLib from 'node:path';
@@ -28,6 +26,9 @@ import { SendMessageContext } from '@/onebot/api';
import { getFileTypeForSendType } from '../helper/msg';
import { FFmpegService } from '@/common/ffmpeg';
import { rkeyDataType } from '../types/file';
import { NapProtoMsg } from '@napneko/nap-proto-core';
import { FileId } from '../packet/transformer/proto/misc/fileid';
import { imageSizeFallBack } from '@/image-size';
export class NTQQFileApi {
context: InstanceContext;
@@ -63,6 +64,76 @@ export class NTQQFileApi {
}
}
async getFileUrl(chatType: ChatType, peer: string, fileUUID?: string, file10MMd5?: string | undefined) {
if (this.core.apis.PacketApi.available) {
try {
if (chatType === ChatType.KCHATTYPEGROUP && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetGroupFileUrl(+peer, fileUUID);
} else if (file10MMd5 && fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPrivateFileUrl(peer, fileUUID, file10MMd5);
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('fileUUID or file10MMd5 is undefined');
}
async getPttUrl(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1403) {
return this.core.apis.PacketApi.pkt.operation.GetGroupPttUrl(+peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetPttUrl(peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('packet cant get ptt url');
}
async getVideoUrlPacket(peer: string, fileUUID?: string) {
if (this.core.apis.PacketApi.available && fileUUID) {
let appid = new NapProtoMsg(FileId).decode(Buffer.from(fileUUID.replaceAll('-', '+').replaceAll('_', '/'), 'base64')).appid;
try {
if (appid && appid === 1415) {
return this.core.apis.PacketApi.pkt.operation.GetGroupVideoUrl(+peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
} else if (fileUUID) {
return this.core.apis.PacketApi.pkt.operation.GetVideoUrl(peer, {
fileUuid: fileUUID,
storeId: 1,
uploadTime: 0,
ttl: 0,
subType: 0,
});
}
} catch (error) {
this.context.logger.logError('获取文件URL失败', (error as Error).message);
}
}
throw new Error('packet cant get video url');
}
async copyFile(filePath: string, destPath: string) {
await this.core.util.copyFile(filePath, destPath);
@@ -137,7 +208,7 @@ export class NTQQFileApi {
if (fileSize === 0) {
throw new Error('文件异常大小为0');
}
const imageSize = await this.core.apis.FileApi.getImageSize(picPath);
const imageSize = await imageSizeFallBack(picPath);
context.deleteAfterSentFiles.push(path);
return {
elementType: ElementType.PIC,
@@ -325,6 +396,7 @@ export class NTQQFileApi {
}
});
});
return res.flat();
}
async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, timeout = 1000 * 60 * 2, force: boolean = false) {
@@ -364,19 +436,6 @@ export class NTQQFileApi {
return completeRetData.filePath;
}
async getImageSize(filePath: string): Promise<ISizeCalculationResult> {
return new Promise((resolve, reject) => {
imageSize(filePath, (err: Error | null, dimensions) => {
if (err) {
reject(new Error(err.message));
} else if (!dimensions) {
reject(new Error('获取图片尺寸失败'));
} else {
resolve(dimensions);
}
});
});
}
async searchForFile(keys: string[]): Promise<SearchResultItem | undefined> {
const randomResultId = 100000 + Math.floor(Math.random() * 10000);

View File

@@ -71,6 +71,7 @@ export class NTQQMsgApi {
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
//searchFields: 3,
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
@@ -84,6 +85,7 @@ export class NTQQMsgApi {
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
chatInfo: peer,
filterMsgType: [],
//searchFields: 3,
filterSendersUid: SendersUid,
filterMsgToTime: MsgTime,
filterMsgFromTime: MsgTime,
@@ -100,6 +102,7 @@ export class NTQQMsgApi {
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: false,
//searchFields: 3,
isIncludeCurrent: true,
pageLimit: 1,
});
@@ -110,6 +113,7 @@ export class NTQQMsgApi {
filterMsgType: [],
filterSendersUid: [],
filterMsgToTime: '0',
//searchFields: 3,
filterMsgFromTime: '0',
isReverseOrder: true,
isIncludeCurrent: true,
@@ -128,6 +132,7 @@ export class NTQQMsgApi {
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
filterMsgType: [],
filterSendersUid: [],
//searchFields: 3,
filterMsgToTime: filterMsgToTime,
filterMsgFromTime: filterMsgFromTime,
isReverseOrder: false,
@@ -142,6 +147,7 @@ export class NTQQMsgApi {
chatInfo: peer,
filterMsgType: [],
filterSendersUid: SendersUid,
//searchFields: 3,
filterMsgToTime: '0',
filterMsgFromTime: '0',
isReverseOrder: true,

View File

@@ -282,5 +282,9 @@
"3.2.17-34740": {
"appid": 537290727,
"qua": "V1_LNX_NQ_3.2.17_34740_GW_B"
},
"9.9.19-34958": {
"appid": 537290742,
"qua": "V1_WIN_NQ_9.9.19_34958_GW_B"
}
}

View File

@@ -354,5 +354,17 @@
"9.9.19-34740-x64": {
"send": "3BDD8D0",
"recv": "3BE20D0"
},
"3.2.17-34740-x64": {
"send": "ADDF0A0",
"recv": "ADE2AC0"
},
"3.2.17-34740-arm64": {
"send": "7753BB8",
"recv": "77574E8"
},
"9.9.19-34958-x64": {
"send": "3BDD8D0",
"recv": "3BE20D0"
}
}

View File

@@ -3,43 +3,43 @@ import { BuddyCategoryType, FriendRequestNotify } from '@/core/types';
export type OnBuddyChangeParams = BuddyCategoryType[];
export class NodeIKernelBuddyListener {
onBuddyListChangedV2(arg: unknown): any {
onBuddyListChangedV2(_arg: unknown): any {
}
onAddBuddyNeedVerify(arg: unknown): any {
onAddBuddyNeedVerify(_arg: unknown): any {
}
onAddMeSettingChanged(arg: unknown): any {
onAddMeSettingChanged(_arg: unknown): any {
}
onAvatarUrlUpdated(arg: unknown): any {
onAvatarUrlUpdated(_arg: unknown): any {
}
onBlockChanged(arg: unknown): any {
onBlockChanged(_arg: unknown): any {
}
onBuddyDetailInfoChange(arg: unknown): any {
onBuddyDetailInfoChange(_arg: unknown): any {
}
onBuddyInfoChange(arg: unknown): any {
onBuddyInfoChange(_arg: unknown): any {
}
onBuddyListChange(arg: OnBuddyChangeParams): any {
onBuddyListChange(_arg: OnBuddyChangeParams): any {
}
onBuddyRemarkUpdated(arg: unknown): any {
onBuddyRemarkUpdated(_arg: unknown): any {
}
onBuddyReqChange(arg: FriendRequestNotify): any {
onBuddyReqChange(_arg: FriendRequestNotify): any {
}
onBuddyReqUnreadCntChange(arg: unknown): any {
onBuddyReqUnreadCntChange(_arg: unknown): any {
}
onCheckBuddySettingResult(arg: unknown): any {
onCheckBuddySettingResult(_arg: unknown): any {
}
onDelBatchBuddyInfos(arg: unknown): any {
onDelBatchBuddyInfos(_arg: unknown): any {
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
@@ -66,12 +66,12 @@ export class NodeIKernelBuddyListener {
onDoubtBuddyReqUnreadNumChange(_num: number): void | Promise<void> {
}
onNickUpdated(arg: unknown): any {
onNickUpdated(_arg: unknown): any {
}
onSmartInfos(arg: unknown): any {
onSmartInfos(_arg: unknown): any {
}
onSpacePermissionInfos(arg: unknown): any {
onSpacePermissionInfos(_arg: unknown): any {
}
}

View File

@@ -124,6 +124,20 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetPttUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadPtt.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetVideoUrl(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadVideo.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadVideo.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupImage.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -131,6 +145,21 @@ export class PacketOperationContext {
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupVideoUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupVideo.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR(imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true);
@@ -154,7 +183,7 @@ export class PacketOperationContext {
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
const ps = msg.map((m) => {
return m.msg.map(async(e) => {
return m.msg.map(async (e) => {
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
if (!e.targetPeer?.peerUid) {
@@ -222,6 +251,7 @@ export class PacketOperationContext {
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);
@@ -229,13 +259,6 @@ export class PacketOperationContext {
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}
async GetGroupPttUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadGroupPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
const req = trans.GetMiniAppAdaptShareInfo.build(param);
const resp = await this.context.client.sendOidbPacket(req, true);

View File

@@ -0,0 +1,50 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadGroupVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 2,
group: {
groupUin: groupUin
}
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11EA, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadGroupVideo();

View File

@@ -0,0 +1,51 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadPtt extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 1,
businessType: 3,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x126D, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadPtt();

View File

@@ -0,0 +1,51 @@
import * as proto from '@/core/packet/transformer/proto';
import { NapProtoEncodeStructType, NapProtoMsg } from '@napneko/nap-proto-core';
import { OidbPacket, PacketTransformer } from '@/core/packet/transformer/base';
import OidbBase from '@/core/packet/transformer/oidb/oidbBase';
import { IndexNode } from '@/core/packet/transformer/proto';
class DownloadVideo extends PacketTransformer<typeof proto.NTV2RichMediaResp> {
constructor() {
super();
}
build(selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>): OidbPacket {
const body = new NapProtoMsg(proto.NTV2RichMediaReq).encode({
reqHead: {
common: {
requestId: 1,
command: 200
},
scene: {
requestType: 2,
businessType: 2,
sceneType: 1,
c2C: {
accountType: 2,
targetUid: selfUid
},
},
client: {
agentType: 2,
}
},
download: {
node: node,
download: {
video: {
busiType: 0,
sceneType: 0
}
}
}
});
return OidbBase.build(0x11E9, 200, body, true, false);
}
parse(data: Buffer) {
const oidbBody = OidbBase.parse(data).body;
return new NapProtoMsg(proto.NTV2RichMediaResp).decode(oidbBody);
}
}
export default new DownloadVideo();

View File

@@ -13,3 +13,6 @@ export { default as UploadPrivatePtt } from './UploadPrivatePtt';
export { default as UploadPrivateVideo } from './UploadPrivateVideo';
export { default as DownloadImage } from './DownloadImage';
export { default as DownloadGroupImage } from './DownloadGroupImage';
export { default as DownloadVideo } from './DownloadVideo';
export { default as DownloadGroupVideo } from './DownloadGroupVideo';
export { default as DownloadPtt } from './DownloadPtt';

View File

@@ -0,0 +1,6 @@
import { ProtoField, ScalarType } from '@napneko/nap-proto-core';
export const FileId = {
appid: ProtoField(4, ScalarType.UINT32, true),
ttl: ProtoField(10, ScalarType.UINT32, true),
};

View File

@@ -249,7 +249,7 @@ export interface NodeIKernelGroupService {
reqToJoinGroup(groupCode: string, arg: unknown): void;
setGroupShutUp(groupCode: string, shutUp: boolean): void;
setGroupShutUp(groupCode: string, shutUp: boolean): Promise<GeneralCallResult>;
getGroupShutUpMemberList(groupCode: string): Promise<GeneralCallResult>;

View File

@@ -148,10 +148,11 @@ export interface NodeIKernelMsgService {
msgList: RawMessage[]
}>;
//@deprecated
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
// getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true
getMsgs(peer: Peer & { privilegeFlag: number }, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>;
//@deprecated
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
msgList: RawMessage[]
}>;

View File

@@ -508,7 +508,8 @@ export interface RawMessage {
* 查询消息参数接口
*/
export interface QueryMsgsParams {
chatInfo: Peer;
chatInfo: Peer & { privilegeFlag?: number };
//searchFields: number;
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
filterSendersUid: string[];
filterMsgFromTime: string;

View File

@@ -132,18 +132,26 @@ export enum BuddyReqType {
KMEINITIATORWAITPEERCONFIRM = 13
}
// 其中 ? 代表新版本参数
export interface FriendRequest {
isBuddy?: boolean;
isInitiator?: boolean;
isDecide: boolean;
friendUid: string;
reqType: BuddyReqType,
reqTime: string; // 时间戳 秒
flag?: number; // 0
preGroupingId?: number; // 0
commFriendNum?: number; // 共同好友数
extWords: string; // 申请人填写的验证消息
isUnread: boolean;
isDoubt?: boolean; // 是否是可疑的好友请求
nameMore?: string;
friendNick: string;
sourceId: number;
groupCode: string
groupCode: string;
isBuddy?: boolean;
isAgreed?: boolean;
relation?: number;
}
export interface FriendRequestNotify {

426
src/image-size/index.ts Normal file
View File

@@ -0,0 +1,426 @@
import * as fs from 'fs';
import { ReadStream } from 'fs';
export interface ImageSize {
width: number;
height: number;
}
export enum ImageType {
JPEG = 'jpeg',
PNG = 'png',
BMP = 'bmp',
GIF = 'gif',
WEBP = 'webp',
UNKNOWN = 'unknown',
}
interface ImageParser {
readonly type: ImageType;
canParse(buffer: Buffer): boolean;
parseSize(stream: ReadStream): Promise<ImageSize | undefined>;
}
// 魔术匹配
function matchMagic(buffer: Buffer, magic: number[], offset = 0): boolean {
if (buffer.length < offset + magic.length) {
return false;
}
for (let i = 0; i < magic.length; i++) {
if (buffer[offset + i] !== magic[i]) {
return false;
}
}
return true;
}
// PNG解析器
class PngParser implements ImageParser {
readonly type = ImageType.PNG;
// PNG 魔术头89 50 4E 47 0D 0A 1A 0A
private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.PNG_SIGNATURE);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(24) as Buffer;
if (!buf || buf.length < 24) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// JPEG解析器
class JpegParser implements ImageParser {
readonly type = ImageType.JPEG;
// JPEG 魔术头FF D8
private readonly JPEG_SIGNATURE = [0xFF, 0xD8];
// JPEG标记常量
private readonly SOF_MARKERS = {
SOF0: 0xC0, // 基线DCT
SOF1: 0xC1, // 扩展顺序DCT
SOF2: 0xC2, // 渐进式DCT
SOF3: 0xC3, // 无损
} as const;
// 非SOF标记
private readonly NON_SOF_MARKERS: number[] = [
0xC4, // DHT
0xC8, // JPEG扩展
0xCC, // DAC
] as const;
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.JPEG_SIGNATURE);
}
isSOFMarker(marker: number): boolean {
return (
marker === this.SOF_MARKERS.SOF0 ||
marker === this.SOF_MARKERS.SOF1 ||
marker === this.SOF_MARKERS.SOF2 ||
marker === this.SOF_MARKERS.SOF3
);
}
isNonSOFMarker(marker: number): boolean {
return this.NON_SOF_MARKERS.includes(marker);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise<ImageSize | undefined>((resolve, reject) => {
const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整
let buffer = Buffer.alloc(0);
let offset = 0;
let found = false;
// 处理错误
stream.on('error', (err) => {
stream.destroy();
reject(err);
});
// 处理数据块
stream.on('data', (chunk: Buffer | string) => {
// 追加新数据到缓冲区
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]);
offset = 0;
// 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配
const bufferSize = buffer.length;
const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数
// 从JPEG头部后开始扫描
while (offset < bufferSize - MIN_REQUIRED_BYTES) {
// 寻找FF标记
if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) {
const marker = buffer[offset + 1];
if (!marker) {
break;
}
// 跳过非SOF标记
if (this.isNonSOFMarker(marker)) {
offset += 2;
continue;
}
// 处理SOF标记 (包含尺寸信息)
if (this.isSOFMarker(marker)) {
// 确保缓冲区中有足够数据读取尺寸
if (offset + 9 < bufferSize) {
// 解析尺寸: FF XX YY YY PP HH HH WW WW ...
// XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
found = true;
stream.destroy();
resolve({ width, height });
return;
} else {
// 如果缓冲区内数据不够,保留当前位置等待更多数据
break;
}
}
}
offset++;
}
// 缓冲区管理: 如果处理了许多数据但没找到标记,
// 保留最后N字节用于跨块匹配丢弃之前的数据
if (offset > BUFFER_SIZE) {
const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况
if (offset > KEEP_BYTES) {
buffer = buffer.subarray(offset - KEEP_BYTES);
offset = KEEP_BYTES;
}
}
});
// 处理流结束
stream.on('end', () => {
if (!found) {
resolve(undefined);
}
});
});
}
}
// BMP解析器
class BmpParser implements ImageParser {
readonly type = ImageType.BMP;
// BMP 魔术头42 4D (BM)
private readonly BMP_SIGNATURE = [0x42, 0x4D];
canParse(buffer: Buffer): boolean {
return matchMagic(buffer, this.BMP_SIGNATURE);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(26) as Buffer;
if (!buf || buf.length < 26) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt32LE(18);
const height = buf.readUInt32LE(22);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// GIF解析器
class GifParser implements ImageParser {
readonly type = ImageType.GIF;
// GIF87a 魔术头47 49 46 38 37 61
private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61];
// GIF89a 魔术头47 49 46 38 39 61
private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
canParse(buffer: Buffer): boolean {
return (
matchMagic(buffer, this.GIF87A_SIGNATURE) ||
matchMagic(buffer, this.GIF89A_SIGNATURE)
);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
stream.once('error', reject);
stream.once('readable', () => {
const buf = stream.read(10) as Buffer;
if (!buf || buf.length < 10) {
return resolve(undefined);
}
if (this.canParse(buf)) {
const width = buf.readUInt16LE(6);
const height = buf.readUInt16LE(8);
resolve({ width, height });
} else {
resolve(undefined);
}
});
});
}
}
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
class WebpParser implements ImageParser {
readonly type = ImageType.WEBP;
// WEBP RIFF 头52 49 46 46 (RIFF)
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
// WEBP 魔术头57 45 42 50 (WEBP)
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
// WEBP 块头
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
canParse(buffer: Buffer): boolean {
return (
buffer.length >= 12 &&
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
);
}
isChunkType(buffer: Buffer, offset: number, chunkType: number[]): boolean {
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
}
async parseSize(stream: ReadStream): Promise<ImageSize | undefined> {
return new Promise((resolve, reject) => {
// 需要读取足够的字节来检测所有三种格式
const MAX_HEADER_SIZE = 32;
let totalBytes = 0;
let buffer = Buffer.alloc(0);
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
buffer = Buffer.concat([buffer, chunkBuffer]);
totalBytes += chunk.length;
// 检查是否有足够的字节进行格式检测
if (totalBytes >= MAX_HEADER_SIZE) {
stream.destroy();
// 检查基本的WEBP签名
if (!this.canParse(buffer)) {
return resolve(undefined);
}
// 检查chunk头部位于字节12-15
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
// VP8格式 - 标准WebP
// 宽度和高度在帧头中
const width = buffer.readUInt16LE(26) & 0x3FFF;
const height = buffer.readUInt16LE(28) & 0x3FFF;
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
// VP8L格式 - 无损WebP
// 1字节标记后是14位宽度和14位高度
const bits = buffer.readUInt32LE(21);
const width = 1 + (bits & 0x3FFF);
const height = 1 + ((bits >> 14) & 0x3FFF);
return resolve({ width, height });
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
// VP8X格式 - 扩展WebP
// 24位宽度和高度(减去1)
if (!buffer[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) {
return resolve(undefined);
}
const width = 1 + ((buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) & 0xFFFFFF);
const height = 1 + ((buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) & 0xFFFFFF);
return resolve({ width, height });
} else {
// 未知的WebP子格式
return resolve(undefined);
}
}
});
stream.on('end', () => {
// 如果没有读到足够的字节
if (totalBytes < MAX_HEADER_SIZE) {
resolve(undefined);
}
});
});
}
}
const parsers: ReadonlyArray<ImageParser> = [
new PngParser(),
new JpegParser(),
new BmpParser(),
new GifParser(),
new WebpParser(),
];
export async function detectImageType(filePath: string): Promise<ImageType> {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(filePath, {
highWaterMark: 64, // 优化读取buffer大小
start: 0,
end: 63
});
let buffer: Buffer | null = null;
stream.once('error', (err) => {
stream.destroy();
reject(err);
});
stream.once('readable', () => {
buffer = stream.read(64) as Buffer;
stream.destroy();
if (!buffer) {
return resolve(ImageType.UNKNOWN);
}
for (const parser of parsers) {
if (parser.canParse(buffer)) {
return resolve(parser.type);
}
}
resolve(ImageType.UNKNOWN);
});
stream.once('end', () => {
if (!buffer) {
resolve(ImageType.UNKNOWN);
}
});
});
}
export async function imageSizeFromFile(filePath: string): Promise<ImageSize | undefined> {
try {
// 先检测类型
const type = await detectImageType(filePath);
const parser = parsers.find(p => p.type === type);
if (!parser) {
return undefined;
}
// 用流式方式解析尺寸
const stream = fs.createReadStream(filePath);
try {
return await parser.parseSize(stream);
} catch (err) {
console.error(`解析图片尺寸出错: ${err}`);
return undefined;
} finally {
if (!stream.destroyed) {
stream.destroy();
}
}
} catch (err) {
console.error(`检测图片类型出错: ${err}`);
return undefined;
}
}
export async function imageSizeFallBack(
filePath: string,
fallback: ImageSize = {
width: 1024,
height: 1024,
}
): Promise<ImageSize> {
return await imageSizeFromFile(filePath) ?? fallback;
}

372
src/on-finished/index.ts Normal file
View File

@@ -0,0 +1,372 @@
/*!
* on-finished
* Copyright(c) 2013 Jonathan Ong
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* 定义消息对象接口
*/
interface Message {
socket?: Socket;
finished?: boolean;
complete?: boolean;
readable?: boolean;
upgrade?: boolean;
__onFinished?: Listener;
on?: (event: string, callback: (socket: Socket) => void) => void;
removeListener?: (event: string, callback: Function) => void;
assignSocket?: (socket: Socket) => void;
}
/**
* 定义Socket接口
*/
interface Socket {
writable: boolean;
readable: boolean;
on?: (event: string, callback: Function) => void;
removeListener?: (event: string, callback: Function) => void;
}
/**
* 事件处理器类型
*/
type EventHandler = (err?: Error) => void;
/**
* 事件元组: [对象, ...事件名称]
*/
type EventTuple = [unknown, ...string[]];
/**
* 取消函数类型
*/
interface CancelableListener {
cancel: () => void;
}
/**
* 监听器函数类型
*/
type ListenerCallback = (err: Error | null, msg: Message) => void;
/**
* 修改监听器接口,使其参数类型更通用
*/
interface Listener {
(err: Error | null | undefined): void;
queue: ListenerCallback[] | null;
}
/**
* 模块导出
* @public
*/
export { onFinished as default, isFinished };
/**
* 模块依赖
* @private
*/
const asyncHooks = tryRequireAsyncHooks();
/**
* 变量
* @private
*/
const defer = function(fn: ListenerCallback, err: Error | null, msg: Message): void {
setImmediate(fn, err, msg);
};
/**
* 实现 ee-first 功能:监听多个事件,当第一个事件触发时执行回调并移除所有监听器
* @param {Array<EventTuple>} stuff - 事件元组数组
* @param {EventHandler} done - 完成时的回调
* @returns {CancelableListener} 可取消的监听器对象
* @private
*/
function first(stuff: EventTuple[], done: EventHandler): CancelableListener {
if (!Array.isArray(stuff)) {
throw new TypeError('参数 "stuff" 必须是数组');
}
if (typeof done !== 'function') {
throw new TypeError('参数 "done" 必须是函数');
}
const cleanups: Function[] = [];
let finished = false;
// 处理所有传入的对象和事件
for (const tuple of stuff) {
if (!Array.isArray(tuple) || tuple.length < 2) {
throw new TypeError('每个事件元组必须是长度大于等于2的数组');
}
const obj = tuple[0];
if (!obj || typeof obj !== 'object') {
throw new TypeError('监听目标必须是对象');
}
// 使用类型断言确保obj有on和removeListener方法
const target = obj as {
on: (event: string, callback: Function) => void;
removeListener: (event: string, callback: Function) => void;
};
if (typeof target.on !== 'function' || typeof target.removeListener !== 'function') {
throw new TypeError('监听目标必须支持 on/removeListener 方法');
}
const events = tuple.slice(1);
// 为每个事件创建一个监听器
for (const event of events) {
if (typeof event !== 'string') {
throw new TypeError('事件名称必须是字符串');
}
const listener = createEventListener(target, event, cleanup);
cleanups.push(listener.cleanup);
target.on(event, listener.callback);
}
}
// 创建可取消的监听器对象
return {
cancel: cleanup
};
// 清理函数
function cleanup(): void {
if (finished) return;
finished = true;
// 执行所有清理函数
for (const cleanup of cleanups) {
try {
cleanup();
} catch (err) {
console.error('清理事件监听器时出错:', err);
}
}
}
// 创建单个事件监听器
function createEventListener(
obj: { on: Function; removeListener: Function },
event: string,
cleanup: Function
): {
callback: (arg?: unknown) => void;
cleanup: () => void;
} {
const callback = (arg?: unknown): void => {
cleanup();
done(arg instanceof Error ? arg : undefined);
};
return {
callback,
cleanup: () => {
obj.removeListener(event, callback);
}
};
}
}
/**
* 当响应结束时调用回调,用于资源清理
*
* @param {Message} msg
* @param {ListenerCallback} listener
* @return {Message}
* @public
*/
function onFinished(msg: Message, listener: ListenerCallback): Message {
if (!msg || typeof msg !== 'object') {
throw new TypeError('参数 "msg" 必须是对象');
}
if (typeof listener !== 'function') {
throw new TypeError('参数 "listener" 必须是函数');
}
if (isFinished(msg) !== false) {
defer(listener, null, msg);
return msg;
}
// 将监听器附加到消息
attachListener(msg, wrap(listener));
return msg;
}
/**
* 确定消息是否已完成
*
* @param {Message} msg
* @return {boolean | undefined}
* @public
*/
function isFinished(msg: Message): boolean | undefined {
if (!msg || typeof msg !== 'object') {
throw new TypeError('参数 "msg" 必须是对象');
}
const socket = msg.socket;
if (typeof msg.finished === 'boolean') {
// OutgoingMessage
return Boolean(msg.finished || (socket && !socket.writable));
}
if (typeof msg.complete === 'boolean') {
// IncomingMessage
return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable));
}
// 不确定状态
return undefined;
}
/**
* 将完成监听器附加到消息
*
* @param {Message} msg
* @param {Function} callback
* @private
*/
function attachFinishedListener(msg: Message, callback: (error?: Error) => void): void {
let eeMsg: CancelableListener;
let eeSocket: CancelableListener;
let finished = false;
function onFinish(error?: Error): void {
if (!eeMsg || !eeSocket) {
return; // 防御性检查
}
eeMsg.cancel();
eeSocket.cancel();
finished = true;
callback(error);
}
// 在第一个消息事件上完成
eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish);
function onSocket(socket: Socket): void {
// 移除监听器
if (msg.removeListener && typeof msg.removeListener === 'function') {
msg.removeListener('socket', onSocket);
}
if (finished) return;
if (eeMsg !== eeSocket) return;
if (!socket || typeof socket !== 'object') {
return; // 防御性检查
}
// 在第一个socket事件上完成
eeSocket = first([[socket, 'error', 'close']], onFinish);
}
if (msg.socket) {
// socket已分配
onSocket(msg.socket);
return;
}
// 等待socket分配
if (msg.on && typeof msg.on === 'function') {
msg.on('socket', onSocket);
}
}
/**
* 将监听器附加到消息
*
* @param {Message} msg
* @param {ListenerCallback} listener
* @private
*/
function attachListener(msg: Message, listener: ListenerCallback): void {
let attached = msg.__onFinished;
// 创建私有单一监听器和队列
if (!attached || !attached.queue) {
attached = msg.__onFinished = createListener(msg);
attachFinishedListener(msg, attached);
}
if (attached.queue) {
attached.queue.push(listener);
}
}
/**
* 在消息上创建监听器
*
* @param {Message} msg
* @return {Listener}
* @private
*/
function createListener(msg: Message): Listener {
const listener: Listener = function listener(err: Error | null | undefined): void {
if (msg.__onFinished === listener) msg.__onFinished = undefined;
if (!listener.queue) return;
const queue = listener.queue;
listener.queue = null;
for (let i = 0; i < queue.length; i++) {
queue[i]?.(err || null, msg);
}
};
listener.queue = [];
return listener;
}
/**
* 尝试引入async_hooks
* @private
*/
function tryRequireAsyncHooks(): { AsyncResource?: typeof import('async_hooks').AsyncResource } {
try {
return require('async_hooks');
} catch (e) {
return {};
}
}
/**
* 如果可能用async resource包装函数
* @private
*/
function wrap(fn: ListenerCallback): ListenerCallback {
if (typeof fn !== 'function') {
throw new TypeError('参数必须是函数');
}
// Node.js 16+ 总是有 AsyncResource
if (!asyncHooks.AsyncResource) {
return fn;
}
// 创建匿名资源
const res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn');
// 返回绑定函数
return (res.runInAsyncScope.bind(res, fn, null) as ListenerCallback);
}

View File

@@ -15,7 +15,10 @@ export default class SetGroupWholeBan extends OneBotAction<Payload, null> {
async _handle(payload: Payload): Promise<null> {
const enable = payload.enable?.toString() !== 'false';
await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
let res = await this.core.apis.GroupApi.banGroup(payload.group_id.toString(), enable);
if (res.result !== 0) {
throw new Error(`SetGroupWholeBan failed: ${res.errMsg} ${res.result}`);
}
return null;
}
}

View File

@@ -174,9 +174,11 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
nickname: string,
}, dp: number = 0): Promise<{
finallySendElements: SendArkElement,
res_id?: string
res_id?: string,
deleteAfterSentFiles: string[],
} | null> {
const packetMsg: PacketMsg[] = [];
let delFiles: string[] = [];
for (const node of messageNodes) {
if (dp >= 3) {
this.core.context.logger.logWarn('转发消息深度超过3层将停止解析');
@@ -192,9 +194,11 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
}, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
} else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(OB11Data, msgPeer);
sendElements = sendElementsCreateReturn.sendElements;
delFiles.push(...sendElementsCreateReturn.deleteAfterSentFiles);
}
const packetMsgElements: rawMsgWithSendMsg = {
@@ -218,7 +222,8 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
if (msg) {
await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
let msgCache = await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
delFiles.push(...msgCache);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
@@ -234,6 +239,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsg(packetMsg, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt);
return {
deleteAfterSentFiles: delFiles,
finallySendElements: {
elementType: ElementType.ARK,
elementId: '',
@@ -255,7 +261,7 @@ export class SendMsgBase extends OneBotAction<OB11PostSendMsg, ReturnDataType> {
const res_id = uploadReturnData?.res_id;
const finallySendElements = uploadReturnData?.finallySendElements;
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], []).catch(() => undefined);
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], uploadReturnData.deleteAfterSentFiles || []).catch(() => undefined);
return { message: returnMsg ?? null, res_id: res_id! };
}

View File

@@ -85,9 +85,6 @@ export class OneBotMsgApi {
textElement: async element => {
if (element.atType === NTMsgAtType.ATTYPEUNKNOWN) {
let text = element.content;
if (!text.trim()) {
return null;
}
// 兼容 9.7.x 换行符
if (text.indexOf('\n') === -1 && text.indexOf('\r\n') === -1) {
text = text.replace(/\r/g, '\n');
@@ -100,7 +97,7 @@ export class OneBotMsgApi {
let qq: string = 'all';
if (element.atType !== NTMsgAtType.ATTYPEALL) {
const { atNtUid, atUid } = element;
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : atUid;
qq = !atUid || atUid === '0' ? await this.core.apis.UserApi.getUinByUidV2(atNtUid) : String(Number(atUid) >>> 0);
}
return {
type: OB11MessageDataType.at,
@@ -150,12 +147,31 @@ export class OneBotMsgApi {
};
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileUuid);
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
if (this.core.apis.PacketApi.available) {
let url;
try {
url = await this.core.apis.FileApi.getFileUrl(msg.chatType, msg.peerUid, element.fileUuid, element.file10MMd5)
} catch (error) {
url = '';
}
if (url) {
return {
type: OB11MessageDataType.file,
data: {
file: element.fileName,
file_id: element.fileUuid,
file_size: element.fileSize,
url: url,
},
}
}
}
return {
type: OB11MessageDataType.file,
data: {
file: element.fileName,
file_id: element.fileUuid,
file_size: element.fileSize,
file_size: element.fileSize
},
};
},
@@ -225,17 +241,13 @@ export class OneBotMsgApi {
},
replyElement: async (element, msg) => {
const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords);
const peer = {
chatType: msg.chatType,
peerUid: msg.peerUid,
guildId: '',
};
if (!records || !element.replyMsgTime || !element.senderUidStr) {
this.core.context.logger.logError('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
return null;
}
// 创建回复数据的通用方法
const createReplyData = (msgId: string): OB11MessageData => ({
type: OB11MessageDataType.reply,
data: {
@@ -243,48 +255,96 @@ export class OneBotMsgApi {
},
});
if (records.peerUin === '284840486' || records.peerUin === '1094950020') {
// 查找记录
const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords);
// 特定账号的特殊处理
if (records && (records.peerUin === '284840486' || records.peerUin === '1094950020')) {
return createReplyData(records.msgId);
}
let replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(peer, element.replayMsgSeq, records.msgTime, [element.senderUidStr])).msgList;
let replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError(
'筛选结果,筛选消息失败,将使用Fallback-1 Seq: ',
// 获取消息的通用方法组
const tryFetchMethods = async (msgSeq: string, senderUid?: string, msgTime?: string, msgRandom?: string): Promise<RawMessage | undefined> => {
try {
// 方法1通过序号和时间筛选
if (senderUid && msgTime) {
const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV2(
peer, msgSeq, msgTime, [senderUid]
)).msgList;
const replyMsg = msgRandom
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
if (replyMsg) return replyMsg;
this.core.context.logger.logWarn(`方法1查询失败序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
}
// 方法2直接通过序号获取
const replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(
peer, msgSeq, 1, true, true
)).msgList;
const replyMsg = msgRandom
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
if (replyMsg) return replyMsg;
this.core.context.logger.logWarn(`方法2查询失败序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
// 方法3另一种筛选方式
if (senderUid) {
const replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(
peer, msgSeq, [senderUid]
)).msgList;
const replyMsg = msgRandom
? replyMsgList.find(msg => msg.msgRandom === msgRandom)
: replyMsgList.find(msg => msg.msgSeq === msgSeq);
if (replyMsg) return replyMsg;
this.core.context.logger.logWarn(`方法3查询失败序号: ${msgSeq}, 消息数: ${replyMsgList.length}`);
}
return undefined;
} catch (error) {
this.core.context.logger.logError('查询回复消息出错', error);
return undefined;
}
};
// 有记录情况下,使用完整信息查询
if (records && element.replyMsgTime && element.senderUidStr) {
const replyMsg = await tryFetchMethods(
element.replayMsgSeq,
',消息长度:',
replyMsgList.length
element.senderUidStr,
records.msgTime,
records.msgRandom
);
replyMsgList = (await this.core.apis.MsgApi.getMsgsBySeqAndCount(peer, element.replayMsgSeq, 1, true, true)).msgList;
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
if (replyMsg) {
return createReplyData(replyMsg.msgId);
}
this.core.context.logger.logError('所有查找方法均失败,获取不到带记录的引用消息', element.replayMsgSeq);
} else {
// 旧版客户端或不完整记录的情况,也尝试使用相同流程
this.core.context.logger.logWarn('似乎是旧版客户端,尝试仅通过序号获取引用消息', element.replayMsgSeq);
const replyMsg = await tryFetchMethods(element.replayMsgSeq);
if (replyMsg) {
return createReplyData(replyMsg.msgId);
}
this.core.context.logger.logError('所有查找方法均失败,获取不到旧客户端的引用消息', element.replayMsgSeq);
}
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logWarn(
'筛选消息失败,将使用Fallback-2 Seq:',
element.replayMsgSeq,
',消息长度:',
replyMsgList.length
);
replyMsgList = (await this.core.apis.MsgApi.queryMsgsWithFilterExWithSeqV3(peer, element.replayMsgSeq, [element.senderUidStr])).msgList;
replyMsg = replyMsgList.find(msg => msg.msgRandom === records.msgRandom);
}
// 丢弃该消息段
if (!replyMsg || records.msgRandom !== replyMsg.msgRandom) {
this.core.context.logger.logError(
'最终筛选结果,筛选消息失败,获取不到引用的消息 Seq: ',
element.replayMsgSeq,
',消息长度:',
replyMsgList.length
);
return null;
}
return createReplyData(replyMsg.msgId);
return null;
},
videoElement: async (element, msg, elementWrapper) => {
const peer = {
chatType: msg.chatType,
@@ -331,7 +391,17 @@ export class OneBotMsgApi {
//开始兜底
if (!videoDownUrl) {
videoDownUrl = element.filePath;
if (this.core.apis.PacketApi.available) {
try {
videoDownUrl = await this.core.apis.FileApi.getVideoUrlPacket(msg.peerUid, element.fileUuid);
} catch (e) {
this.core.context.logger.logError('获取视频url失败', (e as Error).stack);
videoDownUrl = element.filePath;
}
} else {
videoDownUrl = element.filePath;
}
}
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
return {
@@ -351,6 +421,28 @@ export class OneBotMsgApi {
guildId: '',
};
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName);
let pttUrl = '';
if (this.core.apis.PacketApi.available) {
try {
pttUrl = await this.core.apis.FileApi.getPttUrl(msg.peerUid, element.fileUuid);
} catch (e) {
this.core.context.logger.logError('获取语音url失败', (e as Error).stack);
pttUrl = element.filePath;
}
} else {
pttUrl = element.filePath;
}
if (pttUrl) {
return {
type: OB11MessageDataType.voice,
data: {
file: fileCode,
path: element.filePath,
url: pttUrl,
file_size: element.fileSize,
},
}
}
return {
type: OB11MessageDataType.voice,
data: {

View File

@@ -334,7 +334,7 @@ export class NapCatOneBot11Adapter {
for (let i = 0; i < reqs.unreadNums; i++) {
const req = reqs.buddyReqs[i];
if (!req) continue;
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM)) {
if (!!req.isInitiator || (req.isDecide && req.reqType !== BuddyReqType.KMEINITIATORWAITPEERCONFIRM) || !req.isUnread) {
continue;
}
try {
@@ -352,7 +352,6 @@ export class NapCatOneBot11Adapter {
}
}
};
this.context.session
.getBuddyService()
.addKernelBuddyListener(proxiedListenerOf(buddyListener, this.context.logger));

View File

@@ -39,8 +39,11 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
wsClient.close();
return;
}
//鉴权
this.authorize(this.config.token, wsClient, wsReq);
// 鉴权 close 不会立刻销毁 当前返回可避免挂载message事件 close 并未立刻关闭 而是存在timer操作后关闭
// 引发高危漏洞
if (!this.authorize(this.config.token, wsClient, wsReq)) {
return;
}
const paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
if (!isApiConnect) {
@@ -145,15 +148,16 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
}
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
if (!token || token.length == 0) return;//客户端未设置密钥
if (!token || token.length == 0) return true;//客户端未设置密钥
const QueryClientToken = urlParse.parse(wsReq?.url || '', true).query['access_token'];
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
if (ClientToken === token) {
return;
return true;
}
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
wsClient.close();
return false;
}
private checkStateAndReply<T>(data: T, wsClient: WebSocket) {

View File

@@ -1,6 +1,7 @@
import { LogWrapper } from '@/common/log';
import * as net from 'net';
import * as process from 'process';
import { Writable } from 'stream';
/**
* 连接到命名管道并重定向stdout
@@ -11,7 +12,6 @@ import * as process from 'process';
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: () => { } });
}
@@ -25,12 +25,50 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
}, timeoutMs);
try {
let originalStdoutWrite = process.stdout.write.bind(process.stdout);
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
const pipeSocket = net.connect(pipePath, () => {
// 清除超时
clearTimeout(timeoutId);
// 优化网络性能设置
pipeSocket.setNoDelay(true); // 减少延迟
// 设置更高的高水位线,允许更多数据缓冲
logger.log(`[StdOut] 已重定向到命名管道: ${pipePath}`);
// 创建拥有更优雅背压处理的 Writable 流
const pipeWritable = new Writable({
highWaterMark: 1024 * 64, // 64KB 高水位线
write(chunk, encoding, callback) {
if (!pipeSocket.writable) {
// 如果管道不可写退回到原始stdout
logger.log('[StdOut] 管道不可写,回退到控制台输出');
return originalStdoutWrite(chunk, encoding, callback);
}
// 尝试写入数据到管道
const canContinue = pipeSocket.write(chunk, encoding, () => {
// 数据已被发送或放入内部缓冲区
});
if (canContinue) {
// 如果返回true表示可以继续写入更多数据
// 立即通知写入流可以继续
process.nextTick(callback);
} else {
// 如果返回false表示内部缓冲区已满
// 等待drain事件再恢复写入
pipeSocket.once('drain', () => {
callback();
});
}
// 明确返回true表示写入已处理
return true;
}
});
// 重定向stdout
process.stdout.write = (
chunk: any,
encoding?: BufferEncoding | (() => void),
@@ -40,8 +78,11 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
cb = encoding;
encoding = undefined;
}
return pipeSocket.write(chunk, encoding as BufferEncoding, cb);
// 使用优化的writable流处理写入
return pipeWritable.write(chunk, encoding as BufferEncoding, cb as () => void);
};
// 提供断开连接的方法
const disconnect = () => {
process.stdout.write = originalStdoutWrite;
@@ -53,6 +94,7 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
resolve({ disconnect });
});
// 管道错误处理
pipeSocket.on('error', (err) => {
clearTimeout(timeoutId);
process.stdout.write = originalStdoutWrite;
@@ -60,11 +102,18 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
reject(err);
});
// 管道关闭处理
pipeSocket.on('end', () => {
process.stdout.write = originalStdoutWrite;
logger.log('命名管道连接已关闭');
});
// 确保在连接意外关闭时恢复stdout
pipeSocket.on('close', () => {
process.stdout.write = originalStdoutWrite;
logger.log('命名管道连接已关闭');
});
} catch (error) {
clearTimeout(timeoutId);
logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);

View File

@@ -122,7 +122,9 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 /webui
app.use('/webui', express.static(pathWrapper.staticPath));
app.use('/webui', express.static(pathWrapper.staticPath, {
maxAge: '1d'
}));
// 初始化WebSocket服务器
const sslCerts = await checkCertificates(logger);
const isHttps = !!sslCerts;

View File

@@ -1,5 +1,4 @@
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import {
ListFilesHandler,
CreateDirHandler,
@@ -20,15 +19,15 @@ import {
const router = Router();
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分钟内
max: 60, // 最大60个请求
validate: {
xForwardedForHeader: false,
},
});
// const apiLimiter = rateLimit({
// windowMs: 1 * 60 * 1000, // 1分钟内
// max: 60, // 最大60个请求
// validate: {
// xForwardedForHeader: false,
// },
// });
router.use(apiLimiter);
// router.use(apiLimiter);
router.get('/list', ListFilesHandler);
router.post('/mkdir', CreateDirHandler);

View File

@@ -3,6 +3,7 @@ import { defineConfig, PluginOption, UserConfig } from 'vite';
import { resolve } from 'path';
import nodeResolve from '@rollup/plugin-node-resolve';
import { builtinModules } from 'module';
import wasm from 'vite-plugin-wasm';
//依赖排除
const external = [
'silk-wasm',
@@ -21,6 +22,7 @@ if (process.env.NAPCAT_BUILDSYS == 'linux') {
}
const UniversalBaseConfigPlugin: PluginOption[] = [
wasm(),
cp({
targets: [
{ src: './manifest.json', dest: 'dist' },
@@ -44,6 +46,7 @@ const UniversalBaseConfigPlugin: PluginOption[] = [
];
const FrameworkBaseConfigPlugin: PluginOption[] = [
wasm(),
cp({
targets: [
{ src: './manifest.json', dest: 'dist' },
@@ -63,6 +66,7 @@ const FrameworkBaseConfigPlugin: PluginOption[] = [
];
const ShellBaseConfigPlugin: PluginOption[] = [
wasm(),
cp({
targets: [
{ src: './src/native/packet', dest: 'dist/moehoo', flatten: false },