Compare commits

...

13 Commits

Author SHA1 Message Date
手瓜一十雪
8243abaf0c feat: remote 2025-06-17 12:49:47 +08:00
手瓜一十雪
25be976fc9 feat: mock Wrapper 2025-06-17 12:48:37 +08:00
手瓜一十雪
a8d8a94309 fix 2025-06-17 12:41:38 +08:00
手瓜一十雪
eb9209cffb fix 2025-06-17 12:41:08 +08:00
手瓜一十雪
06bc761dd3 feat: 本地pipe分离测试 2025-06-17 11:58:31 +08:00
手瓜一十雪
8994d3af14 fix 2025-06-16 18:51:01 +08:00
手瓜一十雪
5eefd3dbe8 feat: 支持client 多注册 2025-06-16 17:41:49 +08:00
手瓜一十雪
2be014a9f2 feat: remove superjson 2025-06-16 17:36:27 +08:00
手瓜一十雪
ee4c9e95ad fix 2025-06-16 16:59:44 +08:00
手瓜一十雪
3e25172450 fix 2025-06-16 16:57:50 +08:00
手瓜一十雪
ac51c50046 fix 2025-06-16 16:55:09 +08:00
手瓜一十雪
a2cae1734b fix: 初步解耦 session到远程 2025-06-16 16:53:12 +08:00
手瓜一十雪
88e9caddfa feat: test-rpc-service 2025-06-14 17:42:31 +08:00
15 changed files with 2264 additions and 117 deletions

View File

@@ -41,6 +41,7 @@
"ajv": "^8.13.0",
"async-mutex": "^0.5.0",
"commander": "^13.0.0",
"compressing": "^1.10.1",
"cors": "^2.8.5",
"esbuild": "0.25.5",
"eslint": "^9.14.0",
@@ -52,18 +53,17 @@
"globals": "^16.0.0",
"json5": "^2.2.3",
"multer": "^2.0.1",
"napcat.protobuf": "^1.1.4",
"typescript": "^5.3.3",
"typescript-eslint": "^8.13.0",
"vite": "^6.0.1",
"vite-plugin-cp": "^6.0.0",
"vite-tsconfig-paths": "^5.1.0",
"napcat.protobuf": "^1.1.4",
"winston": "^3.17.0",
"compressing": "^1.10.1"
"winston": "^3.17.0"
},
"dependencies": {
"express": "^5.0.0",
"silk-wasm": "^3.6.1",
"ws": "^8.18.0"
}
}
}

View File

@@ -10,9 +10,9 @@ interface InternalMapKey {
checker: ((...args: any[]) => boolean) | undefined;
}
type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
export type EnsureFunc<T> = T extends (...args: any) => any ? T : never;
type FuncKeys<T> = Extract<
export type FuncKeys<T> = Extract<
{
[K in keyof T]: EnsureFunc<T[K]> extends never ? never : K;
}[keyof T],

View File

