mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-07-19 12:03:37 +00:00
Compare commits
20 Commits
protobuf-d
...
v4.7.51
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cfae4f5acd | ||
![]() |
de541e3249 | ||
![]() |
f5187c5c01 | ||
![]() |
9936279443 | ||
![]() |
2818773fd4 | ||
![]() |
b9293cbcd0 | ||
![]() |
5b9e44ddfc | ||
![]() |
1791accab7 | ||
![]() |
08081360f3 | ||
![]() |
e933a95e97 | ||
![]() |
4ef457fe6f | ||
![]() |
bd9cae8921 | ||
![]() |
303a74f8fd | ||
![]() |
0b7f126ce1 | ||
![]() |
308b5c027f | ||
![]() |
ed3abc4b43 | ||
![]() |
87ecb3b380 | ||
![]() |
7e31763a25 | ||
![]() |
c9df57d16a | ||
![]() |
3d0f8ee657 |
@@ -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
|
||||
|
||||

|
||||
|
||||
|
||||
_Modern protocol-side framework implemented based on NTQQ._
|
||||
|
||||
|
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
"name": "NapCatQQ",
|
||||
"slug": "NapCat.Framework",
|
||||
"description": "高性能的 OneBot 11 协议实现",
|
||||
"version": "4.7.45",
|
||||
"version": "4.5.50",
|
||||
"icon": "./logo.png",
|
||||
"authors": [
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"name": "napcat",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "4.7.45",
|
||||
"version": "4.5.50",
|
||||
"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",
|
||||
|
@@ -1 +1 @@
|
||||
export const napCatVersion = '4.7.45';
|
||||
export const napCatVersion = '4.5.50';
|
||||
|
@@ -28,6 +28,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';
|
||||
|
||||
export class NTQQFileApi {
|
||||
context: InstanceContext;
|
||||
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
50
src/core/packet/transformer/highway/DownloadGroupVideo.ts
Normal file
50
src/core/packet/transformer/highway/DownloadGroupVideo.ts
Normal 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();
|
51
src/core/packet/transformer/highway/DownloadPtt.ts
Normal file
51
src/core/packet/transformer/highway/DownloadPtt.ts
Normal 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();
|
51
src/core/packet/transformer/highway/DownloadVideo.ts
Normal file
51
src/core/packet/transformer/highway/DownloadVideo.ts
Normal 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();
|
@@ -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';
|
6
src/core/packet/transformer/proto/misc/fileid.ts
Normal file
6
src/core/packet/transformer/proto/misc/fileid.ts
Normal 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),
|
||||
};
|
@@ -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[]
|
||||
}>;
|
||||
|
@@ -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;
|
||||
|
@@ -100,7 +100,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 +150,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 +244,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 +258,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 +394,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 +424,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: {
|
||||
|
@@ -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) {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { LogWrapper } from '@/common/log';
|
||||
import * as net from 'net';
|
||||
import * as process from 'process';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
/**
|
||||
* 连接到命名管道并重定向stdout
|
||||
@@ -25,12 +26,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 +79,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 +95,7 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
||||
resolve({ disconnect });
|
||||
});
|
||||
|
||||
// 管道错误处理
|
||||
pipeSocket.on('error', (err) => {
|
||||
clearTimeout(timeoutId);
|
||||
process.stdout.write = originalStdoutWrite;
|
||||
@@ -60,11 +103,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);
|
||||
|
Reference in New Issue
Block a user