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">
|
<div align="center">
|
||||||
|
|
||||||
# NapCat
|
# NapCat
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_Modern protocol-side framework implemented based on NTQQ._
|
_Modern protocol-side framework implemented based on NTQQ._
|
||||||
|
|
||||||
|
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
"name": "NapCatQQ",
|
"name": "NapCatQQ",
|
||||||
"slug": "NapCat.Framework",
|
"slug": "NapCat.Framework",
|
||||||
"description": "高性能的 OneBot 11 协议实现",
|
"description": "高性能的 OneBot 11 协议实现",
|
||||||
"version": "4.7.45",
|
"version": "4.5.50",
|
||||||
"icon": "./logo.png",
|
"icon": "./logo.png",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"name": "napcat",
|
"name": "napcat",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "4.7.45",
|
"version": "4.5.50",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
"build:universal": "npm run build:webui && vite build --mode universal || exit 1",
|
||||||
"build:framework": "npm run build:webui && vite build --mode framework || 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 { getFileTypeForSendType } from '../helper/msg';
|
||||||
import { FFmpegService } from '@/common/ffmpeg';
|
import { FFmpegService } from '@/common/ffmpeg';
|
||||||
import { rkeyDataType } from '../types/file';
|
import { rkeyDataType } from '../types/file';
|
||||||
|
import { NapProtoMsg } from '@napneko/nap-proto-core';
|
||||||
|
import { FileId } from '../packet/transformer/proto/misc/fileid';
|
||||||
|
|
||||||
export class NTQQFileApi {
|
export class NTQQFileApi {
|
||||||
context: InstanceContext;
|
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) {
|
async copyFile(filePath: string, destPath: string) {
|
||||||
await this.core.util.copyFile(filePath, destPath);
|
await this.core.util.copyFile(filePath, destPath);
|
||||||
|
@@ -71,6 +71,7 @@ export class NTQQMsgApi {
|
|||||||
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
|
async queryMsgsWithFilterExWithSeq(peer: Peer, msgSeq: string) {
|
||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
@@ -84,6 +85,7 @@ export class NTQQMsgApi {
|
|||||||
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', msgSeq, {
|
||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
|
//searchFields: 3,
|
||||||
filterSendersUid: SendersUid,
|
filterSendersUid: SendersUid,
|
||||||
filterMsgToTime: MsgTime,
|
filterMsgToTime: MsgTime,
|
||||||
filterMsgFromTime: MsgTime,
|
filterMsgFromTime: MsgTime,
|
||||||
@@ -100,6 +102,7 @@ export class NTQQMsgApi {
|
|||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: false,
|
isReverseOrder: false,
|
||||||
|
//searchFields: 3,
|
||||||
isIncludeCurrent: true,
|
isIncludeCurrent: true,
|
||||||
pageLimit: 1,
|
pageLimit: 1,
|
||||||
});
|
});
|
||||||
@@ -110,6 +113,7 @@ export class NTQQMsgApi {
|
|||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: true,
|
isReverseOrder: true,
|
||||||
isIncludeCurrent: true,
|
isIncludeCurrent: true,
|
||||||
@@ -128,6 +132,7 @@ export class NTQQMsgApi {
|
|||||||
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
|
chatInfo: peer,//此处为Peer 为关键查询参数 没有啥也没有 by mlik iowa
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: [],
|
filterSendersUid: [],
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgToTime: filterMsgToTime,
|
filterMsgToTime: filterMsgToTime,
|
||||||
filterMsgFromTime: filterMsgFromTime,
|
filterMsgFromTime: filterMsgFromTime,
|
||||||
isReverseOrder: false,
|
isReverseOrder: false,
|
||||||
@@ -142,6 +147,7 @@ export class NTQQMsgApi {
|
|||||||
chatInfo: peer,
|
chatInfo: peer,
|
||||||
filterMsgType: [],
|
filterMsgType: [],
|
||||||
filterSendersUid: SendersUid,
|
filterSendersUid: SendersUid,
|
||||||
|
//searchFields: 3,
|
||||||
filterMsgToTime: '0',
|
filterMsgToTime: '0',
|
||||||
filterMsgFromTime: '0',
|
filterMsgFromTime: '0',
|
||||||
isReverseOrder: true,
|
isReverseOrder: true,
|
||||||
|
@@ -124,6 +124,20 @@ export class PacketOperationContext {
|
|||||||
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
|
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>) {
|
async GetGroupImageUrl(groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>) {
|
||||||
const req = trans.DownloadGroupImage.build(groupUin, node);
|
const req = trans.DownloadGroupImage.build(groupUin, node);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
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}`;
|
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) {
|
async ImageOCR(imgUrl: string) {
|
||||||
const req = trans.ImageOCR.build(imgUrl);
|
const req = trans.ImageOCR.build(imgUrl);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
const resp = await this.context.client.sendOidbPacket(req, true);
|
||||||
@@ -154,7 +183,7 @@ export class PacketOperationContext {
|
|||||||
|
|
||||||
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
|
private async SendPreprocess(msg: PacketMsg[], groupUin: number = 0) {
|
||||||
const ps = msg.map((m) => {
|
const ps = msg.map((m) => {
|
||||||
return m.msg.map(async(e) => {
|
return m.msg.map(async (e) => {
|
||||||
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
|
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
|
||||||
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
|
this.context.logger.debug(`Cannot find reply element's targetElems, prepare to fetch it...`);
|
||||||
if (!e.targetPeer?.peerUid) {
|
if (!e.targetPeer?.peerUid) {
|
||||||
@@ -222,6 +251,7 @@ export class PacketOperationContext {
|
|||||||
const res = trans.DownloadGroupFile.parse(resp);
|
const res = trans.DownloadGroupFile.parse(resp);
|
||||||
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
|
async GetPrivateFileUrl(self_id: string, fileUUID: string, md5: string) {
|
||||||
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
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`;
|
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) {
|
async GetMiniAppAdaptShareInfo(param: MiniAppReqParams) {
|
||||||
const req = trans.GetMiniAppAdaptShareInfo.build(param);
|
const req = trans.GetMiniAppAdaptShareInfo.build(param);
|
||||||
const resp = await this.context.client.sendOidbPacket(req, true);
|
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 UploadPrivateVideo } from './UploadPrivateVideo';
|
||||||
export { default as DownloadImage } from './DownloadImage';
|
export { default as DownloadImage } from './DownloadImage';
|
||||||
export { default as DownloadGroupImage } from './DownloadGroupImage';
|
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[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
//@deprecated
|
// getMsgService/getMsgs { chatType: 2, peerUid: '975206796', privilegeFlag: 336068800 } 0 20 true
|
||||||
getMsgs(peer: Peer, msgId: string, count: unknown, queryOrder: boolean): Promise<unknown>;
|
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 & {
|
getMsgsIncludeSelf(peer: Peer, msgId: string, count: number, queryOrder: boolean): Promise<GeneralCallResult & {
|
||||||
msgList: RawMessage[]
|
msgList: RawMessage[]
|
||||||
}>;
|
}>;
|
||||||
|
@@ -508,7 +508,8 @@ export interface RawMessage {
|
|||||||
* 查询消息参数接口
|
* 查询消息参数接口
|
||||||
*/
|
*/
|
||||||
export interface QueryMsgsParams {
|
export interface QueryMsgsParams {
|
||||||
chatInfo: Peer;
|
chatInfo: Peer & { privilegeFlag?: number };
|
||||||
|
//searchFields: number;
|
||||||
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
filterMsgType: Array<{ type: NTMsgType, subType: Array<number> }>;
|
||||||
filterSendersUid: string[];
|
filterSendersUid: string[];
|
||||||
filterMsgFromTime: string;
|
filterMsgFromTime: string;
|
||||||
|
@@ -100,7 +100,7 @@ export class OneBotMsgApi {
|
|||||||
let qq: string = 'all';
|
let qq: string = 'all';
|
||||||
if (element.atType !== NTMsgAtType.ATTYPEALL) {
|
if (element.atType !== NTMsgAtType.ATTYPEALL) {
|
||||||
const { atNtUid, atUid } = element;
|
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 {
|
return {
|
||||||
type: OB11MessageDataType.at,
|
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.fileUuid);
|
||||||
FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
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 {
|
return {
|
||||||
type: OB11MessageDataType.file,
|
type: OB11MessageDataType.file,
|
||||||
data: {
|
data: {
|
||||||
file: element.fileName,
|
file: element.fileName,
|
||||||
file_id: element.fileUuid,
|
file_id: element.fileUuid,
|
||||||
file_size: element.fileSize,
|
file_size: element.fileSize
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -225,17 +244,13 @@ export class OneBotMsgApi {
|
|||||||
},
|
},
|
||||||
|
|
||||||
replyElement: async (element, msg) => {
|
replyElement: async (element, msg) => {
|
||||||
const records = msg.records.find(msgRecord => msgRecord.msgId === element?.sourceMsgIdInRecords);
|
|
||||||
const peer = {
|
const peer = {
|
||||||
chatType: msg.chatType,
|
chatType: msg.chatType,
|
||||||
peerUid: msg.peerUid,
|
peerUid: msg.peerUid,
|
||||||
guildId: '',
|
guildId: '',
|
||||||
};
|
};
|
||||||
if (!records || !element.replyMsgTime || !element.senderUidStr) {
|
|
||||||
this.core.context.logger.logError('似乎是旧版客户端,获取不到引用的消息', element.replayMsgSeq);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 创建回复数据的通用方法
|
||||||
const createReplyData = (msgId: string): OB11MessageData => ({
|
const createReplyData = (msgId: string): OB11MessageData => ({
|
||||||
type: OB11MessageDataType.reply,
|
type: OB11MessageDataType.reply,
|
||||||
data: {
|
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);
|
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(
|
const tryFetchMethods = async (msgSeq: string, senderUid?: string, msgTime?: string, msgRandom?: string): Promise<RawMessage | undefined> => {
|
||||||
'筛选结果,筛选消息失败,将使用Fallback-1 Seq: ',
|
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,
|
element.replayMsgSeq,
|
||||||
',消息长度:',
|
element.senderUidStr,
|
||||||
replyMsgList.length
|
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) {
|
return null;
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
videoElement: async (element, msg, elementWrapper) => {
|
videoElement: async (element, msg, elementWrapper) => {
|
||||||
const peer = {
|
const peer = {
|
||||||
chatType: msg.chatType,
|
chatType: msg.chatType,
|
||||||
@@ -331,7 +394,17 @@ export class OneBotMsgApi {
|
|||||||
|
|
||||||
//开始兜底
|
//开始兜底
|
||||||
if (!videoDownUrl) {
|
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);
|
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, element.fileUuid, element.fileName);
|
||||||
return {
|
return {
|
||||||
@@ -351,6 +424,28 @@ export class OneBotMsgApi {
|
|||||||
guildId: '',
|
guildId: '',
|
||||||
};
|
};
|
||||||
const fileCode = FileNapCatOneBotUUID.encode(peer, msg.msgId, elementWrapper.elementId, '', element.fileName);
|
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 {
|
return {
|
||||||
type: OB11MessageDataType.voice,
|
type: OB11MessageDataType.voice,
|
||||||
data: {
|
data: {
|
||||||
|
@@ -39,8 +39,11 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
wsClient.close();
|
wsClient.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//鉴权
|
// 鉴权 close 不会立刻销毁 当前返回可避免挂载message事件 close 并未立刻关闭 而是存在timer操作后关闭
|
||||||
this.authorize(this.config.token, wsClient, wsReq);
|
// 引发高危漏洞
|
||||||
|
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 paramUrl = wsReq.url?.indexOf('?') !== -1 ? wsReq.url?.substring(0, wsReq.url?.indexOf('?')) : wsReq.url;
|
||||||
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
|
const isApiConnect = paramUrl === '/api' || paramUrl === '/api/';
|
||||||
if (!isApiConnect) {
|
if (!isApiConnect) {
|
||||||
@@ -145,15 +148,16 @@ export class OB11WebSocketServerAdapter extends IOB11NetworkAdapter<WebsocketSer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private authorize(token: string | undefined, wsClient: WebSocket, wsReq: IncomingMessage) {
|
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 QueryClientToken = urlParse.parse(wsReq?.url || '', true).query['access_token'];
|
||||||
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
|
const HeaderClientToken = wsReq.headers.authorization?.split('Bearer ').pop() || '';
|
||||||
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
const ClientToken = typeof (QueryClientToken) === 'string' && QueryClientToken !== '' ? QueryClientToken : HeaderClientToken;
|
||||||
if (ClientToken === token) {
|
if (ClientToken === token) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
|
wsClient.send(JSON.stringify(OB11Response.res(null, 'failed', 1403, 'token验证失败')));
|
||||||
wsClient.close();
|
wsClient.close();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkStateAndReply<T>(data: T, wsClient: WebSocket) {
|
private checkStateAndReply<T>(data: T, wsClient: WebSocket) {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { LogWrapper } from '@/common/log';
|
import { LogWrapper } from '@/common/log';
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import * as process from 'process';
|
import * as process from 'process';
|
||||||
|
import { Writable } from 'stream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接到命名管道并重定向stdout
|
* 连接到命名管道并重定向stdout
|
||||||
@@ -25,12 +26,50 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
const pipeSocket = net.connect(pipePath, () => {
|
const pipeSocket = net.connect(pipePath, () => {
|
||||||
// 清除超时
|
// 清除超时
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// 优化网络性能设置
|
||||||
|
pipeSocket.setNoDelay(true); // 减少延迟
|
||||||
|
|
||||||
|
// 设置更高的高水位线,允许更多数据缓冲
|
||||||
|
|
||||||
logger.log(`[StdOut] 已重定向到命名管道: ${pipePath}`);
|
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 = (
|
process.stdout.write = (
|
||||||
chunk: any,
|
chunk: any,
|
||||||
encoding?: BufferEncoding | (() => void),
|
encoding?: BufferEncoding | (() => void),
|
||||||
@@ -40,8 +79,11 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
cb = encoding;
|
cb = encoding;
|
||||||
encoding = undefined;
|
encoding = undefined;
|
||||||
}
|
}
|
||||||
return pipeSocket.write(chunk, encoding as BufferEncoding, cb);
|
|
||||||
|
// 使用优化的writable流处理写入
|
||||||
|
return pipeWritable.write(chunk, encoding as BufferEncoding, cb as () => void);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提供断开连接的方法
|
// 提供断开连接的方法
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
@@ -53,6 +95,7 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
resolve({ disconnect });
|
resolve({ disconnect });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 管道错误处理
|
||||||
pipeSocket.on('error', (err) => {
|
pipeSocket.on('error', (err) => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
@@ -60,11 +103,18 @@ export function connectToNamedPipe(logger: LogWrapper, timeoutMs: number = 5000)
|
|||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 管道关闭处理
|
||||||
pipeSocket.on('end', () => {
|
pipeSocket.on('end', () => {
|
||||||
process.stdout.write = originalStdoutWrite;
|
process.stdout.write = originalStdoutWrite;
|
||||||
logger.log('命名管道连接已关闭');
|
logger.log('命名管道连接已关闭');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 确保在连接意外关闭时恢复stdout
|
||||||
|
pipeSocket.on('close', () => {
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
logger.log('命名管道连接已关闭');
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);
|
logger.log(`尝试连接命名管道 ${pipePath} 时发生异常:`, error);
|
||||||
|
Reference in New Issue
Block a user