@@ -20,3 +20,23 @@ export function proxyHandlerOf(logger: LogWrapper) {
export function proxiedListenerOf<T extends object>(listener: T, logger: LogWrapper) {
return new Proxy<T>(listener, proxyHandlerOf(logger));
}
export function proxyHandlerOfWithoutLogger() {
return {
get(target: any, prop: any, receiver: any) {
if (typeof target[prop] === 'undefined') {
// 如果方法不存在返回一个函数这个函数调用existentMethod
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (..._args: unknown[]) => {
console.log(`${target.constructor.name} has no method ${prop}`);
};
}
// 如果方法存在,正常返回
return Reflect.get(target, prop, receiver);
},
};
}
export function proxiedListenerOfWithoutLogger<T extends object>(listener: T) {
return new Proxy<T>(listener, proxyHandlerOfWithoutLogger());
}

View File

@@ -142,7 +142,6 @@ export class NTQQMsgApi {
}
async queryFirstMsgBySender(peer: Peer, SendersUid: string[]) {
console.log(peer, SendersUid);
return await this.context.session.getMsgService().queryMsgsWithFilterEx('0', '0', '0', {
chatInfo: peer,
filterMsgType: [],

View File

@@ -30,6 +30,10 @@ import os from 'node:os';
import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners';
import { proxiedListenerOf } from '@/common/proxy-handler';
import { NTQQPacketApi } from './apis/packet';
import { handleServiceServerOnce, receiverServiceListener, ServiceMethodCommand } from '@/remote/service';
import { rpc_decode, rpc_encode } from '@/remote/serialize';
import { PipeClient, PipeServer } from '@/remote/pipe';
import { RemoteWrapperSession } from '@/remote/remoteSession';
export * from './wrapper';
export * from './types';
export * from './services';
@@ -97,9 +101,63 @@ export class NapCatCore {
constructor(context: InstanceContext, selfInfo: SelfInfo) {
this.selfInfo = selfInfo;
this.context = context;
this.util = this.context.wrapper.NodeQQNTWrapperUtil;
this.eventWrapper = new NTEventWrapper(context.session);
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath,NapcatConfigSchema);
// 管道服务端测试
let pipe_server = new PipeServer('//./pipe/napcat');
pipe_server.registerHandler(async (packet, helper) => {
if (packet.type !== 'event_request') {
return helper.error('Invalid packet type');
}
let event_rpc_data = rpc_decode<{ params: any[] }>(JSON.parse(packet.data));
let event_rpc_trace = packet.trace;
let event_rpc_command = packet.command as ServiceMethodCommand;
let event_rpc_result = await handleServiceServerOnce(event_rpc_command,
async (listenerCommand: string, ...args: any[]) => {
let listener_data = rpc_encode<{ params: any[] }>({ params: args });
helper.sendListenerCallback(listenerCommand, JSON.stringify(rpc_encode(listener_data)));
},
this.eventWrapper,
...event_rpc_data.params
);
return helper.sendEventResponse(event_rpc_trace, JSON.stringify(rpc_encode(event_rpc_result)));
});
pipe_server.start().then(() => {
this.context.logger.log('Pipe server started successfully');
let pipe_client = new PipeClient('//./pipe/napcat');
let trace_callback_map = new Map<string, (trace: string, data: any) => void>();
pipe_client.registerHandler(async (packet, _helper) => {
if (packet.type == 'event_response') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
trace_callback_map.get(packet.trace)?.(packet.trace, event_rpc_data);
} else if (packet.type == 'listener_callback') {
let event_rpc_data = rpc_decode<Array<any>>(JSON.parse(packet.data));
await receiverServiceListener(packet.command, ...event_rpc_data);
}
});
this.context.session = new RemoteWrapperSession(async (_serviceClient, serviceCommand, ...args) => {
let trace = crypto.randomUUID();
return await new Promise((resolve, _reject) => {
trace_callback_map.set(trace, (_trace, data) => {
//console.log('Received response for trace:', _trace, 'with data:', data);
resolve(data);
});
pipe_client.sendRequest(serviceCommand, JSON.stringify(rpc_encode({ params: args })), trace);
});
});
pipe_client.connect().then(() => {
this.context.logger.log('Pipe client connected successfully');
}).catch((e) => {
this.context.logger.logError('Pipe client connection failed: ' + e.message);
});
}).catch((e) => {
this.context.logger.logError('Pipe server start failed: ' + e.message);
});
this.configLoader = new NapCatConfigLoader(this, this.context.pathWrapper.configPath, NapcatConfigSchema);
this.apis = {
FileApi: new NTQQFileApi(this.context, this),
SystemApi: new NTQQSystemApi(this.context, this),
@@ -251,13 +309,13 @@ export async function genSessionConfig(
}
export interface InstanceContext {
readonly workingEnv: NapCatCoreWorkingEnv;
readonly wrapper: WrapperNodeApi;
readonly session: NodeIQQNTWrapperSession;
readonly logger: LogWrapper;
readonly loginService: NodeIKernelLoginService;
readonly basicInfoWrapper: QQBasicInfoWrapper;
readonly pathWrapper: NapCatPathWrapper;
session: NodeIQQNTWrapperSession;
workingEnv: NapCatCoreWorkingEnv;
wrapper: WrapperNodeApi;
logger: LogWrapper;
loginService: NodeIKernelLoginService;
basicInfoWrapper: QQBasicInfoWrapper;
pathWrapper: NapCatPathWrapper;
}
export interface StableNTApiWrapper {

View File

@@ -40,7 +40,6 @@ export class NodeIKernelBuddyListener {
}
onDelBatchBuddyInfos(_arg: unknown): any {
console.log('onDelBatchBuddyInfos not implemented', ...arguments);
}
onDoubtBuddyReqChange(_arg:

View File

@@ -1,71 +1,71 @@
import { User, UserDetailInfoListenerArg } from '@/core/types';
import { SelfStatusInfo, User, UserDetailInfoListenerArg } from '@/core/types';
export class NodeIKernelProfileListener {
onUserDetailInfoChanged(arg: UserDetailInfoListenerArg): void {
onUserDetailInfoChanged(_arg: UserDetailInfoListenerArg): void {
}
onProfileSimpleChanged(...args: unknown[]): any {
onProfileSimpleChanged(..._args: unknown[]): any {
}
onProfileDetailInfoChanged(profile: User): any {
onProfileDetailInfoChanged(_profile: User): any {
}
onStatusUpdate(...args: unknown[]): any {
onStatusUpdate(..._args: unknown[]): any {
}
onSelfStatusChanged(...args: unknown[]): any {
onSelfStatusChanged(_info: SelfStatusInfo): any {
}
onStrangerRemarkChanged(...args: unknown[]): any {
onStrangerRemarkChanged(..._args: unknown[]): any {
}
onMemberListChange(...args: unknown[]): any {
onMemberListChange(..._args: unknown[]): any {
}
onMemberInfoChange(...args: unknown[]): any {
onMemberInfoChange(..._args: unknown[]): any {
}
onGroupListUpdate(...args: unknown[]): any {
onGroupListUpdate(..._args: unknown[]): any {
}
onGroupAllInfoChange(...args: unknown[]): any {
onGroupAllInfoChange(..._args: unknown[]): any {
}
onGroupDetailInfoChange(...args: unknown[]): any {
onGroupDetailInfoChange(..._args: unknown[]): any {
}
onGroupConfMemberChange(...args: unknown[]): any {
onGroupConfMemberChange(..._args: unknown[]): any {
}
onGroupExtListUpdate(...args: unknown[]): any {
onGroupExtListUpdate(..._args: unknown[]): any {
}
onGroupNotifiesUpdated(...args: unknown[]): any {
onGroupNotifiesUpdated(..._args: unknown[]): any {
}
onGroupNotifiesUnreadCountUpdated(...args: unknown[]): any {
onGroupNotifiesUnreadCountUpdated(..._args: unknown[]): any {
}
onGroupMemberLevelInfoChange(...args: unknown[]): any {
onGroupMemberLevelInfoChange(..._args: unknown[]): any {
}
onGroupBulletinChange(...args: unknown[]): any {
onGroupBulletinChange(..._args: unknown[]): any {
}
}

View File

@@ -16,6 +16,16 @@ export * from './NodeIKernelDbToolsService';
export * from './NodeIKernelTipOffService';
export * from './NodeIKernelSearchService';
export * from './NodeIKernelCollectionService';
export * from './NodeIKernelAlbumService';
export * from './NodeIKernelECDHService';
export * from './NodeIKernelNodeMiscService';
export * from './NodeIKernelMsgBackupService';
export * from './NodeIKernelTianShuService';
export * from './NodeIKernelUnitedConfigService';
export * from './NodeIkernelTestPerformanceService';
export * from './NodeIKernelUixConvertService';
export * from './NodeIKernelMSFService';
export * from './NodeIKernelRecentContactService';
import type {
NodeIKernelAvatarService,
@@ -36,8 +46,19 @@ import type {
NodeIKernelTicketService,
NodeIKernelTipOffService,
} from '.';
import { NodeIKernelAlbumService } from './NodeIKernelAlbumService';
import { NodeIKernelECDHService } from './NodeIKernelECDHService';
import { NodeIKernelNodeMiscService } from './NodeIKernelNodeMiscService';
import { NodeIKernelMsgBackupService } from './NodeIKernelMsgBackupService';
import { NodeIKernelTianShuService } from './NodeIKernelTianShuService';
import { NodeIKernelUnitedConfigService } from './NodeIKernelUnitedConfigService';
import { NodeIkernelTestPerformanceService } from './NodeIkernelTestPerformanceService';
import { NodeIKernelUixConvertService } from './NodeIKernelUixConvertService';
import { NodeIKernelMSFService } from './NodeIKernelMSFService';
import { NodeIKernelRecentContactService } from './NodeIKernelRecentContactService';
export type ServiceNamingMapping = {
NodeIKernelAlbumService: NodeIKernelAlbumService;
NodeIKernelAvatarService: NodeIKernelAvatarService;
NodeIKernelBuddyService: NodeIKernelBuddyService;
NodeIKernelFileAssistantService: NodeIKernelFileAssistantService;
@@ -53,6 +74,15 @@ export type ServiceNamingMapping = {
NodeIKernelRichMediaService: NodeIKernelRichMediaService;
NodeIKernelDbToolsService: NodeIKernelDbToolsService;
NodeIKernelTipOffService: NodeIKernelTipOffService;
NodeIKernelSearchService: NodeIKernelSearchService,
NodeIKernelSearchService: NodeIKernelSearchService;
NodeIKernelCollectionService: NodeIKernelCollectionService;
NodeIKernelECDHService: NodeIKernelECDHService;
NodeIKernelNodeMiscService: NodeIKernelNodeMiscService;
NodeIKernelMsgBackupService: NodeIKernelMsgBackupService;
NodeIKernelTianShuService: NodeIKernelTianShuService;
NodeIKernelUnitedConfigService: NodeIKernelUnitedConfigService;
NodeIkernelTestPerformanceService: NodeIkernelTestPerformanceService;
NodeIKernelUixConvertService: NodeIKernelUixConvertService;
NodeIKernelMSFService: NodeIKernelMSFService;
NodeIKernelRecentContactService: NodeIKernelRecentContactService;
};

View File

@@ -1,82 +0,0 @@
# QRCode Terminal Edition [![Build Status][travis-ci-img]][travis-ci-url]
> Going where no QRCode has gone before.
![Basic Example][basic-example-img]
# Node Library
## Install
Can be installed with:
$ npm install qrcode-terminal
and used:
var qrcode = require('qrcode-terminal');
## Usage
To display some data to the terminal just call:
qrcode.generate('This will be a QRCode, eh!');
You can even specify the error level (default is 'L'):
qrcode.setErrorLevel('Q');
qrcode.generate('This will be a QRCode with error level Q!');
If you don't want to display to the terminal but just want to string you can provide a callback:
qrcode.generate('http://github.com', function (qrcode) {
console.log(qrcode);
});
If you want to display small output, provide `opts` with `small`:
qrcode.generate('This will be a small QRCode, eh!', {small: true});
qrcode.generate('This will be a small QRCode, eh!', {small: true}, function (qrcode) {
console.log(qrcode)
});
# Command-Line
## Install
$ npm install -g qrcode-terminal
## Usage
$ qrcode-terminal --help
$ qrcode-terminal 'http://github.com'
$ echo 'http://github.com' | qrcode-terminal
# Support
- OS X
- Linux
- Windows
# Server-side
[node-qrcode][node-qrcode-url] is a popular server-side QRCode generator that
renders to a `canvas` object.
# Developing
To setup the development envrionment run `npm install`
To run tests run `npm test`
# Contributers
Gord Tanner <gtanner@gmail.com>
Micheal Brooks <michael@michaelbrooks.ca>
[travis-ci-img]: https://travis-ci.org/gtanner/qrcode-terminal.png
[travis-ci-url]: https://travis-ci.org/gtanner/qrcode-terminal
[basic-example-img]: https://raw.github.com/gtanner/qrcode-terminal/master/example/basic.png
[node-qrcode-url]: https://github.com/soldair/node-qrcode

536
src/remote/pipe.ts Normal file
View File

@@ -0,0 +1,536 @@
import * as net from 'net';
import { randomUUID } from 'crypto';
import { EventEmitter } from 'events';
export interface Packet<T = any> {
command: string;
trace: string;
data: T;
type: 'listener_callback' | 'event_response' | 'event_request' | 'default';
}
// 协议常量
const PROTOCOL_MAGIC = 0x4E415043; // 'NAPC'
const PROTOCOL_VERSION = 0x01;
const HEADER_SIZE = 12;
const MAX_PACKET_SIZE = 16 * 1024 * 1024; // 降低到16MB
const BUFFER_HIGH_WATER_MARK = 2 * 1024 * 1024; // 2MB背压阈值
const BUFFER_LOW_WATER_MARK = 512 * 1024; // 512KB恢复阈值
// 高效缓冲区管理器
class BufferManager {
private buffers: Buffer[] = [];
private totalSize: number = 0;
private readOffset: number = 0;
private isHighWaterMark: boolean = false;
// 添加数据
append(data: Buffer): void {
this.buffers.push(data);
this.totalSize += data.length;
// 检查背压
if (!this.isHighWaterMark && this.totalSize > BUFFER_HIGH_WATER_MARK) {
this.isHighWaterMark = true;
}
}
// 消费数据
consume(length: number): Buffer {
if (length > this.available) {
throw new Error('消费长度超过可用数据');
}
const result = Buffer.allocUnsafe(length);
let resultOffset = 0;
let remaining = length;
while (remaining > 0 && this.buffers.length > 0) {
const currentBuffer = this.buffers[0];
if (!currentBuffer?.[0]) continue;
const availableInCurrent = currentBuffer.length - this.readOffset;
const toCopy = Math.min(remaining, availableInCurrent);
currentBuffer.copy(result, resultOffset, this.readOffset, this.readOffset + toCopy);
resultOffset += toCopy;
remaining -= toCopy;
this.readOffset += toCopy;
// 如果当前buffer用完了移除它
if (this.readOffset >= currentBuffer.length) {
this.buffers.shift();
this.readOffset = 0;
}
}
this.totalSize -= length;
// 检查是否可以恢复读取
if (this.isHighWaterMark && this.totalSize < BUFFER_LOW_WATER_MARK) {
this.isHighWaterMark = false;
}
return result;
}
// 预览数据(不消费)
peek(length: number): Buffer | null {
if (length > this.available) {
return null;
}
const result = Buffer.allocUnsafe(length);
let resultOffset = 0;
let remaining = length;
let bufferIndex = 0;
let currentReadOffset = this.readOffset;
while (remaining > 0 && bufferIndex < this.buffers.length) {
const currentBuffer = this.buffers[bufferIndex];
if (!currentBuffer) continue;
const availableInCurrent = currentBuffer.length - currentReadOffset;
const toCopy = Math.min(remaining, availableInCurrent);
currentBuffer.copy(result, resultOffset, currentReadOffset, currentReadOffset + toCopy);
resultOffset += toCopy;
remaining -= toCopy;
if (currentReadOffset + toCopy >= currentBuffer.length) {
bufferIndex++;
currentReadOffset = 0;
} else {
currentReadOffset += toCopy;
}
}
return result;
}
get available(): number {
return this.totalSize;
}
get shouldPause(): boolean {
return this.isHighWaterMark;
}
reset(): void {
this.buffers = [];
this.totalSize = 0;
this.readOffset = 0;
this.isHighWaterMark = false;
}
}
// 简化的数据包管理器
class PacketManager {
static pack(packet: Packet): Buffer {
const jsonStr = JSON.stringify(packet);
const jsonBuffer = Buffer.from(jsonStr, 'utf8');
if (jsonBuffer.length > MAX_PACKET_SIZE - HEADER_SIZE) {
throw new Error(`数据包过大: ${jsonBuffer.length}`);
}
const buffer = Buffer.allocUnsafe(HEADER_SIZE + jsonBuffer.length);
buffer.writeUInt32BE(PROTOCOL_MAGIC, 0);
buffer.writeUInt32BE(jsonBuffer.length, 4);
buffer.writeUInt32BE(PROTOCOL_VERSION, 8);
jsonBuffer.copy(buffer, HEADER_SIZE);
return buffer;
}
static unpack(bufferManager: BufferManager): Packet[] {
const packets: Packet[] = [];
while (bufferManager.available >= HEADER_SIZE) {
// 检查魔数
const header = bufferManager.peek(HEADER_SIZE);
if (!header) break;
const magic = header.readUInt32BE(0);
if (magic !== PROTOCOL_MAGIC) {
// 简单的同步恢复:跳过一个字节
bufferManager.consume(1);
continue;
}
const dataLength = header.readUInt32BE(4);
//const version = header.readUInt32BE(8);
// 基本验证
if (dataLength <= 0 || dataLength > MAX_PACKET_SIZE - HEADER_SIZE) {
bufferManager.consume(1);
continue;
}
// 检查完整包
const totalSize = HEADER_SIZE + dataLength;
if (bufferManager.available < totalSize) {
break;
}
// 消费完整包
bufferManager.consume(HEADER_SIZE);
const jsonBuffer = bufferManager.consume(dataLength);
try {
const packet = JSON.parse(jsonBuffer.toString('utf8')) as Packet;
if (this.isValidPacket(packet)) {
packets.push(packet);
}
} catch (error) {
console.error('JSON解析失败:', error);
}
}
return packets;
}
private static isValidPacket(packet: any): packet is Packet {
return packet &&
typeof packet.command === 'string' &&
typeof packet.trace === 'string' &&
packet.data !== undefined &&
['listener_callback', 'event_response', 'event_request', 'default'].includes(packet.type);
}
static createRequest<T = any>(command: string, data: T, trace?: string): Packet<T> {
return {
command,
trace: trace || randomUUID(),
data,
type: 'event_request'
};
}
static createResponse<T = any>(trace: string, data: T, command = ''): Packet<T> {
return {
command,
trace,
data,
type: 'event_response'
};
}
static createCallback<T = any>(command: string, data: T, trace?: string): Packet<T> {
return {
command,
trace: trace || randomUUID(),
data,
type: 'listener_callback'
};
}
}
// 响应助手类
class ResponseHelper {
private responseSent = false;
constructor(private socket: net.Socket, private trace: string, private command: string = '') { }
success<T = any>(data: T): void {
if (this.responseSent) return;
const response = PacketManager.createResponse(this.trace, data, this.command);
this.writePacket(response);
this.responseSent = true;
}
error(message: string, code = 500): void {
if (this.responseSent) return;
const response = PacketManager.createResponse(this.trace, { error: message, code }, this.command);
this.writePacket(response);
this.responseSent = true;
}
sendEventResponse<T = any>(trace: string, data: T): void {
const response = PacketManager.createResponse(trace, data, this.command);
this.writePacket(response);
}
sendListenerCallback<T = any>(command: string, data: T): void {
const callback = PacketManager.createCallback(command, data);
this.writePacket(callback);
}
private writePacket(packet: Packet): void {
console.log(`发送数据包: ${packet.command}, trace: ${packet.trace} (${packet.type}) `);
if (!this.socket.destroyed) {
const buffer = PacketManager.pack(packet);
this.socket.write(buffer);
}
}
get hasResponseSent(): boolean {
return this.responseSent;
}
}
// 带背压控制的Socket包装器
class ManagedSocket {
private bufferManager = new BufferManager();
private isPaused = false;
constructor(private socket: net.Socket, private onPacket: (packet: Packet) => void) {
this.setupSocket();
}
private setupSocket(): void {
this.socket.on('data', (chunk) => {
this.bufferManager.append(chunk);
// 背压控制
if (this.bufferManager.shouldPause && !this.isPaused) {
this.socket.pause();
this.isPaused = true;
console.warn('Socket暂停读取 - 缓冲区过大');
}
this.processPackets();
});
this.socket.on('drain', () => {
// 当socket的写缓冲区有空间时检查是否可以恢复读取
if (this.isPaused && !this.bufferManager.shouldPause) {
this.socket.resume();
this.isPaused = false;
console.log('Socket恢复读取');
}
});
}
private processPackets(): void {
try {
const packets = PacketManager.unpack(this.bufferManager);
packets.forEach(packet => this.onPacket(packet));
// 处理完包后检查是否可以恢复读取
if (this.isPaused && !this.bufferManager.shouldPause) {
this.socket.resume();
this.isPaused = false;
console.log('Socket恢复读取');
}
} catch (error) {
console.error('处理数据包失败:', error);
this.bufferManager.reset();
if (this.isPaused) {
this.socket.resume();
this.isPaused = false;
}
}
}
write(buffer: Buffer): boolean {
return this.socket.write(buffer);
}
destroy(): void {
this.socket.destroy();
}
get destroyed(): boolean {
return this.socket.destroyed;
}
}
type PacketHandler = (packet: Packet, helper: ResponseHelper) => Promise<any> | any;
// 简化的管道服务端
class PipeServer extends EventEmitter {
private server: net.Server;
private clients: Map<net.Socket, ManagedSocket> = new Map();
private handler: PacketHandler | null = null;
constructor(private pipeName: string) {
super();
this.server = net.createServer();
this.setupServer();
}
private setupServer(): void {
this.server.on('connection', (socket) => {
console.log('客户端连接');
const managedSocket = new ManagedSocket(socket, (packet) => {
this.handlePacket(packet, socket);
});
this.clients.set(socket, managedSocket);
socket.on('close', () => {
console.log('客户端断开');
this.clients.delete(socket);
});
socket.on('error', (error) => {
console.error('Socket错误:', error);
this.clients.delete(socket);
});
});
}
registerHandler(handler: PacketHandler): void {
this.handler = handler;
}
private async handlePacket(packet: Packet, socket: net.Socket): Promise<void> {
if (packet.type === 'event_response' || packet.type === 'listener_callback') {
this.emit(packet.type, packet);
return;
}
const helper = new ResponseHelper(socket, packet.trace, packet.command);
if (!this.handler) {
helper.error('未注册处理器');
return;
}
try {
const result = await this.handler(packet, helper);
if (result !== undefined && !helper.hasResponseSent) {
helper.success(result);
}
} catch (error) {
if (!helper.hasResponseSent) {
const message = error instanceof Error ? error.message : String(error);
helper.error(message);
}
}
}
async start(): Promise<void> {
return new Promise((resolve, reject) => {
this.server.listen(this.pipeName, () => {
console.log(`管道服务器启动: ${this.pipeName}`);
resolve();
});
this.server.on('error', reject);
});
}
async stop(): Promise<void> {
return new Promise((resolve) => {
this.clients.forEach((managedSocket) => managedSocket.destroy());
this.clients.clear();
this.server.close(() => {
console.log('管道服务器停止');
resolve();
});
});
}
broadcast<T = any>(command: string, data: T, type: Packet['type'] = 'default'): void {
const packet: Packet<T> = {
command,
trace: randomUUID(),
data,
type
};
const buffer = PacketManager.pack(packet);
this.clients.forEach((managedSocket) => {
if (!managedSocket.destroyed) {
managedSocket.write(buffer);
}
});
}
get clientCount(): number {
return this.clients.size;
}
}
// 简化的管道客户端
class PipeClient extends EventEmitter {
private socket: net.Socket | null = null;
private managedSocket: ManagedSocket | null = null;
private isConnected = false;
private handler: PacketHandler | null = null;
constructor(private pipeName: string) {
super();
}
registerHandler(handler: PacketHandler): void {
this.handler = handler;
}
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = net.createConnection(this.pipeName);
this.managedSocket = new ManagedSocket(this.socket, (packet) => {
this.handlePacket(packet);
});
this.socket.on('connect', () => {
console.log('连接到管道服务器');
this.isConnected = true;
resolve();
});
this.socket.on('close', () => {
console.log('与服务器断开连接');
this.isConnected = false;
this.emit('disconnect');
});
this.socket.on('error', (error) => {
console.error('Socket错误:', error);
this.isConnected = false;
reject(error);
});
});
}
private async handlePacket(packet: Packet): Promise<void> {
if (this.handler && this.socket) {
const helper = new ResponseHelper(this.socket, packet.trace, packet.command);
try {
await this.handler(packet, helper);
} catch (error) {
console.error('处理数据包失败:', error);
}
}
}
sendRequest<T = any>(command: string, data: T, trace?: string): void {
if (!this.isConnected || !this.managedSocket) {
throw new Error('未连接到服务器');
}
const packet = PacketManager.createRequest(command, data, trace);
const buffer = PacketManager.pack(packet);
this.managedSocket.write(buffer);
}
sendResponse<T = any>(trace: string, data: T, command = ''): void {
if (!this.isConnected || !this.managedSocket) {
throw new Error('未连接到服务器');
}
const packet = PacketManager.createResponse(trace, data, command);
const buffer = PacketManager.pack(packet);
this.managedSocket.write(buffer);
}
disconnect(): void {
if (this.managedSocket) {
this.managedSocket.destroy();
this.managedSocket = null;
}
this.socket = null;
this.isConnected = false;
}
get connected(): boolean {
return this.isConnected;
}
}
export { PipeServer, PipeClient, PacketManager, ResponseHelper, BufferManager };

109
src/remote/remoteSession.ts Normal file
View File

@@ -0,0 +1,109 @@
import { createRemoteServiceClient } from "@/remote/service";
import {
NodeIQQNTWrapperSession,
WrapperSessionInitConfig
} from "../core/wrapper";
import { NodeIKernelSessionListener } from "../core/listeners/NodeIKernelSessionListener";
import {
NodeIDependsAdapter,
NodeIDispatcherAdapter
} from "../core/adapters";
import { ServiceNamingMapping } from "@/core";
class RemoteServiceManager {
private services: Map<string, any> = new Map();
private handler;
constructor(handler: (client: any, listenerCommand: string, ...args: any[]) => Promise<any>) {
this.handler = handler;
}
private createRemoteService<T extends keyof ServiceNamingMapping>(
serviceName: T
): ServiceNamingMapping[T] {
if (this.services.has(serviceName)) {
return this.services.get(serviceName);
}
let serviceClient: any;
serviceClient = createRemoteServiceClient(serviceName, async (serviceCommand, ...args) => {
return await this.handler(serviceClient, serviceCommand, ...args);
});
this.services.set(serviceName, serviceClient.object);
return serviceClient.object;
}
getService<T extends keyof ServiceNamingMapping>(
serviceName: T
): ServiceNamingMapping[T] {
return this.createRemoteService(serviceName);
}
}
export class RemoteWrapperSession implements NodeIQQNTWrapperSession {
private serviceManager: RemoteServiceManager;
constructor(handler: (client: { object: keyof ServiceNamingMapping, receiverListener: (command: string, ...args: any[]) => void }, listenerCommand: string, ...args: any[]) => Promise<void>) {
this.serviceManager = new RemoteServiceManager(handler);
}
create(): RemoteWrapperSession {
return this;
}
init(
_wrapperSessionInitConfig: WrapperSessionInitConfig,
_nodeIDependsAdapter: NodeIDependsAdapter,
_nodeIDispatcherAdapter: NodeIDispatcherAdapter,
_nodeIKernelSessionListener: NodeIKernelSessionListener,
): void {
}
startNT(_session?: number): void {
}
getBdhUploadService() { return null; }
getECDHService() { return this.serviceManager.getService('NodeIKernelECDHService'); }
getMsgService() { return this.serviceManager.getService('NodeIKernelMsgService'); }
getProfileService() { return this.serviceManager.getService('NodeIKernelProfileService'); }
getProfileLikeService() { return this.serviceManager.getService('NodeIKernelProfileLikeService'); }
getGroupService() { return this.serviceManager.getService('NodeIKernelGroupService'); }
getStorageCleanService() { return this.serviceManager.getService('NodeIKernelStorageCleanService'); }
getBuddyService() { return this.serviceManager.getService('NodeIKernelBuddyService'); }
getRobotService() { return this.serviceManager.getService('NodeIKernelRobotService'); }
getTicketService() { return this.serviceManager.getService('NodeIKernelTicketService'); }
getTipOffService() { return this.serviceManager.getService('NodeIKernelTipOffService'); }
getNodeMiscService() { return this.serviceManager.getService('NodeIKernelNodeMiscService'); }
getRichMediaService() { return this.serviceManager.getService('NodeIKernelRichMediaService'); }
getMsgBackupService() { return this.serviceManager.getService('NodeIKernelMsgBackupService'); }
getAlbumService() { return this.serviceManager.getService('NodeIKernelAlbumService'); }
getTianShuService() { return this.serviceManager.getService('NodeIKernelTianShuService'); }
getUnitedConfigService() { return this.serviceManager.getService('NodeIKernelUnitedConfigService'); }
getSearchService() { return this.serviceManager.getService('NodeIKernelSearchService'); }
getDirectSessionService() { return null; }
getRDeliveryService() { return null; }
getAvatarService() { return this.serviceManager.getService('NodeIKernelAvatarService'); }
getFeedChannelService() { return null; }
getYellowFaceService() { return null; }
getCollectionService() { return this.serviceManager.getService('NodeIKernelCollectionService'); }
getSettingService() { return null; }
getQiDianService() { return null; }
getFileAssistantService() { return this.serviceManager.getService('NodeIKernelFileAssistantService'); }
getGuildService() { return null; }
getSkinService() { return null; }
getTestPerformanceService() { return this.serviceManager.getService('NodeIkernelTestPerformanceService'); }
getQQPlayService() { return null; }
getDbToolsService() { return this.serviceManager.getService('NodeIKernelDbToolsService'); }
getUixConvertService() { return this.serviceManager.getService('NodeIKernelUixConvertService'); }
getOnlineStatusService() { return this.serviceManager.getService('NodeIKernelOnlineStatusService'); }
getRemotingService() { return null; }
getGroupTabService() { return null; }
getGroupSchoolService() { return null; }
getLiteBusinessService() { return null; }
getGuildMsgService() { return null; }
getLockService() { return null; }
getMSFService() { return this.serviceManager.getService('NodeIKernelMSFService'); }
getGuildHotUpdateService() { return null; }
getAVSDKService() { return null; }
getRecentContactService() { return this.serviceManager.getService('NodeIKernelRecentContactService'); }
getConfigMgrService() { return null; }
}

1210
src/remote/serialize.cpp Normal file

File diff suppressed because it is too large Load Diff

131
src/remote/serialize.ts Normal file
View File

@@ -0,0 +1,131 @@
interface EncodedValue {
$type: string;
$value?: unknown;
}
interface EncodedNull {
$type: "null";
}
interface EncodedUndefined {
$type: "undefined";
}
interface EncodedPrimitive {
$type: "number" | "string" | "boolean";
$value: number | string | boolean;
}
interface EncodedBuffer {
$type: "Buffer";
$value: string;
}
interface EncodedMap {
$type: "Map";
$value: [EncodedValue, EncodedValue][];
}
interface EncodedArray {
$type: "Array";
$value: EncodedValue[];
}
interface EncodedObject {
$type: "Object";
$value: { [key: string]: EncodedValue };
}
type SerializedValue = EncodedNull | EncodedUndefined | EncodedPrimitive | EncodedBuffer | EncodedMap | EncodedArray | EncodedObject;
function rpc_encode<T>(value: T): SerializedValue {
if (value === null) return { $type: "null" };
if (value === undefined) return { $type: "undefined" };
if (typeof value === "number") return { $type: "number", $value: value };
if (typeof value === "string") return { $type: "string", $value: value };
if (typeof value === "boolean") return { $type: "boolean", $value: value };
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
// Buffer和Uint8Array都转成base64字符串
let base64: string = Buffer.from(value).toString("base64");
return { $type: "Buffer", $value: base64 };
}
if (value instanceof Map) {
let arr: [SerializedValue, SerializedValue][] = [];
for (let [k, v] of value.entries()) {
arr.push([rpc_encode(k), rpc_encode(v)]);
}
return { $type: "Map", $value: arr };
}
if (Array.isArray(value) || (typeof value === "object" && value !== null && typeof (value as unknown as ArrayLike<unknown>).length === "number")) {
// ArrayLike也认为是Array
let arr: SerializedValue[] = [];
const arrayLike = value as unknown as ArrayLike<unknown>;
for (let i = 0; i < arrayLike.length; i++) {
arr.push(rpc_encode(arrayLike[i]));
}
return { $type: "Array", $value: arr };
}
if (typeof value === "object" && value !== null) {
let obj: { [key: string]: SerializedValue } = {};
for (let k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
obj[k] = rpc_encode((value as Record<string, unknown>)[k]);
}
}
return { $type: "Object", $value: obj };
}
throw new Error("Unsupported type");
}
function rpc_decode<T = unknown>(obj: EncodedValue): T {
if (obj == null || typeof obj !== "object" || !("$type" in obj)) {
throw new Error("Invalid encoded object");
}
switch (obj.$type) {
case "null": return null as T;
case "undefined": return undefined as T;
case "number": return (obj as EncodedPrimitive).$value as T;
case "string": return (obj as EncodedPrimitive).$value as T;
case "boolean": return (obj as EncodedPrimitive).$value as T;
case "Buffer":
return Buffer.from((obj as EncodedBuffer).$value, "base64") as T;
case "Map":
{
let map = new Map();
for (let [k, v] of (obj as EncodedMap).$value) {
map.set(rpc_decode(k), rpc_decode(v));
}
return map as T;
}
case "Array":
{
let arr: unknown[] = [];
for (let item of (obj as EncodedArray).$value) {
arr.push(rpc_decode(item));
}
return arr as T;
}
case "Object":
{
let out: Record<string, unknown> = {};
for (let k in (obj as EncodedObject).$value) {
const value = (obj as EncodedObject).$value[k];
if (value !== undefined) {
out[k] = rpc_decode(value);
}
}
return out as T;
}
default:
throw new Error("Unknown $type: " + obj.$type);
}
}
export { rpc_encode, rpc_decode };
export type { SerializedValue };

114
src/remote/service.ts Normal file
View File

@@ -0,0 +1,114 @@
import { FuncKeys, NTEventWrapper } from "@/common/event";
import { ServiceNamingMapping } from "@/core";
export type ServiceMethodCommand = {
[Service in keyof ServiceNamingMapping]: `${Service}/${FuncKeys<ServiceNamingMapping[Service]>}`
}[keyof ServiceNamingMapping];
const LISTENER_COMMAND_PATTERN = /\/addKernel\w*Listener$/;
function isListenerCommand(command: ServiceMethodCommand): boolean {
return LISTENER_COMMAND_PATTERN.test(command);
}
export function createRemoteServiceServer<T extends keyof ServiceNamingMapping>(
serviceName: T,
ntevent: NTEventWrapper,
callback: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
): ServiceNamingMapping[T] {
return new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (isListenerCommand(command)) {
return async (..._args: any[]) => {
const listener = new Proxy(new class { }(), {
apply: (_target, _thisArg, _arguments) => {
return callback(command, ..._arguments);
}
});
return await (ntevent.callNoListenerEvent as any)(command, listener);
};
}
return async (...args: any[]) => {
return await (ntevent.callNoListenerEvent as any)(command, ...args);
};
}
});
}
// 避免重复远程注册 多份传输会消耗很大
export const listenerCmdRegisted = new Map<ServiceMethodCommand, boolean>();
// 已经注册的Listener实例托管
export const clientCallback = new Map<string, Array<(...args: any[]) => Promise<any>>>();
export async function handleServiceServerOnce(
command: ServiceMethodCommand,// 服务注册命令
recvListener: (command: string, ...args: any[]) => Promise<any>,//listener监听器
ntevent: NTEventWrapper,// 事件处理器
...args: any[]//实际参数
) {
if (isListenerCommand(command)) {
if (!listenerCmdRegisted.has(command)) {
listenerCmdRegisted.set(command, true);
return (ntevent.callNoListenerEvent as any)(command, new Proxy(new class { }(), {
get: (_target: any, prop: string) => {
return async (..._args: any[]) => {
let listenerCmd = `${command.split('/')[0]}/${prop}`;
recvListener(listenerCmd, ..._args);
};
}
}));
}
return 0;
}
return await (ntevent.callNoListenerEvent as (command: ServiceMethodCommand, ...args: any[]) => Promise<any>)(command, ...args);
}
export function createRemoteServiceClient<T extends keyof ServiceNamingMapping>(
serviceName: T,
receiverEvent: (command: ServiceMethodCommand, ...args: any[]) => Promise<any>
) {
const object = new Proxy(() => { }, {
get: (_target: any, functionName: string) => {
const command = `${serviceName}/${functionName}` as ServiceMethodCommand;
if (isListenerCommand(command)) {
return async (listener: Record<string, any>) => {
for (const key in listener) {
if (typeof listener[key] === 'function') {
const listenerCmd = `${command.split('/')[0]}/${key}`;
if (!clientCallback.has(listenerCmd)) {
clientCallback.set(listenerCmd, [listener[key].bind(listener)]);
} else {
clientCallback.get(listenerCmd)?.push(listener[key].bind(listener));
}
}
}
return await receiverEvent(command);
};
}
return async (...args: any[]) => {
return await receiverEvent(command, ...args);
};
}
});
const receiverListener = async function (command: string, ...args: any[]) {
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
};
return { receiverListener: receiverListener, object: object as ServiceNamingMapping[T] };
}
export async function receiverServiceListener(
command: string,
...args: any[]
) {
if (clientCallback.has(command)) {
return clientCallback.get(command)?.forEach(async (callback) => await callback(...args));
}
return 0;
}
export function clearServiceState() {
listenerCmdRegisted.clear();
clientCallback.clear();
}

23
src/remote/wrapper.ts Normal file
View File

@@ -0,0 +1,23 @@
import { NodeIKernelLoginService, NodeIQQNTWrapperEngine, NodeIQQNTWrapperSession, NodeQQNTWrapperUtil, WrapperNodeApi } from "@/core";
import { NodeIO3MiscService } from "@/core/services/NodeIO3MiscService";
import { dirname } from "path";
import { fileURLToPath } from "url";
export const LocalVirtualWrapper: WrapperNodeApi = {
NodeIO3MiscService: {
get: () => LocalVirtualWrapper.NodeIO3MiscService,
addO3MiscListener: () => 0,
setAmgomDataPiece: () => { },
reportAmgomWeather: () => { },
} as NodeIO3MiscService,
NodeQQNTWrapperUtil: {
get: () => LocalVirtualWrapper.NodeQQNTWrapperUtil,
getNTUserDataInfoConfig: function (): string {
let current_path = dirname(fileURLToPath(import.meta.url));
return current_path;
}
} as NodeQQNTWrapperUtil,
NodeIQQNTWrapperSession: {} as NodeIQQNTWrapperSession,
NodeIQQNTWrapperEngine: {} as NodeIQQNTWrapperEngine,
NodeIKernelLoginService: {} as NodeIKernelLoginService,
};