diff --git a/README.md b/README.md index 6d3d7f25..95d7527e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ +NapCat
# 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) diff --git a/external/LiteLoaderWrapper.zip b/external/LiteLoaderWrapper.zip index 9861a5a1..e38ceabc 100644 Binary files a/external/LiteLoaderWrapper.zip and b/external/LiteLoaderWrapper.zip differ diff --git a/external/logo.png b/external/logo.png index a87d363e..839691c4 100644 Binary files a/external/logo.png and b/external/logo.png differ diff --git a/launcher/NapCatWinBootMain.exe b/launcher/NapCatWinBootMain.exe index 66f69e09..99a69b9e 100644 Binary files a/launcher/NapCatWinBootMain.exe and b/launcher/NapCatWinBootMain.exe differ diff --git a/launcher/qqnt.json b/launcher/qqnt.json index 74d47b93..f3db1840 100644 --- a/launcher/qqnt.json +++ b/launcher/qqnt.json @@ -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,27 +16,10 @@ "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", "eleArch": "x64" -} +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index 6a8caaa2..0b11828d 100644 --- a/manifest.json +++ b/manifest.json @@ -4,7 +4,7 @@ "name": "NapCatQQ", "slug": "NapCat.Framework", "description": "高性能的 OneBot 11 协议实现", - "version": "4.7.46", + "version": "4.7.60", "icon": "./logo.png", "authors": [ { diff --git a/package.json b/package.json index 00090fdc..5580e8b2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "napcat", "private": true, "type": "module", - "version": "4.7.46", + "version": "4.7.60", "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", diff --git a/src/common/download-ffmpeg.ts b/src/common/download-ffmpeg.ts index 1eb6dd27..25d4a798 100644 --- a/src/common/download-ffmpeg.ts +++ b/src/common/download-ffmpeg.ts @@ -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 diff --git a/src/common/version.ts b/src/common/version.ts index b798e50d..d9a8f0ba 100644 --- a/src/common/version.ts +++ b/src/common/version.ts @@ -1 +1 @@ -export const napCatVersion = '4.7.46'; +export const napCatVersion = '4.7.60'; diff --git a/src/core/apis/file.ts b/src/core/apis/file.ts index 31f3417e..8b0c56f6 100644 --- a/src/core/apis/file.ts +++ b/src/core/apis/file.ts @@ -27,6 +27,8 @@ 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 { imageSizeFromFile } from '@/image-size/fromFile'; export class NTQQFileApi { @@ -63,6 +65,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); @@ -325,6 +397,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) { diff --git a/src/core/apis/msg.ts b/src/core/apis/msg.ts index 5dd4d969..7c2d3614 100644 --- a/src/core/apis/msg.ts +++ b/src/core/apis/msg.ts @@ -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, diff --git a/src/core/external/appid.json b/src/core/external/appid.json index 9a7ff556..56742929 100644 --- a/src/core/external/appid.json +++ b/src/core/external/appid.json @@ -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" } } \ No newline at end of file diff --git a/src/core/external/offset.json b/src/core/external/offset.json index 48f84d17..dafe490b 100644 --- a/src/core/external/offset.json +++ b/src/core/external/offset.json @@ -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" } } \ No newline at end of file diff --git a/src/core/listeners/NodeIKernelBuddyListener.ts b/src/core/listeners/NodeIKernelBuddyListener.ts index edf29044..c85883ad 100644 --- a/src/core/listeners/NodeIKernelBuddyListener.ts +++ b/src/core/listeners/NodeIKernelBuddyListener.ts @@ -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 { } - onNickUpdated(arg: unknown): any { + onNickUpdated(_arg: unknown): any { } - onSmartInfos(arg: unknown): any { + onSmartInfos(_arg: unknown): any { } - onSpacePermissionInfos(arg: unknown): any { + onSpacePermissionInfos(_arg: unknown): any { } } diff --git a/src/core/packet/context/operationContext.ts b/src/core/packet/context/operationContext.ts index d89e8899..76866709 100644 --- a/src/core/packet/context/operationContext.ts +++ b/src/core/packet/context/operationContext.ts @@ -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) { + 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) { + 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) { 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) { + 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) { + 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) { - 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); diff --git a/src/core/packet/transformer/highway/DownloadGroupVideo.ts b/src/core/packet/transformer/highway/DownloadGroupVideo.ts new file mode 100644 index 00000000..22fe2e8e --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadGroupVideo.ts @@ -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 { + constructor() { + super(); + } + + build(groupUin: number, node: NapProtoEncodeStructType): 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(); diff --git a/src/core/packet/transformer/highway/DownloadPtt.ts b/src/core/packet/transformer/highway/DownloadPtt.ts new file mode 100644 index 00000000..41ab6c7e --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadPtt.ts @@ -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 { + constructor() { + super(); + } + + build(selfUid: string, node: NapProtoEncodeStructType): 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(); diff --git a/src/core/packet/transformer/highway/DownloadVideo.ts b/src/core/packet/transformer/highway/DownloadVideo.ts new file mode 100644 index 00000000..0731b258 --- /dev/null +++ b/src/core/packet/transformer/highway/DownloadVideo.ts @@ -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 { + constructor() { + super(); + } + + build(selfUid: string, node: NapProtoEncodeStructType): 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(); diff --git a/src/core/packet/transformer/highway/index.ts b/src/core/packet/transformer/highway/index.ts index 7ca56645..ef202112 100644 --- a/src/core/packet/transformer/highway/index.ts +++ b/src/core/packet/transformer/highway/index.ts @@ -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'; \ No newline at end of file diff --git a/src/core/packet/transformer/proto/misc/fileid.ts b/src/core/packet/transformer/proto/misc/fileid.ts new file mode 100644 index 00000000..71426f3f --- /dev/null +++ b/src/core/packet/transformer/proto/misc/fileid.ts @@ -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), +}; diff --git a/src/core/services/NodeIKernelGroupService.ts b/src/core/services/NodeIKernelGroupService.ts index babcad73..d072b394 100644 --- a/src/core/services/NodeIKernelGroupService.ts +++ b/src/core/services/NodeIKernelGroupService.ts @@ -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; getGroupShutUpMemberList(groupCode: string): Promise; diff --git a/src/core/services/NodeIKernelMsgService.ts b/src/core/services/NodeIKernelMsgService.ts index abd6c969..34dd8e0c 100644 --- a/src/core/services/NodeIKernelMsgService.ts +++ b/src/core/services/NodeIKernelMsgService.ts @@ -148,10 +148,11 @@ export interface NodeIKernelMsgService { msgList: RawMessage[] }>; - //@deprecated - getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise; + // getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true + getMsgs(peer: Peer & { privilegeFlag: number }, msgId: string, count: number, queryOrder: boolean): Promise; - //@deprecated getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise; diff --git a/src/core/types/msg.ts b/src/core/types/msg.ts index 9e542ad8..e949bbd9 100644 --- a/src/core/types/msg.ts +++ b/src/core/types/msg.ts @@ -508,7 +508,8 @@ export interface RawMessage { * 查询消息参数接口 */ export interface QueryMsgsParams { - chatInfo: Peer; + chatInfo: Peer & { privilegeFlag?: number }; + //searchFields: number; filterMsgType: Array<{ type: NTMsgType, subType: Array }>; filterSendersUid: string[]; filterMsgFromTime: string; diff --git a/src/core/types/notify.ts b/src/core/types/notify.ts index 52a1bafc..45fe4c9b 100644 --- a/src/core/types/notify.ts +++ b/src/core/types/notify.ts @@ -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 { diff --git a/src/onebot/action/group/SetGroupWholeBan.ts b/src/onebot/action/group/SetGroupWholeBan.ts index a4c84c44..3ff633ca 100644 --- a/src/onebot/action/group/SetGroupWholeBan.ts +++ b/src/onebot/action/group/SetGroupWholeBan.ts @@ -15,7 +15,10 @@ export default class SetGroupWholeBan extends OneBotAction { async _handle(payload: Payload): Promise { 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; } } diff --git a/src/onebot/action/msg/SendMsg.ts b/src/onebot/action/msg/SendMsg.ts index bb17807c..dcf96fac 100644 --- a/src/onebot/action/msg/SendMsg.ts +++ b/src/onebot/action/msg/SendMsg.ts @@ -174,9 +174,11 @@ export class SendMsgBase extends OneBotAction { 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 { 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 { 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 { 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 { 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! }; } diff --git a/src/onebot/api/msg.ts b/src/onebot/api/msg.ts index ffef73b0..88b5d988 100644 --- a/src/onebot/api/msg.ts +++ b/src/onebot/api/msg.ts @@ -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 => { + 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: { diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 4fb0089c..baa4406a 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -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)); diff --git a/src/onebot/network/websocket-server.ts b/src/onebot/network/websocket-server.ts index e56ff884..e96157bf 100644 --- a/src/onebot/network/websocket-server.ts +++ b/src/onebot/network/websocket-server.ts @@ -148,7 +148,7 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter 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);