mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2024-11-21 09:36:35 +00:00
refactor: automatically select the optimal packet backend
This commit is contained in:
parent
347ba5f354
commit
479b971b0c
@ -2,7 +2,6 @@ import * as crypto from 'crypto';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { ChatType, InstanceContext, NapCatCore } from '..';
|
import { ChatType, InstanceContext, NapCatCore } from '..';
|
||||||
import offset from '@/core/external/offset.json';
|
import offset from '@/core/external/offset.json';
|
||||||
import { PacketClient, RecvPacketData } from '@/core/packet/client';
|
|
||||||
import { PacketSession } from "@/core/packet/session";
|
import { PacketSession } from "@/core/packet/session";
|
||||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||||
import { NapProtoEncodeStructType, NapProtoDecodeStructType, NapProtoMsg } from '@/core/packet/proto/NapProto';
|
import { NapProtoEncodeStructType, NapProtoDecodeStructType, NapProtoMsg } from '@/core/packet/proto/NapProto';
|
||||||
@ -25,6 +24,7 @@ import { AIVoiceChatType, AIVoiceItemList } from "@/core/packet/entities/aiChat"
|
|||||||
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
|
import { OidbSvcTrpcTcp0X929B_0Resp, OidbSvcTrpcTcp0X929D_0Resp } from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||||
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
import { IndexNode, MsgInfo } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaReq";
|
||||||
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
import { NTV2RichMediaResp } from "@/core/packet/proto/oidb/common/Ntv2.RichMediaResp";
|
||||||
|
import { RecvPacketData } from "@/core/packet/client/client";
|
||||||
|
|
||||||
|
|
||||||
interface OffsetType {
|
interface OffsetType {
|
||||||
@ -40,7 +40,6 @@ export class NTQQPacketApi {
|
|||||||
context: InstanceContext;
|
context: InstanceContext;
|
||||||
core: NapCatCore;
|
core: NapCatCore;
|
||||||
logger: LogWrapper;
|
logger: LogWrapper;
|
||||||
serverUrl: string | undefined;
|
|
||||||
qqVersion: string | undefined;
|
qqVersion: string | undefined;
|
||||||
packetSession: PacketSession | undefined;
|
packetSession: PacketSession | undefined;
|
||||||
|
|
||||||
@ -50,9 +49,8 @@ export class NTQQPacketApi {
|
|||||||
this.logger = core.context.logger;
|
this.logger = core.context.logger;
|
||||||
this.packetSession = undefined;
|
this.packetSession = undefined;
|
||||||
const config = this.core.configLoader.configData;
|
const config = this.core.configLoader.configData;
|
||||||
if (config && config.packetServer && config.packetServer.length > 0) {
|
if (config) {
|
||||||
const serverUrl = this.core.configLoader.configData.packetServer ?? '127.0.0.1:8086';
|
this.InitSendPacket(this.context.basicInfoWrapper.getFullQQVesion())
|
||||||
this.InitSendPacket(serverUrl, this.context.basicInfoWrapper.getFullQQVesion())
|
|
||||||
.then()
|
.then()
|
||||||
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
|
.catch(this.core.context.logger.logError.bind(this.core.context.logger));
|
||||||
} else {
|
} else {
|
||||||
@ -64,17 +62,14 @@ export class NTQQPacketApi {
|
|||||||
return this.packetSession?.client.available ?? false;
|
return this.packetSession?.client.available ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async InitSendPacket(serverUrl: string, qqversion: string) {
|
async InitSendPacket(qqversion: string) {
|
||||||
this.serverUrl = serverUrl;
|
|
||||||
this.qqVersion = qqversion;
|
this.qqVersion = qqversion;
|
||||||
const offsetTable: OffsetType = offset;
|
const table = typedOffset[qqversion + '-' + os.arch()];
|
||||||
const table = offsetTable[qqversion + '-' + os.arch()];
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
this.logger.logError('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
|
this.logger.logError('PacketServer Offset table not found for QQVersion: ', qqversion + '-' + os.arch());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const url = 'ws://' + this.serverUrl + '/ws';
|
this.packetSession = new PacketSession(this.core);
|
||||||
this.packetSession = new PacketSession(this.core.context.logger, new PacketClient(url, this.core));
|
|
||||||
const cb = () => {
|
const cb = () => {
|
||||||
if (this.packetSession && this.packetSession.client) {
|
if (this.packetSession && this.packetSession.client) {
|
||||||
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
|
this.packetSession.client.init(process.pid, table.recv, table.send).then().catch(this.logger.logError.bind(this.logger));
|
||||||
|
@ -1,183 +0,0 @@
|
|||||||
import { LogWrapper } from "@/common/log";
|
|
||||||
import { LRUCache } from "@/common/lru-cache";
|
|
||||||
import WebSocket, { Data } from "ws";
|
|
||||||
import crypto, { createHash } from "crypto";
|
|
||||||
import { NapCatCore } from "@/core";
|
|
||||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
|
||||||
|
|
||||||
export interface RecvPacket {
|
|
||||||
type: string, // 仅recv
|
|
||||||
trace_id_md5?: string,
|
|
||||||
data: RecvPacketData
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecvPacketData {
|
|
||||||
seq: number
|
|
||||||
cmd: string
|
|
||||||
hex_data: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PacketClient {
|
|
||||||
private websocket: WebSocket | undefined;
|
|
||||||
private isConnected: boolean = false;
|
|
||||||
private reconnectAttempts: number = 0;
|
|
||||||
private readonly maxReconnectAttempts: number = 60;//现在暂时不可配置
|
|
||||||
private readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
|
||||||
private readonly clientUrl: string = '';
|
|
||||||
readonly napCatCore: NapCatCore;
|
|
||||||
private readonly logger: LogWrapper;
|
|
||||||
|
|
||||||
constructor(url: string, core: NapCatCore) {
|
|
||||||
this.clientUrl = url;
|
|
||||||
this.napCatCore = core;
|
|
||||||
this.logger = core.context.logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
get available(): boolean {
|
|
||||||
return this.isConnected && this.websocket !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private randText(len: number) {
|
|
||||||
let text = '';
|
|
||||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(cb: any): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
|
||||||
this.websocket = new WebSocket(this.clientUrl);
|
|
||||||
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
|
||||||
this.isConnected = true;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
|
|
||||||
cb();
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onerror = (error) => {
|
|
||||||
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
|
||||||
reject(new Error(`${error.message}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onmessage = (event) => {
|
|
||||||
// const message = JSON.parse(event.data.toString());
|
|
||||||
// console.log("Received message:", message);
|
|
||||||
this.handleMessage(event.data).then().catch();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onclose = () => {
|
|
||||||
this.isConnected = false;
|
|
||||||
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
|
||||||
this.attemptReconnect(cb);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private attemptReconnect(cb: any): void {
|
|
||||||
try {
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.connect(cb).catch((error) => {
|
|
||||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
|
|
||||||
});
|
|
||||||
}, 5000 * this.reconnectAttempts);
|
|
||||||
} else {
|
|
||||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
|
||||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
|
||||||
if (!this.isConnected || !this.websocket) {
|
|
||||||
throw new Error("WebSocket is not connected");
|
|
||||||
}
|
|
||||||
const initMessage = {
|
|
||||||
action: 'init',
|
|
||||||
pid: pid,
|
|
||||||
recv: recv,
|
|
||||||
send: send
|
|
||||||
};
|
|
||||||
this.websocket.send(JSON.stringify(initMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
|
||||||
}): Promise<RecvPacketData> {
|
|
||||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
|
||||||
if (!this.isConnected || !this.websocket) {
|
|
||||||
throw new Error("WebSocket is not connected");
|
|
||||||
}
|
|
||||||
const commandMessage = {
|
|
||||||
action: 'send',
|
|
||||||
cmd: cmd,
|
|
||||||
data: data,
|
|
||||||
trace_id: trace_id
|
|
||||||
};
|
|
||||||
this.websocket.send(JSON.stringify(commandMessage));
|
|
||||||
if (rsp) {
|
|
||||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
resolve(json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
|
||||||
sendcb(json);
|
|
||||||
if (!rsp) {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
resolve(json);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const timeoutHandle = setTimeout(() => {
|
|
||||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
|
||||||
}, timeout);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleMessage(message: Data): Promise<void> {
|
|
||||||
try {
|
|
||||||
const json: RecvPacket = JSON.parse(message.toString());
|
|
||||||
const trace_id_md5 = json.trace_id_md5;
|
|
||||||
const action = json?.type ?? 'init';
|
|
||||||
const event = this.cb.get(trace_id_md5 + action);
|
|
||||||
if (event) {
|
|
||||||
await event(json.data);
|
|
||||||
}
|
|
||||||
//console.log("Received message:", json);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendPacket(cmd: string, data: PacketHexStr, rsp = false): Promise<RecvPacketData> {
|
|
||||||
// wtfk tx
|
|
||||||
// 校验失败和异常 可能返回undefined
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.available) {
|
|
||||||
this.logger.logError('NapCat.Packet 未初始化!');
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const md5 = crypto.createHash('md5').update(data).digest('hex');
|
|
||||||
const trace_id = (this.randText(4) + md5 + data).slice(0, data.length / 2);
|
|
||||||
this.sendCommand(cmd, data, trace_id, rsp, 20000, async () => {
|
|
||||||
// await sleep(10);
|
|
||||||
await this.napCatCore.context.session.getMsgService().sendSsoCmdReqByContend(cmd, trace_id);
|
|
||||||
}).then((res) => resolve(res)).catch((e: Error) => reject(e));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
|
||||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,37 @@
|
|||||||
import { LogWrapper } from "@/common/log";
|
|
||||||
import { LRUCache } from "@/common/lru-cache";
|
import { LRUCache } from "@/common/lru-cache";
|
||||||
import crypto, { createHash } from "crypto";
|
|
||||||
import { NapCatCore } from "@/core";
|
import { NapCatCore } from "@/core";
|
||||||
|
import { LogWrapper } from "@/common/log";
|
||||||
|
import crypto, { createHash } from "crypto";
|
||||||
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
import { OidbPacket, PacketHexStr } from "@/core/packet/packer";
|
||||||
import path, { dirname } from "path";
|
import { NapCatConfig } from "@/core/helper/config";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import fs from "fs";
|
|
||||||
import { constants } from "os";
|
|
||||||
import { console } from "inspector";
|
|
||||||
|
|
||||||
export interface RecvPacketData {
|
export interface RecvPacket {
|
||||||
seq: number;
|
type: string, // 仅recv
|
||||||
cmd: string;
|
trace_id_md5?: string,
|
||||||
hex_data: string;
|
data: RecvPacketData
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NativePacketClient {
|
export interface RecvPacketData {
|
||||||
private isInit: boolean = false;
|
seq: number
|
||||||
private readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
cmd: string
|
||||||
readonly napCatCore: NapCatCore;
|
hex_data: string
|
||||||
private readonly logger: LogWrapper;
|
}
|
||||||
private supportedPlatforms = ['win32.x64'];
|
|
||||||
private MoeHooExport: any = { exports: {} };
|
|
||||||
|
|
||||||
constructor(core: NapCatCore) {
|
export abstract class PacketClient {
|
||||||
|
readonly napCatCore: NapCatCore;
|
||||||
|
protected readonly logger: LogWrapper;
|
||||||
|
protected readonly cb = new LRUCache<string, (json: RecvPacketData) => Promise<void>>(500); // trace_id-type callback
|
||||||
|
protected isAvailable: boolean = false;
|
||||||
|
protected config: NapCatConfig;
|
||||||
|
|
||||||
|
protected constructor(core: NapCatCore) {
|
||||||
this.napCatCore = core;
|
this.napCatCore = core;
|
||||||
this.logger = core.context.logger;
|
this.logger = core.context.logger;
|
||||||
|
this.config = core.configLoader.configData;
|
||||||
}
|
}
|
||||||
|
|
||||||
get available(): boolean {
|
get available(): boolean {
|
||||||
return this.isInit;
|
return this.isAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private randText(len: number): string {
|
private randText(len: number): string {
|
||||||
@ -41,55 +43,37 @@ export class NativePacketClient {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static create(core: NapCatCore): PacketClient {
|
||||||
|
throw new Error("Must be implemented by subclasses");
|
||||||
|
}
|
||||||
|
|
||||||
|
static compatibilityScore(logger: LogWrapper): number {
|
||||||
|
throw new Error("Must be implemented by subclasses");
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract init(pid: number, recv: string, send: string): Promise<void>;
|
||||||
|
|
||||||
|
abstract connect(cb: () => void): Promise<void>;
|
||||||
|
|
||||||
|
abstract sendCommandImpl(cmd: string, data: string, trace_id: string): void;
|
||||||
|
|
||||||
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
private async registerCallback(trace_id: string, type: string, callback: (json: RecvPacketData) => Promise<void>): Promise<void> {
|
||||||
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
this.cb.put(createHash('md5').update(trace_id).digest('hex') + type, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(pid: number, recv: string, send: string): Promise<void> {
|
private async sendCommand(cmd: string, data: string, trace_id: string, rsp: boolean = false, timeout: number = 20000, sendcb: (json: RecvPacketData) => void = () => {
|
||||||
const platform = process.platform + '.' + process.arch;
|
}): Promise<RecvPacketData> {
|
||||||
if (!this.supportedPlatforms.includes(platform)) {
|
|
||||||
throw new Error(`Unsupported platform: ${platform}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/moehoo.' + platform + '.node');
|
|
||||||
if (!fs.existsSync(moehoo_path)) {
|
|
||||||
throw new Error(`moehoo.${platform}.node not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
|
||||||
this.MoeHooExport.exports.InitHook(pid, recv, send, (type: number, uin: string, seq: number, cmd: string, hex_data: string) => {
|
|
||||||
const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv'));
|
|
||||||
if (callback) {
|
|
||||||
callback({ seq, cmd, hex_data });
|
|
||||||
} else {
|
|
||||||
this.logger.logError(`Callback not found for hex_data: ${hex_data}`);
|
|
||||||
}
|
|
||||||
console.log('type:', type, 'uin:', uin, 'seq:', seq, 'cmd:', cmd, 'hex_data:', hex_data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendCommand(
|
|
||||||
cmd: string,
|
|
||||||
data: string,
|
|
||||||
trace_id: string,
|
|
||||||
rsp: boolean = false,
|
|
||||||
timeout: number = 20000,
|
|
||||||
sendcb: (json: RecvPacketData) => void = () => { }
|
|
||||||
): Promise<RecvPacketData> {
|
|
||||||
return new Promise<RecvPacketData>((resolve, reject) => {
|
return new Promise<RecvPacketData>((resolve, reject) => {
|
||||||
if (!this.available) {
|
if (!this.isAvailable) {
|
||||||
throw new Error("MoeHoo is not Init");
|
throw new Error("WebSocket is not connected");
|
||||||
}
|
}
|
||||||
|
this.sendCommandImpl(cmd, data, trace_id);
|
||||||
this.MoeHooExport.exports.SendPacket(cmd, data, crypto.createHash('md5').update(trace_id).digest('hex'));
|
|
||||||
|
|
||||||
if (rsp) {
|
if (rsp) {
|
||||||
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
this.registerCallback(trace_id, 'recv', async (json: RecvPacketData) => {
|
||||||
clearTimeout(timeoutHandle);
|
clearTimeout(timeoutHandle);
|
||||||
resolve(json);
|
resolve(json);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
this.registerCallback(trace_id, 'send', async (json: RecvPacketData) => {
|
||||||
sendcb(json);
|
sendcb(json);
|
||||||
if (!rsp) {
|
if (!rsp) {
|
||||||
@ -97,7 +81,6 @@ export class NativePacketClient {
|
|||||||
resolve(json);
|
resolve(json);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const timeoutHandle = setTimeout(() => {
|
const timeoutHandle = setTimeout(() => {
|
||||||
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
reject(new Error(`sendCommand timed out after ${timeout} ms for ${cmd} with trace_id ${trace_id}`));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
@ -123,4 +106,4 @@ export class NativePacketClient {
|
|||||||
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
async sendOidbPacket(pkt: OidbPacket, rsp = false): Promise<RecvPacketData> {
|
||||||
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
return this.sendPacket(pkt.cmd, pkt.data, rsp);
|
||||||
}
|
}
|
||||||
}
|
}
|
67
src/core/packet/client/nativeClient.ts
Normal file
67
src/core/packet/client/nativeClient.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import crypto, { createHash } from "crypto";
|
||||||
|
import { NapCatCore } from "@/core";
|
||||||
|
import path, { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import fs from "fs";
|
||||||
|
import { console } from "inspector";
|
||||||
|
import { PacketClient } from "@/core/packet/client/client";
|
||||||
|
import { constants, platform, type } from "node:os";
|
||||||
|
import { LogWrapper } from "@/common/log";
|
||||||
|
|
||||||
|
export class NativePacketClient extends PacketClient {
|
||||||
|
static supportedPlatforms = ['win32.x64'];
|
||||||
|
private MoeHooExport: any = { exports: {} };
|
||||||
|
|
||||||
|
protected constructor(core: NapCatCore) {
|
||||||
|
super(core);
|
||||||
|
}
|
||||||
|
|
||||||
|
get available(): boolean {
|
||||||
|
return this.isAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compatibilityScore(logger: LogWrapper): number {
|
||||||
|
const platform = process.platform + '.' + process.arch;
|
||||||
|
if (!this.supportedPlatforms.includes(platform)) {
|
||||||
|
logger.logError(`[NativePacketClient] Unsupported platform: ${platform}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/moehoo.' + platform + '.node');
|
||||||
|
if (!fs.existsSync(moehoo_path)) {
|
||||||
|
logger.logError(`[NativePacketClient] Missing moehoo binary: ${moehoo_path}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(core: NapCatCore): NativePacketClient {
|
||||||
|
return new NativePacketClient(core);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||||
|
const platform = process.platform + '.' + process.arch;
|
||||||
|
const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './moehoo/moehoo.' + platform + '.node');
|
||||||
|
process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY);
|
||||||
|
this.MoeHooExport.exports.InitHook(pid, recv, send, (type: number, uin: string, seq: number, cmd: string, hex_data: string) => {
|
||||||
|
const callback = this.cb.get(createHash('md5').update(Buffer.from(hex_data, 'hex')).digest('hex') + (type === 0 ? 'send' : 'recv'));
|
||||||
|
if (callback) {
|
||||||
|
callback({ seq, cmd, hex_data });
|
||||||
|
} else {
|
||||||
|
this.logger.logError(`Callback not found for hex_data: ${hex_data}`);
|
||||||
|
}
|
||||||
|
// TODO: cannot use console.log here, fxxk tx
|
||||||
|
// Error [ERR_INSPECTOR_NOT_AVAILABLE]: Inspector is not available
|
||||||
|
// console.log('type:', type, 'uin:', uin, 'seq:', seq, 'cmd:', cmd, 'hex_data:', hex_data);
|
||||||
|
});
|
||||||
|
this.isAvailable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommandImpl(cmd: string, data: string, trace_id: string): void {
|
||||||
|
this.MoeHooExport.exports.SendPacket(cmd, data, crypto.createHash('md5').update(trace_id).digest('hex'));
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(cb: () => void): Promise<void> {
|
||||||
|
cb();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
113
src/core/packet/client/wsClient.ts
Normal file
113
src/core/packet/client/wsClient.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { Data, WebSocket } from "ws";
|
||||||
|
import { NapCatCore } from "@/core";
|
||||||
|
import { PacketClient, RecvPacket } from "@/core/packet/client/client";
|
||||||
|
import { LogWrapper } from "@/common/log";
|
||||||
|
|
||||||
|
export class wsPacketClient extends PacketClient {
|
||||||
|
private websocket: WebSocket | undefined;
|
||||||
|
private reconnectAttempts: number = 0;
|
||||||
|
private readonly maxReconnectAttempts: number = 60; // 现在暂时不可配置
|
||||||
|
private readonly clientUrl: string = '';
|
||||||
|
private clientUrlWrap: (url: string) => string = (url: string) => `ws://${url}/ws`;
|
||||||
|
|
||||||
|
protected constructor(core: NapCatCore) {
|
||||||
|
super(core);
|
||||||
|
this.clientUrl = this.clientUrlWrap(this.config.packetServer ?? '127.0.0.1:8086');
|
||||||
|
}
|
||||||
|
|
||||||
|
static compatibilityScore(logger: LogWrapper): number {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(core: NapCatCore): wsPacketClient {
|
||||||
|
return new wsPacketClient(core);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(cb: () => void): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
//this.logger.log.bind(this.logger)(`[Core] [Packet Server] Attempting to connect to ${this.clientUrl}`);
|
||||||
|
this.websocket = new WebSocket(this.clientUrl);
|
||||||
|
this.websocket.on('error', (err) => { }/*this.logger.logError.bind(this.logger)('[Core] [Packet Server] Error:', err.message)*/);
|
||||||
|
|
||||||
|
this.websocket.onopen = () => {
|
||||||
|
this.isAvailable = true;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.logger.log.bind(this.logger)(`[Core] [Packet Server] 已连接到 ${this.clientUrl}`);
|
||||||
|
cb();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onerror = (error) => {
|
||||||
|
//this.logger.logError.bind(this.logger)(`WebSocket error: ${error}`);
|
||||||
|
reject(new Error(`${error.message}`));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onmessage = (event) => {
|
||||||
|
// const message = JSON.parse(event.data.toString());
|
||||||
|
// console.log("Received message:", message);
|
||||||
|
this.handleMessage(event.data).then().catch();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.websocket.onclose = () => {
|
||||||
|
this.isAvailable = false;
|
||||||
|
//this.logger.logWarn.bind(this.logger)(`[Core] [Packet Server] Disconnected from ${this.clientUrl}`);
|
||||||
|
this.attemptReconnect(cb);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private attemptReconnect(cb: any): void {
|
||||||
|
try {
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connect(cb).catch((error) => {
|
||||||
|
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 尝试重连失败:${error.message}`);
|
||||||
|
});
|
||||||
|
}, 5000 * this.reconnectAttempts);
|
||||||
|
} else {
|
||||||
|
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] ${this.clientUrl} 已达到最大重连次数!`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.logError.bind(this.logger)(`[Core] [Packet Server] 重连时出错: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(pid: number, recv: string, send: string): Promise<void> {
|
||||||
|
if (!this.isAvailable || !this.websocket) {
|
||||||
|
throw new Error("WebSocket is not connected");
|
||||||
|
}
|
||||||
|
const initMessage = {
|
||||||
|
action: 'init',
|
||||||
|
pid: pid,
|
||||||
|
recv: recv,
|
||||||
|
send: send
|
||||||
|
};
|
||||||
|
this.websocket.send(JSON.stringify(initMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCommandImpl(cmd: string, data: string, trace_id: string) : void {
|
||||||
|
const commandMessage = {
|
||||||
|
action: 'send',
|
||||||
|
cmd: cmd,
|
||||||
|
data: data,
|
||||||
|
trace_id: trace_id
|
||||||
|
};
|
||||||
|
this.websocket!.send(JSON.stringify(commandMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleMessage(message: Data): Promise<void> {
|
||||||
|
try {
|
||||||
|
const json: RecvPacket = JSON.parse(message.toString());
|
||||||
|
const trace_id_md5 = json.trace_id_md5;
|
||||||
|
const action = json?.type ?? 'init';
|
||||||
|
const event = this.cb.get(trace_id_md5 + action);
|
||||||
|
if (event) {
|
||||||
|
await event(json.data);
|
||||||
|
}
|
||||||
|
//console.log("Received message:", json);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.logError.bind(this.logger)(`Error parsing message: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import { ChatType, Peer } from "@/core";
|
import { ChatType, Peer } from "@/core";
|
||||||
import { LogWrapper } from "@/common/log";
|
import { LogWrapper } from "@/common/log";
|
||||||
import { PacketClient } from "@/core/packet/client";
|
|
||||||
import { PacketPacker } from "@/core/packet/packer";
|
import { PacketPacker } from "@/core/packet/packer";
|
||||||
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
import { NapProtoMsg } from "@/core/packet/proto/NapProto";
|
||||||
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
import { HttpConn0x6ff_501Response } from "@/core/packet/proto/action/action";
|
||||||
@ -19,6 +18,7 @@ import { int32ip2str, oidbIpv4s2HighwayIpv4s } from "@/core/packet/highway/utils
|
|||||||
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
import { calculateSha1, calculateSha1StreamBytes, computeMd5AndLengthWithLimit } from "@/core/packet/utils/crypto/hash";
|
||||||
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
import { OidbSvcTrpcTcp0x6D6Response } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||||
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
import { OidbSvcTrpcTcp0XE37_800Response, OidbSvcTrpcTcp0XE37Response } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||||
|
import { PacketClient } from "@/core/packet/client/client";
|
||||||
|
|
||||||
export const BlockSize = 1024 * 1024;
|
export const BlockSize = 1024 * 1024;
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import { PacketMsg } from "@/core/packet/message/message";
|
|||||||
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
import { OidbSvcTrpcTcp0x6D6 } from "@/core/packet/proto/oidb/Oidb.0x6D6";
|
||||||
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
import { OidbSvcTrpcTcp0XE37_1200 } from "@/core/packet/proto/oidb/Oidb.0xE37_1200";
|
||||||
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
import { PacketMsgConverter } from "@/core/packet/message/converter";
|
||||||
import { PacketClient } from "@/core/packet/client";
|
|
||||||
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
|
import { OidbSvcTrpcTcp0XE37_1700 } from "@/core/packet/proto/oidb/Oidb.0xE37_1700";
|
||||||
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
import { OidbSvcTrpcTcp0XE37_800 } from "@/core/packet/proto/oidb/Oidb.0XE37_800";
|
||||||
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
import { OidbSvcTrpcTcp0XEB7 } from "./proto/oidb/Oidb.0xEB7";
|
||||||
@ -30,6 +29,7 @@ import { MiniAppReqParams } from "@/core/packet/entities/miniApp";
|
|||||||
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
import { MiniAppAdaptShareInfoReq } from "@/core/packet/proto/action/miniAppAdaptShareInfo";
|
||||||
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
import { AIVoiceChatType } from "@/core/packet/entities/aiChat";
|
||||||
import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929";
|
import { OidbSvcTrpcTcp0X929B_0, OidbSvcTrpcTcp0X929D_0 } from "@/core/packet/proto/oidb/Oidb.0x929";
|
||||||
|
import { PacketClient } from "@/core/packet/client/client";
|
||||||
|
|
||||||
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
export type PacketHexStr = string & { readonly hexNya: unique symbol };
|
||||||
|
|
||||||
|
@ -1,7 +1,19 @@
|
|||||||
import { PacketClient } from "@/core/packet/client";
|
|
||||||
import { PacketHighwaySession } from "@/core/packet/highway/session";
|
import { PacketHighwaySession } from "@/core/packet/highway/session";
|
||||||
import { LogWrapper } from "@/common/log";
|
import { LogWrapper } from "@/common/log";
|
||||||
import { PacketPacker } from "@/core/packet/packer";
|
import { PacketPacker } from "@/core/packet/packer";
|
||||||
|
import { PacketClient } from "@/core/packet/client/client";
|
||||||
|
import { NativePacketClient } from "@/core/packet/client/nativeClient";
|
||||||
|
import { wsPacketClient } from "@/core/packet/client/wsClient";
|
||||||
|
import { NapCatCore } from "@/core";
|
||||||
|
|
||||||
|
type clientPriority = {
|
||||||
|
[key: number]: typeof PacketClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientPriority: clientPriority = {
|
||||||
|
10: NativePacketClient,
|
||||||
|
1: wsPacketClient,
|
||||||
|
};
|
||||||
|
|
||||||
export class PacketSession {
|
export class PacketSession {
|
||||||
readonly logger: LogWrapper;
|
readonly logger: LogWrapper;
|
||||||
@ -9,10 +21,29 @@ export class PacketSession {
|
|||||||
readonly packer: PacketPacker;
|
readonly packer: PacketPacker;
|
||||||
readonly highwaySession: PacketHighwaySession;
|
readonly highwaySession: PacketHighwaySession;
|
||||||
|
|
||||||
constructor(logger: LogWrapper, client: PacketClient) {
|
constructor(core: NapCatCore) {
|
||||||
this.logger = logger;
|
this.logger = core.context.logger;
|
||||||
this.client = client;
|
this.client = this.judgeClient(core);
|
||||||
this.packer = new PacketPacker(this.logger, this.client);
|
this.packer = new PacketPacker(this.logger, this.client);
|
||||||
this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer);
|
this.highwaySession = new PacketHighwaySession(this.logger, this.client, this.packer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private judgeClient(core: NapCatCore): PacketClient {
|
||||||
|
let selectedClient: typeof PacketClient | null = null;
|
||||||
|
let maxScore = -1;
|
||||||
|
for (const key in clientPriority) {
|
||||||
|
const priority = parseInt(key);
|
||||||
|
const ClientClass = clientPriority[priority];
|
||||||
|
const score = priority * ClientClass.compatibilityScore(core.context.logger);
|
||||||
|
if (score > maxScore) {
|
||||||
|
maxScore = score;
|
||||||
|
selectedClient = ClientClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!selectedClient) {
|
||||||
|
throw new Error("No compatible PacketClient found");
|
||||||
|
}
|
||||||
|
this.logger.log(`[Packet] 自动选择了: ${selectedClient.name}`);
|
||||||
|
return selectedClient.create(core);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user